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/
@@ -318,12 +320,12 @@ class AsyncAlert:
318320 timestamp : float = field (default_factory = time .monotonic , init = False )
319321
320322
323+ @dataclass
321324class _ConsoleCache (threading .local ):
322325 """Thread-local storage for cached Rich consoles used by core print methods."""
323326
324- def __init__ (self ) -> None :
325- self .stdout : Cmd2BaseConsole | None = None
326- self .stderr : Cmd2BaseConsole | None = None
327+ stdout : Cmd2BaseConsole | None = None
328+ stderr : Cmd2BaseConsole | None = None
327329
328330
329331class Cmd :
@@ -3191,13 +3193,8 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
31913193 """
31923194 import subprocess
31933195
3194- # Only redirect sys.stdout if it's the same as self.stdout
3195- stdouts_match = self .stdout == sys .stdout
3196-
31973196 # Initialize the redirection saved state
3198- redir_saved_state = utils .RedirectionSavedState (
3199- self .stdout , stdouts_match , self ._cur_pipe_proc_reader , self ._redirecting
3200- )
3197+ redir_saved_state = utils .RedirectionSavedState (self .stdout , self ._cur_pipe_proc_reader , self ._redirecting )
32013198
32023199 # The ProcReader for this command
32033200 cmd_pipe_proc_reader : utils .ProcReader | None = None
@@ -3254,8 +3251,6 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
32543251 cmd_pipe_proc_reader = utils .ProcReader (proc , self .stdout , sys .stderr )
32553252
32563253 self .stdout = new_stdout
3257- if stdouts_match :
3258- sys .stdout = self .stdout
32593254
32603255 elif statement .redirector in (constants .REDIRECTION_OVERWRITE , constants .REDIRECTION_APPEND ):
32613256 if statement .redirect_to :
@@ -3271,8 +3266,6 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
32713266 redir_saved_state .redirecting = True
32723267
32733268 self .stdout = new_stdout
3274- if stdouts_match :
3275- sys .stdout = self .stdout
32763269
32773270 else :
32783271 # Redirecting to a paste buffer
@@ -3292,8 +3285,6 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
32923285 redir_saved_state .redirecting = True
32933286
32943287 self .stdout = new_stdout
3295- if stdouts_match :
3296- sys .stdout = self .stdout
32973288
32983289 if statement .redirector == constants .REDIRECTION_APPEND :
32993290 self .stdout .write (current_paste_buffer )
@@ -3324,10 +3315,8 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
33243315 # Close the file or pipe that stdout was redirected to
33253316 self .stdout .close ()
33263317
3327- # Restore the stdout values
3318+ # Restore self. stdout
33283319 self .stdout = cast (TextIO , saved_redir_state .saved_self_stdout )
3329- if saved_redir_state .stdouts_match :
3330- sys .stdout = self .stdout
33313320
33323321 # Check if we need to wait for the process being piped to
33333322 if self ._cur_pipe_proc_reader is not None :
@@ -4395,8 +4384,6 @@ def print_topics(self, header: str, cmds: Sequence[str] | None, cmdlen: int, max
43954384
43964385 def _print_documented_command_topics (self , header : str , commands : Sequence [str ], verbose : bool ) -> None :
43974386 """Print topics which are documented commands, switching between verbose or traditional output."""
4398- import io
4399-
44004387 if not commands :
44014388 return
44024389
@@ -4410,34 +4397,11 @@ def _print_documented_command_topics(self, header: str, commands: Sequence[str],
44104397 )
44114398
44124399 # Try to get the documentation string for each command
4413- topics = self .get_help_topics ()
44144400 for command in commands :
44154401 if (command_func := self .get_command_func (command )) is None :
44164402 continue
44174403
4418- doc : str | None
4419-
4420- # Non-argparse commands can have help_functions for their documentation
4421- if command in topics :
4422- help_func = getattr (self , constants .HELP_FUNC_PREFIX + command )
4423- result = io .StringIO ()
4424-
4425- # try to redirect system stdout
4426- with contextlib .redirect_stdout (result ):
4427- # save our internal stdout
4428- stdout_orig = self .stdout
4429- try :
4430- # redirect our internal stdout
4431- self .stdout = cast (TextIO , result )
4432- help_func ()
4433- finally :
4434- with self .sigint_protection :
4435- # restore internal stdout
4436- self .stdout = stdout_orig
4437- doc = result .getvalue ()
4438-
4439- else :
4440- doc = command_func .__doc__
4404+ doc = command_func .__doc__
44414405
44424406 # Attempt to locate the first documentation block
44434407 cmd_desc = strip_doc_annotations (doc ) if doc else ""
@@ -4903,8 +4867,38 @@ def _run_python(self, *, pyscript: str | None = None) -> bool | None:
49034867 """
49044868 self .last_result = False
49054869
4870+ # Replace print() in the embedded Python environment. Standard print() writes to
4871+ # sys.stdout, which bypasses cmd2 redirection (e.g., run_pyscript script.py > out.txt).
4872+ # Using self.print_to(self.stdout) ensures output is capturable and respects 'allow_style'
4873+ # without requiring the user to have access to 'self'.
4874+ def py_print (
4875+ * objects : Any ,
4876+ sep : str = " " ,
4877+ end : str = "\n " ,
4878+ file : IO [str ] | None = None ,
4879+ flush : bool = False , # noqa: ARG001
4880+ ) -> None :
4881+ """Print objects to a stream, defaulting to self.stdout.
4882+
4883+ This is used as the print() function within interactive Python shells and pyscripts.
4884+ It wraps cmd2's print_to() method to honor output redirection and style settings.
4885+
4886+ :param objects: objects to print (including Rich objects)
4887+ :param sep: string to write between printed text. Defaults to " ".
4888+ :param end: string to write at end of printed text. Defaults to a newline.
4889+ :param file: file stream being written to. Defaults to self.stdout.
4890+ :param flush: ignored as Rich-based output is flushed automatically. Defaults to False.
4891+ """
4892+ if file is None :
4893+ file = self .stdout
4894+
4895+ self .print_to (file , * objects , sep = sep , end = end )
4896+
4897+ # Replace quit/exit in the embedded Python environment. Standard sys.exit()
4898+ # would kill the entire application process; raising EmbeddedConsoleExit
4899+ # allows the interpreter to return gracefully to the cmd2 prompt.
49064900 def py_quit () -> None :
4907- """Exit an interactive Python environment, callable from the interactive Python console ."""
4901+ """Exit an interactive Python shell or pyscript ."""
49084902 raise EmbeddedConsoleExit
49094903
49104904 from .py_bridge import PyBridge
@@ -4927,6 +4921,7 @@ def py_quit() -> None:
49274921 # it's OK for py_locals to contain objects which are editable in a pyscript.
49284922 local_vars = self .py_locals .copy ()
49294923 local_vars [self .py_bridge_name ] = py_bridge
4924+ local_vars ["print" ] = py_print
49304925 local_vars ["quit" ] = py_quit
49314926 local_vars ["exit" ] = py_quit
49324927
@@ -5086,19 +5081,13 @@ def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover
50865081 except NameError :
50875082 from IPython import start_ipython
50885083
5089- from IPython .terminal .interactiveshell import (
5090- TerminalInteractiveShell ,
5091- )
5092- from IPython .terminal .ipapp import (
5093- TerminalIPythonApp ,
5094- )
5084+ from IPython .terminal .interactiveshell import TerminalInteractiveShell
5085+ from IPython .terminal .ipapp import TerminalIPythonApp
50955086 except ImportError :
50965087 self .perror ("IPython package is not installed" )
50975088 return None
50985089
5099- from .py_bridge import (
5100- PyBridge ,
5101- )
5090+ from .py_bridge import PyBridge
51025091
51035092 if self .in_pyscript ():
51045093 self .perror ("Recursively entering interactive Python shells is not allowed" )
0 commit comments