1515- Redirection to file or paste buffer (clipboard) with > or >>
1616- Bash-style ``select`` available
1717
18- Note, if self.stdout is different than sys.stdout, then redirection with > and |
19- will only work if `self.poutput()` is used in place of `print`.
18+ Note: cmd2 redirection only captures output directed to self.stdout (e.g., via self.poutput()).
19+ Standard print() calls write directly to sys.stdout and are not captured. However, print() calls
20+ within pyscripts and the interactive Python shell are treated as command output and sent to
21+ self.stdout, allowing them to be captured.
2022
2123GitHub: https://github.com/python-cmd2/cmd2
2224Documentation: https://cmd2.readthedocs.io/
@@ -321,12 +323,12 @@ class AsyncAlert:
321323 timestamp : float = field (default_factory = time .monotonic , init = False )
322324
323325
326+ @dataclass
324327class _ConsoleCache (threading .local ):
325328 """Thread-local storage for cached Rich consoles used by core print methods."""
326329
327- def __init__ (self ) -> None :
328- self .stdout : Cmd2BaseConsole | None = None
329- self .stderr : Cmd2BaseConsole | None = None
330+ stdout : Cmd2BaseConsole | None = None
331+ stderr : Cmd2BaseConsole | None = None
330332
331333
332334class Cmd :
@@ -3223,13 +3225,8 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
32233225 """
32243226 import subprocess
32253227
3226- # Only redirect sys.stdout if it's the same as self.stdout
3227- stdouts_match = self .stdout == sys .stdout
3228-
32293228 # Initialize the redirection saved state
3230- redir_saved_state = utils .RedirectionSavedState (
3231- self .stdout , stdouts_match , self ._cur_pipe_proc_reader , self ._redirecting
3232- )
3229+ redir_saved_state = utils .RedirectionSavedState (self .stdout , self ._cur_pipe_proc_reader , self ._redirecting )
32333230
32343231 # The ProcReader for this command
32353232 cmd_pipe_proc_reader : utils .ProcReader | None = None
@@ -3286,8 +3283,6 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
32863283 cmd_pipe_proc_reader = utils .ProcReader (proc , self .stdout , sys .stderr )
32873284
32883285 self .stdout = new_stdout
3289- if stdouts_match :
3290- sys .stdout = self .stdout
32913286
32923287 elif statement .redirector in (constants .REDIRECTION_OVERWRITE , constants .REDIRECTION_APPEND ):
32933288 if statement .redirect_to :
@@ -3303,8 +3298,6 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
33033298 redir_saved_state .redirecting = True
33043299
33053300 self .stdout = new_stdout
3306- if stdouts_match :
3307- sys .stdout = self .stdout
33083301
33093302 else :
33103303 # Redirecting to a paste buffer
@@ -3324,8 +3317,6 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
33243317 redir_saved_state .redirecting = True
33253318
33263319 self .stdout = new_stdout
3327- if stdouts_match :
3328- sys .stdout = self .stdout
33293320
33303321 if statement .redirector == constants .REDIRECTION_APPEND :
33313322 self .stdout .write (current_paste_buffer )
@@ -3356,10 +3347,8 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
33563347 # Close the file or pipe that stdout was redirected to
33573348 self .stdout .close ()
33583349
3359- # Restore the stdout values
3350+ # Restore self. stdout
33603351 self .stdout = cast (TextIO , saved_redir_state .saved_self_stdout )
3361- if saved_redir_state .stdouts_match :
3362- sys .stdout = self .stdout
33633352
33643353 # Check if we need to wait for the process being piped to
33653354 if self ._cur_pipe_proc_reader is not None :
@@ -4429,8 +4418,6 @@ def print_topics(self, header: str, cmds: Sequence[str] | None, cmdlen: int, max
44294418
44304419 def _print_documented_command_topics (self , header : str , commands : Sequence [str ], verbose : bool ) -> None :
44314420 """Print topics which are documented commands, switching between verbose or traditional output."""
4432- import io
4433-
44344421 if not commands :
44354422 return
44364423
@@ -4444,34 +4431,11 @@ def _print_documented_command_topics(self, header: str, commands: Sequence[str],
44444431 )
44454432
44464433 # Try to get the documentation string for each command
4447- topics = self .get_help_topics ()
44484434 for command in commands :
44494435 if (command_func := self .get_command_func (command )) is None :
44504436 continue
44514437
4452- doc : str | None
4453-
4454- # Non-argparse commands can have help_functions for their documentation
4455- if command in topics :
4456- help_func = getattr (self , constants .HELP_FUNC_PREFIX + command )
4457- result = io .StringIO ()
4458-
4459- # try to redirect system stdout
4460- with contextlib .redirect_stdout (result ):
4461- # save our internal stdout
4462- stdout_orig = self .stdout
4463- try :
4464- # redirect our internal stdout
4465- self .stdout = cast (TextIO , result )
4466- help_func ()
4467- finally :
4468- with self .sigint_protection :
4469- # restore internal stdout
4470- self .stdout = stdout_orig
4471- doc = result .getvalue ()
4472-
4473- else :
4474- doc = command_func .__doc__
4438+ doc = command_func .__doc__
44754439
44764440 # Attempt to locate the first documentation block
44774441 cmd_desc = strip_doc_annotations (doc ) if doc else ""
@@ -4937,8 +4901,38 @@ def _run_python(self, *, pyscript: str | None = None) -> bool | None:
49374901 """
49384902 self .last_result = False
49394903
4904+ # Replace print() in the embedded Python environment. Standard print() writes to
4905+ # sys.stdout, which bypasses cmd2 redirection (e.g., run_pyscript script.py > out.txt).
4906+ # Using self.print_to(self.stdout) ensures output is capturable and respects 'allow_style'
4907+ # without requiring the user to have access to 'self'.
4908+ def py_print (
4909+ * objects : Any ,
4910+ sep : str = " " ,
4911+ end : str = "\n " ,
4912+ file : IO [str ] | None = None ,
4913+ flush : bool = False , # noqa: ARG001
4914+ ) -> None :
4915+ """Print objects to a stream, defaulting to self.stdout.
4916+
4917+ This is used as the print() function within interactive Python shells and pyscripts.
4918+ It wraps cmd2's print_to() method to honor output redirection and style settings.
4919+
4920+ :param objects: objects to print (including Rich objects)
4921+ :param sep: string to write between printed text. Defaults to " ".
4922+ :param end: string to write at end of printed text. Defaults to a newline.
4923+ :param file: file stream being written to. Defaults to self.stdout.
4924+ :param flush: ignored as Rich-based output is flushed automatically. Defaults to False.
4925+ """
4926+ if file is None :
4927+ file = self .stdout
4928+
4929+ self .print_to (file , * objects , sep = sep , end = end )
4930+
4931+ # Replace quit/exit in the embedded Python environment. Standard sys.exit()
4932+ # would kill the entire application process; raising EmbeddedConsoleExit
4933+ # allows the interpreter to return gracefully to the cmd2 prompt.
49404934 def py_quit () -> None :
4941- """Exit an interactive Python environment, callable from the interactive Python console ."""
4935+ """Exit an interactive Python shell or pyscript ."""
49424936 raise EmbeddedConsoleExit
49434937
49444938 from .py_bridge import PyBridge
@@ -4961,6 +4955,7 @@ def py_quit() -> None:
49614955 # it's OK for py_locals to contain objects which are editable in a pyscript.
49624956 local_vars = self .py_locals .copy ()
49634957 local_vars [self .py_bridge_name ] = py_bridge
4958+ local_vars ["print" ] = py_print
49644959 local_vars ["quit" ] = py_quit
49654960 local_vars ["exit" ] = py_quit
49664961
@@ -5120,19 +5115,13 @@ def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover
51205115 except NameError :
51215116 from IPython import start_ipython
51225117
5123- from IPython .terminal .interactiveshell import (
5124- TerminalInteractiveShell ,
5125- )
5126- from IPython .terminal .ipapp import (
5127- TerminalIPythonApp ,
5128- )
5118+ from IPython .terminal .interactiveshell import TerminalInteractiveShell
5119+ from IPython .terminal .ipapp import TerminalIPythonApp
51295120 except ImportError :
51305121 self .perror ("IPython package is not installed" )
51315122 return None
51325123
5133- from .py_bridge import (
5134- PyBridge ,
5135- )
5124+ from .py_bridge import PyBridge
51365125
51375126 if self .in_pyscript ():
51385127 self .perror ("Recursively entering interactive Python shells is not allowed" )
0 commit comments