diff options
Diffstat (limited to 'cmd2')
-rw-r--r-- | cmd2/cmd2.py | 59 | ||||
-rw-r--r-- | cmd2/utils.py | 4 |
2 files changed, 50 insertions, 13 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 3a01abc4..2e38529b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2114,10 +2114,10 @@ class Cmd(cmd.Cmd): ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n') rl_force_redisplay() return None - except Exception as e: + except Exception as ex: # Insert a newline so the exception doesn't print in the middle of the command line being tab completed self.perror() - self.pexcept(e) + self.pexcept(ex) rl_force_redisplay() return None @@ -2199,7 +2199,11 @@ class Cmd(cmd.Cmd): # Check if we are allowed to re-raise the KeyboardInterrupt if not self.sigint_protection: - raise KeyboardInterrupt("Got a keyboard interrupt") + self._raise_keyboard_interrupt() + + def _raise_keyboard_interrupt(self) -> None: + """Helper function to raise a KeyboardInterrupt""" + raise KeyboardInterrupt("Got a keyboard interrupt") def precmd(self, statement: Union[Statement, str]) -> Statement: """Hook method executed just before the command is executed by @@ -2430,9 +2434,9 @@ class Cmd(cmd.Cmd): line, add_to_history=add_to_history, raise_keyboard_interrupt=stop_on_keyboard_interrupt ): return True - except KeyboardInterrupt as e: + except KeyboardInterrupt as ex: if stop_on_keyboard_interrupt: - self.perror(e) + self.perror(ex) break return False @@ -2623,12 +2627,17 @@ class Cmd(cmd.Cmd): # Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs, # our sigint handler will forward it only to the most recent pipe process. This makes sure pipe # processes close in the right order (most recent first). - kwargs = dict() + kwargs: Dict[str, Any] = dict() if sys.platform == 'win32': kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP else: kwargs['start_new_session'] = True + # Attempt to run the pipe process in the user's preferred shell instead of the default behavior of using sh. + shell = os.environ.get("SHELL") + if shell: + kwargs['executable'] = shell + # For any stream that is a StdSim, we will use a pipe so we can capture its output proc = subprocess.Popen( # type: ignore[call-overload] statement.pipe_to, @@ -3825,8 +3834,8 @@ class Cmd(cmd.Cmd): orig_value = settable.get_value() new_value = settable.set_value(utils.strip_quotes(args.value)) # noinspection PyBroadException - except Exception as e: - self.perror(f"Error setting {args.param}: {e}") + except Exception as ex: + self.perror(f"Error setting {args.param}: {ex}") else: self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {new_value!r}") return @@ -3863,8 +3872,30 @@ class Cmd(cmd.Cmd): @with_argparser(shell_parser, preserve_quotes=True) def do_shell(self, args: argparse.Namespace) -> None: """Execute a command as if at the OS prompt""" + import signal import subprocess + kwargs: Dict[str, Any] = dict() + + # Set OS-specific parameters + if sys.platform.startswith('win'): + # Windows returns STATUS_CONTROL_C_EXIT when application stopped by Ctrl-C + ctrl_c_ret_code = 0xC000013A + else: + # On POSIX, Popen() returns -SIGINT when application stopped by Ctrl-C + ctrl_c_ret_code = signal.SIGINT.value * -1 + + # On POSIX with shell=True, Popen() defaults to /bin/sh as the shell. + # sh reports an incorrect return code for some applications when Ctrl-C is pressed within that + # application (e.g. less). Since sh received the SIGINT, it sets the return code to reflect being + # closed by SIGINT even though less did not exit upon a Ctrl-C press. In the same situation, other + # shells like bash and zsh report the actual return code of less. Therefore we will try to run the + # user's preferred shell which most likely will be something other than sh. This also allows the user + # to run builtin commands of their preferred shell. + shell = os.environ.get("SHELL") + if shell: + kwargs['executable'] = shell + # Create a list of arguments to shell tokens = [args.command] + args.command_args @@ -3876,11 +3907,12 @@ class Cmd(cmd.Cmd): # still receive the SIGINT since it is in the same process group as us. with self.sigint_protection: # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen( + proc = subprocess.Popen( # type: ignore[call-overload] expanded_command, stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable] stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable] shell=True, + **kwargs, ) proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) @@ -3889,6 +3921,11 @@ class Cmd(cmd.Cmd): # Save the return code of the application for use in a pyscript self.last_result = proc.returncode + # If the process was stopped by Ctrl-C, then inform the caller by raising a KeyboardInterrupt. + # This is to support things like stop_on_keyboard_interrupt in run_cmds_plus_hooks(). + if proc.returncode == ctrl_c_ret_code: + self._raise_keyboard_interrupt() + @staticmethod def _reset_py_display() -> None: """ @@ -4545,8 +4582,8 @@ class Cmd(cmd.Cmd): # then run the command and let the output go into our buffer try: stop = self.onecmd_plus_hooks(history_item, raise_keyboard_interrupt=True) - except KeyboardInterrupt as e: - self.perror(e) + except KeyboardInterrupt as ex: + self.perror(ex) stop = True commands_run += 1 diff --git a/cmd2/utils.py b/cmd2/utils.py index bb3d1a65..cbbd1800 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -601,8 +601,8 @@ class ProcReader: import signal if sys.platform.startswith('win'): - # cmd2 started the Windows process in a new process group. Therefore - # a CTRL_C_EVENT can't be sent to it. Send a CTRL_BREAK_EVENT instead. + # cmd2 started the Windows process in a new process group. Therefore we must send + # a CTRL_BREAK_EVENT since CTRL_C_EVENT signals cannot be generated for process groups. self._proc.send_signal(signal.CTRL_BREAK_EVENT) else: # Since cmd2 uses shell=True in its Popen calls, we need to send the SIGINT to |