@@ -1965,12 +1965,12 @@ def ppaged(
19651965 soft_wrap = soft_wrap ,
19661966 ** (rich_print_kwargs if rich_print_kwargs is not None else {}),
19671967 )
1968- # As for a pipe: on Windows, the pager decodes with the console's code page .
1969- output_bytes = capture .get ().encode (utils . _pipe_encoding () , "replace" )
1968+ # As for a pipe: the pager gets UTF-8, and on Windows the console is UTF-8 while it runs .
1969+ output_bytes = capture .get ().encode ("utf-8" , "replace" )
19701970
19711971 # Prevent KeyboardInterrupts while in the pager. The pager application will
19721972 # still receive the SIGINT since it is in the same process group as us.
1973- with self .sigint_protection :
1973+ with self .sigint_protection , utils . _utf8_console () :
19741974 import subprocess
19751975
19761976 pipe_proc = subprocess .Popen ( # noqa: S602
@@ -3320,13 +3320,11 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
33203320 # Create a pipe with read and write sides
33213321 read_fd , write_fd = os .pipe ()
33223322
3323- # Open each side of the pipe. Both ends are given an explicit encoding: command output is rendered by Rich and
3324- # routinely contains non-ASCII, which the locale encoding cannot always represent. On Windows, that is the
3325- # console's code page, which console programs such as more decode with. It cannot represent everything either, so
3326- # replace what it lacks rather than fail the command.
3327- pipe_encoding = utils ._pipe_encoding ()
3328- subproc_stdin = open (read_fd , encoding = pipe_encoding ) # noqa: SIM115
3329- new_stdout : TextIO = cast (TextIO , open (write_fd , "w" , encoding = pipe_encoding , errors = "replace" )) # noqa: SIM115
3323+ # Open each side of the pipe. Both ends use UTF-8 rather than the locale's encoding, which cannot always represent
3324+ # the non-ASCII that Rich routinely renders. On Windows, the console is UTF-8 too while the pipe runs, for console
3325+ # programs such as more, which decode with its code page.
3326+ subproc_stdin = open (read_fd , encoding = "utf-8" ) # noqa: SIM115
3327+ new_stdout : TextIO = cast (TextIO , open (write_fd , "w" , encoding = "utf-8" )) # noqa: SIM115
33303328
33313329 # Isolate pipeline signals from cmd2. Terminal pipelines receive the foreground terminal; ProcReader relays their
33323330 # job-control stops.
@@ -3373,7 +3371,10 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
33733371 popen_command = f"read -r _ || exit 1; exec { user_shell } -c { shlex .quote (statement .redirect_to )} "
33743372 kwargs ["executable" ] = posix_shell
33753373
3376- with contextlib .ExitStack () as terminal_stack , contextlib .ExitStack () as gate_stack :
3374+ with contextlib .ExitStack () as pipe_stack , contextlib .ExitStack () as gate_stack :
3375+ # The console stays UTF-8 from before the pipe process starts, since a program may read the code page once as
3376+ # it starts, until _restore_output() has reaped it.
3377+ pipe_stack .enter_context (utils ._utf8_console ())
33773378 if terminal_fd is not None :
33783379 # Should cmd2 fail before opening the gate, the held pipeline reads EOF and exits.
33793380 gate_stack .callback (new_stdout .close )
@@ -3391,7 +3392,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
33913392 subproc_stdin .close ()
33923393 if terminal_fd is not None :
33933394 cmd_pipe_proc_reader = utils .ProcReader (proc , self .stdout , sys .stderr , terminal_fd = terminal_fd )
3394- terminal_stack .enter_context (cmd_pipe_proc_reader ._manage_terminal ())
3395+ pipe_stack .enter_context (cmd_pipe_proc_reader ._manage_terminal ())
33953396
33963397 # Popen was called with shell=True so the user can chain pipe commands and redirect their output
33973398 # like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process started
@@ -3429,14 +3430,13 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
34293430 pipe_fd , cmd_pipe_proc_reader , interruptible = lambda : not self .sigint_protection
34303431 )
34313432 ),
3432- encoding = pipe_encoding ,
3433- errors = "replace" ,
3433+ encoding = "utf-8" ,
34343434 )
34353435
34363436 self .stdout = new_stdout
34373437
3438- # Keep the pipeline's job control until _restore_output() reaps the pipe process.
3439- redir_saved_state .pipeline_job = terminal_stack .pop_all ()
3438+ # Keep the UTF-8 console and the pipeline's job control until _restore_output() reaps the pipe process.
3439+ redir_saved_state .pipe_context = pipe_stack .pop_all ()
34403440
34413441 elif statement .redirector in (constants .REDIRECTION_OVERWRITE , constants .REDIRECTION_APPEND ):
34423442 if statement .redirect_to :
@@ -3495,11 +3495,11 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
34953495 :param statement: Statement object which contains the parsed input from the user
34963496 :param saved_redir_state: contains information needed to restore state data
34973497 """
3498- # The pipeline's job control ends once its pipe process has been reaped.
3499- with contextlib .ExitStack () as terminal_stack :
3500- if saved_redir_state .pipeline_job is not None :
3501- terminal_stack .callback (saved_redir_state .pipeline_job .close )
3502- saved_redir_state .pipeline_job = None
3498+ # The UTF-8 console and the pipeline's job control end once its pipe process has been reaped.
3499+ with contextlib .ExitStack () as pipe_stack :
3500+ if saved_redir_state .pipe_context is not None :
3501+ pipe_stack .callback (saved_redir_state .pipe_context .close )
3502+ saved_redir_state .pipe_context = None
35033503
35043504 try :
35053505 if saved_redir_state .redirecting :
0 commit comments