Repository: python-cmd2/cmd2
Pull request #1078 (2021-03-23 15:31:28)

onecmd_plus_hooks() now sets self.exit_code when a SystemExit handled


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f1739c6..4f3b9472 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
     * Settables now have new initialization parameters. It is now a required parameter to supply the reference to the
       object that holds the settable attribute. `cmd2.Cmd.settables` is no longer a public dict attribute - it is now a
       property that aggregates all Settables across all registered CommandSets.
+    * Failed transcript testing now sets self.exit_code to 1 instead of -1.
 * Enhancements
     * Added support for custom tab completion and up-arrow input history to `cmd2.Cmd2.read_input`.
       See [read_input.py](https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py)
@@ -34,6 +35,9 @@
           may have a prepended prefix.
         * Settables now allow changes to be applied to any arbitrary object attribute. It no longer needs to match an
           attribute added to the cmd2 instance itself.
+    * Raising ``SystemExit`` or calling ``sys.exit()`` in a command or hook function will set ``self.exit_code``
+      to the exit code used in those calls. It will also result in the command loop stopping.
+
 ## 1.5.0 (January 31, 2021)
 * Bug Fixes
     * Fixed bug where setting `always_show_hint=True` did not show a hint when completing `Settables`
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index f7188cb4..68ad8201 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2362,7 +2362,8 @@ class Cmd(cmd.Cmd):
         except KeyboardInterrupt as ex:
             if raise_keyboard_interrupt and not stop:
                 raise ex
-        except SystemExit:
+        except SystemExit as ex:
+            self.exit_code = ex.code
             stop = True
         except PassThroughException as ex:
             raise ex.wrapped_ex
@@ -2374,7 +2375,8 @@ class Cmd(cmd.Cmd):
             except KeyboardInterrupt as ex:
                 if raise_keyboard_interrupt and not stop:
                     raise ex
-            except SystemExit:
+            except SystemExit as ex:
+                self.exit_code = ex.code
                 stop = True
             except PassThroughException as ex:
                 raise ex.wrapped_ex
@@ -4748,7 +4750,7 @@ class Cmd(cmd.Cmd):
         transcripts_expanded = utils.files_from_glob_patterns(transcript_paths, access=os.R_OK)
         if not transcripts_expanded:
             self.perror('No test files found - nothing to test')
-            self.exit_code = -1
+            self.exit_code = 1
             return
 
         verinfo = ".".join(map(str, sys.version_info[:3]))
@@ -4785,7 +4787,7 @@ class Cmd(cmd.Cmd):
             self.perror(error_str[start:])
 
             # Return a failure error code to support automated transcript-based testing
-            self.exit_code = -1
+            self.exit_code = 1
 
     def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:  # pragma: no cover
         """
diff --git a/docs/features/commands.rst b/docs/features/commands.rst
index cc603f70..66745469 100644
--- a/docs/features/commands.rst
+++ b/docs/features/commands.rst
@@ -142,7 +142,7 @@ The ``cmd2.Cmd`` object sets an ``exit_code`` attribute to zero when it is
 instantiated. The value of this attribute is returned from the ``cmdloop()``
 call. Therefore, if you don't do anything with this attribute in your code,
 ``cmdloop()`` will (almost) always return zero. There are a few built-in
-``cmd2`` commands which set ``exit_code`` to ``-1`` if an error occurs.
+``cmd2`` commands which set ``exit_code`` to ``1`` if an error occurs.
 
 You can use this capability to easily return your own values to the operating
 system shell::
@@ -175,6 +175,9 @@ the following interaction::
     2
 
 
+Raising ``SystemExit(code)`` or calling ``sys.exit(code)`` in a command
+or hook function also sets ``self.exit_code`` and stops the program.
+
 Exception Handling
 ------------------
 
diff --git a/examples/exit_code.py b/examples/exit_code.py
index 80cef62f..440fc595 100755
--- a/examples/exit_code.py
+++ b/examples/exit_code.py
@@ -28,7 +28,7 @@ class ReplWithExitCode(cmd2.Cmd):
                 self.exit_code = int(arg_list[0])
             except ValueError:
                 self.perror("{} isn't a valid integer exit code".format(arg_list[0]))
-                self.exit_code = -1
+                self.exit_code = 1
 
         return True
 
diff --git a/examples/pirate.py b/examples/pirate.py
index f91b99d7..3a9b7b36 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -46,7 +46,7 @@ class Pirate(cmd2.Cmd):
             self.poutput('Now we gots {0} doubloons'.format(self.gold))
         if self.gold < 0:
             self.poutput("Off to debtorrr's prison.")
-            self.exit_code = -1
+            self.exit_code = 1
             stop = True
         return stop
 


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 91815d50..44d2b304 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -537,13 +537,16 @@ def test_system_exit_in_command(base_app, capsys):
     """Test raising SystemExit in a command"""
     import types
 
+    exit_code = 5
+
     def do_system_exit(self, _):
-        raise SystemExit
+        raise SystemExit(exit_code)
 
     setattr(base_app, 'do_system_exit', types.MethodType(do_system_exit, base_app))
 
     stop = base_app.onecmd_plus_hooks('system_exit')
     assert stop
+    assert base_app.exit_code == exit_code
 
 
 def test_passthrough_exception_in_command(base_app):
@@ -2323,7 +2326,7 @@ class ReplWithExitCode(cmd2.Cmd):
                 self.exit_code = int(arg_list[0])
             except ValueError:
                 self.perror("{} isn't a valid integer exit code".format(arg_list[0]))
-                self.exit_code = -1
+                self.exit_code = 1
 
         # Return True to stop the command loop
         return True
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 1e12d655..61b140ab 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -229,7 +229,7 @@ class Plugin:
     ) -> cmd2.plugin.CommandFinalizationData:
         """A command finalization hook which raises a SystemExit"""
         self.called_cmdfinalization += 1
-        raise SystemExit
+        raise SystemExit(5)
 
     def cmdfinalization_hook_keyboard_interrupt(
         self, data: cmd2.plugin.CommandFinalizationData
@@ -930,6 +930,7 @@ def test_cmdfinalization_hook_system_exit():
     stop = app.onecmd_plus_hooks('say hello')
     assert stop
     assert app.called_cmdfinalization == 1
+    assert app.exit_code == 5
 
 
 def test_cmdfinalization_hook_keyboard_interrupt():