diff --git a/install.py b/install.py index 07b512e6..c33a3077 100644 --- a/install.py +++ b/install.py @@ -9023,6 +9023,18 @@ def _is_terminal(stream: TextIO) -> bool: return False +def _output_kind(stream: TextIO) -> str: + """"pipe" or "file" for what stream writes to, "" when neither is known. + `| tee log.txt` shows every line, `> log.txt` none of them.""" + try: + mode = os.fstat(stream.fileno()).st_mode + except (OSError, ValueError, AttributeError, io.UnsupportedOperation): + return "" + if stat.S_ISFIFO(mode) or stat.S_ISSOCK(mode): + return "pipe" + return "file" if stat.S_ISREG(mode) else "" + + def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: """One run of the installer, once the options are read.""" KEPT.reset() @@ -9035,8 +9047,12 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # to a prompt nobody could see. interactive = False if not listing and not args.check: - print("Output goes to a file, so nothing is asked: the run goes on as " - "with --no-input.", file=sys.stderr) + kind = _output_kind(real_stdout) + where = f"Output goes to a {kind}" if kind else "Output is not a terminal" + undo = "the pipe" if kind == "pipe" else "the redirection" + print(f"{where}, so nothing is asked: the run goes on as with --no-input. " + f"Add --no-input to say so, or run it without {undo} to answer.", + file=sys.stderr) print(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n") os_type = detect_os() diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 05d65bff..825ce6ee 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -618,6 +618,40 @@ class OutputToAFileAsksNothing(unittest.TestCase): self.assertNotIn("Enter) install everything", proc.stdout) self.assertEqual(len(installed), len(FakeRepository.PAYLOADS)) + @unittest.skipIf(os.name == "nt", "a pseudo-terminal for stdin") + def test_a_pipe_is_named_a_pipe(self): + """`install.py | tee log.txt` was told its output went to a file.""" + import pty + + master, slave = pty.openpty() + self.addCleanup(os.close, master) + self.addCleanup(os.close, slave) + with FakeRepository() as repo: + piped = subprocess.run( + [*repo.command(), "--dry-run"], env=repo.env(), stdin=slave, + capture_output=True, text=True, timeout=60, check=False) + log = repo.root / "log.txt" + with open(log, "w", encoding="utf-8") as handle: + filed = subprocess.run( + [*repo.command(), "--dry-run"], env=repo.env(), stdin=slave, + stdout=handle, stderr=subprocess.PIPE, text=True, timeout=60, + check=False) + self.assertIn("Output goes to a pipe, so nothing is asked", piped.stderr) + self.assertIn("run it without the pipe to answer", piped.stderr) + self.assertIn("Output goes to a file, so nothing is asked", filed.stderr) + self.assertIn("run it without the redirection to answer", filed.stderr) + for proc in (piped, filed): + self.assertIn("Add --no-input to say so", proc.stderr) + + def test_the_kind_of_output(self): + self.assertEqual(install._output_kind(io.StringIO()), "") + read, write = os.pipe() + with open(read, "rb") as reader, open(write, "w") as writer: + self.assertEqual(install._output_kind(writer), "pipe") + del reader + with tempfile.TemporaryFile("w", dir=REPO_ROOT / "tmp") as handle: + self.assertEqual(install._output_kind(handle), "file") + class _Tty(io.StringIO): def isatty(self) -> bool: