Skip to content

Commit 2f0dbf5

Browse files
committed
Refactor exit_code implementation
cmd2.Cmd.cmdloop() now returns self.exit_code which should be an integer Also: - Refactored examples to call sys.exit(app.cmdloop()) in their __main__ - Running transcript tests now sets the exit_code accordingly based on success/failure - Updated CHANGELOG - Updated README - Updated Sphinx docs - Added unit test for case when transcript test fails
1 parent 1fd474f commit 2f0dbf5

31 files changed

+111
-51
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Fixed parsing issue in case where output redirection appears before a pipe. In that case, the pipe was given
99
precedence even though it appeared later in the command.
1010
* Fixed issue where quotes around redirection file paths were being lost in `Statement.expanded_command_line()`
11+
* Fixed a bug in how line numbers were calculated for transcript testing
1112
* Enhancements
1213
* Added capability to chain pipe commands and redirect their output (e.g. !ls -l | grep user | wc -l > out.txt)
1314
* `pyscript` limits a command's stdout capture to the same period that redirection does.
@@ -21,11 +22,14 @@
2122
readline. This makes debugging a lot easier since readline suppresses these exceptions.
2223
* Added support for custom Namespaces in the argparse decorators. See description of `ns_provider` argument
2324
for more information.
25+
* Transcript testing now sets the `exit_code` returned from `cmdloop` based on Success/Failure
2426
* Potentially breaking changes
2527
* Replaced `unquote_redirection_tokens()` with `unquote_specific_tokens()`. This was to support the fix
2628
that allows terminators in alias and macro values.
2729
* Changed `Statement.pipe_to` to a string instead of a list
2830
* `preserve_quotes` is now a keyword-only argument in the argparse decorators
31+
* Refactored so that `cmd2.Cmd.cmdloop()` returns the `exit_code` instead of a call to `sys.exit()`
32+
* It is now applicaiton developer's responsibility to treat the return value from `cmdloop()` accordingly
2933
* **Python 3.4 EOL notice**
3034
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019
3135
* This is the last release of `cmd2` which will support Python 3.4

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ A sample application for cmd2.
227227
"""
228228
import argparse
229229
import random
230+
import sys
230231
import cmd2
231232

232233
class CmdLineApp(cmd2.Cmd):
@@ -294,8 +295,8 @@ class CmdLineApp(cmd2.Cmd):
294295
self.poutput(' '.join(output))
295296

296297
if __name__ == '__main__':
297-
c = CmdLineApp()
298-
c.cmdloop()
298+
app = CmdLineApp()
299+
sys.exit(app.cmdloop())
299300
```
300301

301302
The following is a sample session running example.py.

cmd2/cmd2.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -553,8 +553,8 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
553553
# This boolean flag determines whether or not the cmd2 application can interact with the clipboard
554554
self.can_clip = can_clip
555555

556-
# This determines if a non-zero exit code should be used when exiting the application
557-
self.exit_code = None
556+
# This determines the value returned by cmdloop() when exiting the application
557+
self.exit_code = 0
558558

559559
# This lock should be acquired before doing any asynchronous changes to the terminal to
560560
# ensure the updates to the terminal don't interfere with the input being typed or output
@@ -3684,7 +3684,12 @@ class TestMyAppCase(Cmd2TestCase):
36843684
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
36853685
testcase = TestMyAppCase()
36863686
runner = unittest.TextTestRunner()
3687-
runner.run(testcase)
3687+
test_results = runner.run(testcase)
3688+
if test_results.wasSuccessful():
3689+
self.poutput('Tests passed', color=Fore.LIGHTGREEN_EX)
3690+
else:
3691+
self.perror('Tests failed', traceback_war=False)
3692+
self.exit_code = -1
36883693

36893694
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
36903695
"""
@@ -3932,14 +3937,15 @@ def _report_disabled_command_usage(self, *args, message_to_print: str, **kwargs)
39323937
"""
39333938
self.decolorized_write(sys.stderr, "{}\n".format(message_to_print))
39343939

3935-
def cmdloop(self, intro: Optional[str] = None) -> None:
3940+
def cmdloop(self, intro: Optional[str] = None) -> int:
39363941
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
39373942
39383943
_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
39393944
the following extra features provided by cmd2:
39403945
- commands at invocation
39413946
- transcript testing
39423947
- intro banner
3948+
- exit code
39433949
39443950
:param intro: if provided this overrides self.intro and serves as the intro banner printed once at start
39453951
"""
@@ -4002,8 +4008,7 @@ def cmdloop(self, intro: Optional[str] = None) -> None:
40024008
# Restore the original signal handler
40034009
signal.signal(signal.SIGINT, original_sigint_handler)
40044010

4005-
if self.exit_code is not None:
4006-
sys.exit(self.exit_code)
4011+
return self.exit_code
40074012

40084013
###
40094014
#

cmd2/transcript.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def _test_transcript(self, fname: str, transcript):
7171
line = next(transcript)
7272
except StopIteration:
7373
line = ''
74+
line_num += 1
7475
# Read the entirety of a multi-line command
7576
while line.startswith(self.cmdapp.continuation_prompt):
7677
command.append(line[len(self.cmdapp.continuation_prompt):])

docs/unfreefeatures.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ Presents numbered options to user, as bash ``select``.
204204
Exit code to shell
205205
==================
206206
The ``self.exit_code`` attribute of your ``cmd2`` application controls
207-
what exit code is sent to the shell when your application exits from
208-
``cmdloop()``.
207+
what exit code is returned from ``cmdloop()`` when it completes. It is your job to make sure that
208+
this exit code gets sent to the shell when your application exits by calling ``sys.exit(app.cmdloop())``.
209209

210210

211211
Asynchronous Feedback

examples/alias_startup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ def do_nothing(self, args):
2121

2222

2323
if __name__ == '__main__':
24+
import sys
2425
app = AliasAndStartup()
25-
app.cmdloop()
26+
sys.exit(app.cmdloop())

examples/arg_print.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,6 @@ def do_pprint(self, args, unknown):
6363

6464

6565
if __name__ == '__main__':
66+
import sys
6667
app = ArgumentAndOptionPrinter()
67-
app.cmdloop()
68+
sys.exit(app.cmdloop())

examples/async_printing.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def _alerter_thread_func(self) -> None:
197197

198198

199199
if __name__ == '__main__':
200+
import sys
200201
app = AlerterApp()
201202
app.set_window_title("Asynchronous Printer Test")
202-
app.cmdloop()
203+
sys.exit(app.cmdloop())

examples/cmd_as_argument.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import argparse
1616
import random
17-
import sys
1817

1918
import cmd2
2019

@@ -101,13 +100,17 @@ def main(argv=None):
101100

102101
c = CmdLineApp()
103102

103+
sys_exit_code = 0
104104
if args.command:
105105
# we have a command, run it and then exit
106106
c.onecmd_plus_hooks('{} {}'.format(args.command, ' '.join(args.command_args)))
107107
else:
108108
# we have no command, drop into interactive mode
109-
c.cmdloop()
109+
sys_exit_code = c.cmdloop()
110+
111+
return sys_exit_code
110112

111113

112114
if __name__ == '__main__':
115+
import sys
113116
sys.exit(main())

examples/colors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,6 @@ def do_mumble(self, args):
138138

139139

140140
if __name__ == '__main__':
141+
import sys
141142
c = CmdLineApp()
142-
c.cmdloop()
143+
sys.exit(c.cmdloop())

0 commit comments

Comments
 (0)