Skip to content

Commit 143475f

Browse files
committed
All cmd2 built-in commands now populate self.last_result
1 parent 1a85bfe commit 143475f

File tree

3 files changed

+68
-8
lines changed

3 files changed

+68
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* macro tab completion
1313
* Tab completion of `CompletionItems` now includes divider row comprised of `Cmd.ruler` character.
1414
* Removed `--verbose` flag from set command since descriptions always show now.
15+
* All cmd2 built-in commands now populate `self.last_result`.
1516
* Deletions (potentially breaking changes)
1617
* Deleted ``set_choices_provider()`` and ``set_completer()`` which were deprecated in 2.1.2
1718

cmd2/cmd2.py

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,7 @@ def __init__(
358358
terminators=terminators, multiline_commands=multiline_commands, shortcuts=shortcuts
359359
)
360360

361-
# Stores results from the last command run to enable usage of results in a Python script or interactive console
362-
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
361+
# Stores results from the last command run to enable usage of results in a Python script or Python console
363362
self.last_result: Any = None
364363

365364
# Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command
@@ -3162,6 +3161,8 @@ def do_alias(self, args: argparse.Namespace) -> None:
31623161
@as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower())
31633162
def _alias_create(self, args: argparse.Namespace) -> None:
31643163
"""Create or overwrite an alias"""
3164+
self.last_result = False
3165+
31653166
# Validate the alias name
31663167
valid, errmsg = self.statement_parser.is_valid_command(args.name)
31673168
if not valid:
@@ -3191,6 +3192,7 @@ def _alias_create(self, args: argparse.Namespace) -> None:
31913192
self.poutput(f"Alias '{args.name}' {result}")
31923193

31933194
self.aliases[args.name] = value
3195+
self.last_result = True
31943196

31953197
# alias -> delete
31963198
alias_delete_help = "delete aliases"
@@ -3209,11 +3211,14 @@ def _alias_create(self, args: argparse.Namespace) -> None:
32093211
@as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help)
32103212
def _alias_delete(self, args: argparse.Namespace) -> None:
32113213
"""Delete aliases"""
3214+
self.last_result = True
3215+
32123216
if args.all:
32133217
self.aliases.clear()
32143218
self.poutput("All aliases deleted")
32153219
elif not args.names:
32163220
self.perror("Either --all or alias name(s) must be specified")
3221+
self.last_result = False
32173222
else:
32183223
for cur_name in utils.remove_duplicates(args.names):
32193224
if cur_name in self.aliases:
@@ -3243,6 +3248,8 @@ def _alias_delete(self, args: argparse.Namespace) -> None:
32433248
@as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help)
32443249
def _alias_list(self, args: argparse.Namespace) -> None:
32453250
"""List some or all aliases as 'alias create' commands"""
3251+
self.last_result = {} # Dict[alias_name, alias_value]
3252+
32463253
tokens_to_quote = constants.REDIRECTION_TOKENS
32473254
tokens_to_quote.extend(self.statement_parser.terminators)
32483255

@@ -3268,6 +3275,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
32683275
val += ' ' + ' '.join(command_args)
32693276

32703277
self.poutput(f"alias create {name} {val}")
3278+
self.last_result[name] = val
32713279

32723280
for name in not_found:
32733281
self.perror(f"Alias '{name}' not found")
@@ -3346,6 +3354,8 @@ def do_macro(self, args: argparse.Namespace) -> None:
33463354
@as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help)
33473355
def _macro_create(self, args: argparse.Namespace) -> None:
33483356
"""Create or overwrite a macro"""
3357+
self.last_result = False
3358+
33493359
# Validate the macro name
33503360
valid, errmsg = self.statement_parser.is_valid_command(args.name)
33513361
if not valid:
@@ -3420,6 +3430,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:
34203430
self.poutput(f"Macro '{args.name}' {result}")
34213431

34223432
self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list)
3433+
self.last_result = True
34233434

34243435
# macro -> delete
34253436
macro_delete_help = "delete macros"
@@ -3437,11 +3448,14 @@ def _macro_create(self, args: argparse.Namespace) -> None:
34373448
@as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help)
34383449
def _macro_delete(self, args: argparse.Namespace) -> None:
34393450
"""Delete macros"""
3451+
self.last_result = True
3452+
34403453
if args.all:
34413454
self.macros.clear()
34423455
self.poutput("All macros deleted")
34433456
elif not args.names:
34443457
self.perror("Either --all or macro name(s) must be specified")
3458+
self.last_result = False
34453459
else:
34463460
for cur_name in utils.remove_duplicates(args.names):
34473461
if cur_name in self.macros:
@@ -3471,6 +3485,8 @@ def _macro_delete(self, args: argparse.Namespace) -> None:
34713485
@as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help)
34723486
def _macro_list(self, args: argparse.Namespace) -> None:
34733487
"""List some or all macros as 'macro create' commands"""
3488+
self.last_result = {} # Dict[macro_name, macro_value]
3489+
34743490
tokens_to_quote = constants.REDIRECTION_TOKENS
34753491
tokens_to_quote.extend(self.statement_parser.terminators)
34763492

@@ -3496,6 +3512,7 @@ def _macro_list(self, args: argparse.Namespace) -> None:
34963512
val += ' ' + ' '.join(command_args)
34973513

34983514
self.poutput(f"macro create {name} {val}")
3515+
self.last_result[name] = val
34993516

35003517
for name in not_found:
35013518
self.perror(f"Macro '{name}' not found")
@@ -3552,6 +3569,8 @@ def complete_help_subcommands(
35523569
@with_argparser(help_parser)
35533570
def do_help(self, args: argparse.Namespace) -> None:
35543571
"""List available commands or provide detailed help for a specific command"""
3572+
self.last_result = True
3573+
35553574
if not args.command or args.verbose:
35563575
self._help_menu(args.verbose)
35573576

@@ -3586,6 +3605,7 @@ def do_help(self, args: argparse.Namespace) -> None:
35863605

35873606
# Set apply_style to False so help_error's style is not overridden
35883607
self.perror(err_msg, apply_style=False)
3608+
self.last_result = False
35893609

35903610
def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxcol: int) -> None:
35913611
"""
@@ -3797,6 +3817,7 @@ def do_shortcuts(self, _: argparse.Namespace) -> None:
37973817
sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0]))
37983818
result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts)
37993819
self.poutput(f"Shortcuts for other commands:\n{result}")
3820+
self.last_result = True
38003821

38013822
eof_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
38023823
description="Called when Ctrl-D is pressed", epilog=INTERNAL_COMMAND_EPILOG
@@ -3810,6 +3831,7 @@ def do_eof(self, _: argparse.Namespace) -> Optional[bool]:
38103831
"""
38113832
self.poutput()
38123833

3834+
# self.last_result will be set by do_quit()
38133835
# noinspection PyTypeChecker
38143836
return self.do_quit('')
38153837

@@ -3819,6 +3841,7 @@ def do_eof(self, _: argparse.Namespace) -> Optional[bool]:
38193841
def do_quit(self, _: argparse.Namespace) -> Optional[bool]:
38203842
"""Exit this application"""
38213843
# Return True to stop the command loop
3844+
self.last_result = True
38223845
return True
38233846

38243847
def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> str:
@@ -3931,6 +3954,8 @@ def complete_set_value(
39313954
@with_argparser(set_parser, preserve_quotes=True)
39323955
def do_set(self, args: argparse.Namespace) -> None:
39333956
"""Set a settable parameter or show current settings of parameters"""
3957+
self.last_result = False
3958+
39343959
if not self.settables:
39353960
self.pwarning("There are no settable parameters")
39363961
return
@@ -3952,6 +3977,7 @@ def do_set(self, args: argparse.Namespace) -> None:
39523977
self.perror(f"Error setting {args.param}: {ex}")
39533978
else:
39543979
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {new_value!r}")
3980+
self.last_result = True
39553981
return
39563982

39573983
# Show one settable
@@ -3974,11 +4000,14 @@ def do_set(self, args: argparse.Namespace) -> None:
39744000
table = SimpleTable(cols, divider_char=self.ruler)
39754001
self.poutput(table.generate_header())
39764002

3977-
# Build the table
4003+
# Build the table and populate self.last_result
4004+
self.last_result = {} # Dict[settable_name, settable_value]
4005+
39784006
for param in sorted(to_show, key=self.default_sort_key):
39794007
settable = self.settables[param]
39804008
row_data = [param, settable.get_value(), settable.description]
39814009
self.poutput(table.generate_data_row(row_data))
4010+
self.last_result[param] = settable.get_value()
39824011

39834012
shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt")
39844013
shell_parser.add_argument('command', help='the command to run', completer=shell_cmd_complete)
@@ -4182,6 +4211,7 @@ def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]:
41824211
after it sets up sys.argv for the script. (Defaults to None)
41834212
:return: True if running of commands should stop
41844213
"""
4214+
self.last_result = False
41854215

41864216
def py_quit() -> None:
41874217
"""Function callable from the interactive Python console to exit that environment"""
@@ -4198,6 +4228,8 @@ def py_quit() -> None:
41984228
self.perror("Recursively entering interactive Python shells is not allowed")
41994229
return None
42004230

4231+
self.last_result = True
4232+
42014233
try:
42024234
self._in_py = True
42034235
py_code_to_run = ''
@@ -4310,6 +4342,8 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]:
43104342
43114343
:return: True if running of commands should stop
43124344
"""
4345+
self.last_result = False
4346+
43134347
# Expand ~ before placing this path in sys.argv just as a shell would
43144348
args.script_path = os.path.expanduser(args.script_path)
43154349

@@ -4344,6 +4378,8 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
43444378
43454379
:return: True if running of commands should stop
43464380
"""
4381+
self.last_result = False
4382+
43474383
# Detect whether IPython is installed
43484384
try:
43494385
import traitlets.config.loader as TraitletsLoader # type: ignore[import]
@@ -4368,6 +4404,8 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
43684404
self.perror("Recursively entering interactive Python shells is not allowed")
43694405
return None
43704406

4407+
self.last_result = True
4408+
43714409
try:
43724410
self._in_py = True
43734411
py_bridge = PyBridge(self)
@@ -4456,6 +4494,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
44564494
44574495
:return: True if running of commands should stop
44584496
"""
4497+
self.last_result = False
44594498

44604499
# -v must be used alone with no other options
44614500
if args.verbose:
@@ -4471,6 +4510,8 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
44714510
return None
44724511

44734512
if args.clear:
4513+
self.last_result = True
4514+
44744515
# Clear command and readline history
44754516
self.history.clear()
44764517

@@ -4481,6 +4522,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
44814522
pass
44824523
except OSError as ex:
44834524
self.perror(f"Error removing history file '{self.persistent_history_file}': {ex}")
4525+
self.last_result = False
44844526
return None
44854527

44864528
if rl_type != RlType.NONE:
@@ -4495,7 +4537,9 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
44954537
self.perror("Cowardly refusing to run all previously entered commands.")
44964538
self.perror("If this is what you want to do, specify '1:' as the range of history.")
44974539
else:
4498-
return self.runcmds_plus_hooks(list(history.values()))
4540+
stop = self.runcmds_plus_hooks(list(history.values()))
4541+
self.last_result = True
4542+
return stop
44994543
elif args.edit:
45004544
import tempfile
45014545

@@ -4509,8 +4553,10 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
45094553
fobj.write(f'{command.raw}\n')
45104554
try:
45114555
self.run_editor(fname)
4556+
4557+
# self.last_resort will be set by do_run_script()
45124558
# noinspection PyTypeChecker
4513-
self.do_run_script(utils.quote_string(fname))
4559+
return self.do_run_script(utils.quote_string(fname))
45144560
finally:
45154561
os.remove(fname)
45164562
elif args.output_file:
@@ -4527,12 +4573,15 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
45274573
self.perror(f"Error saving history file '{full_path}': {ex}")
45284574
else:
45294575
self.pfeedback(f"{len(history)} command{plural} saved to {full_path}")
4576+
self.last_result = True
45304577
elif args.transcript:
4578+
# self.last_resort will be set by _generate_transcript()
45314579
self._generate_transcript(list(history.values()), args.transcript)
45324580
else:
45334581
# Display the history items retrieved
45344582
for idx, hi in history.items():
45354583
self.poutput(hi.pr(idx, script=args.script, expanded=args.expanded, verbose=args.verbose))
4584+
self.last_result = history
45364585
return None
45374586

45384587
def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryItem]':
@@ -4650,6 +4699,8 @@ def _persist_history(self) -> None:
46504699

46514700
def _generate_transcript(self, history: Union[List[HistoryItem], List[str]], transcript_file: str) -> None:
46524701
"""Generate a transcript file from a given history of commands"""
4702+
self.last_result = False
4703+
46534704
# Validate the transcript file path to make sure directory exists and write access is available
46544705
transcript_path = os.path.abspath(os.path.expanduser(transcript_file))
46554706
transcript_dir = os.path.dirname(transcript_path)
@@ -4732,6 +4783,7 @@ def _generate_transcript(self, history: Union[List[HistoryItem], List[str]], tra
47324783
else:
47334784
plural = 'commands and their outputs'
47344785
self.pfeedback(f"{commands_run} {plural} saved to transcript file '{transcript_path}'")
4786+
self.last_result = True
47354787

47364788
edit_description = (
47374789
"Run a text editor and optionally open a file with it\n"
@@ -4749,6 +4801,8 @@ def _generate_transcript(self, history: Union[List[HistoryItem], List[str]], tra
47494801
@with_argparser(edit_parser)
47504802
def do_edit(self, args: argparse.Namespace) -> None:
47514803
"""Run a text editor and optionally open a file with it"""
4804+
4805+
# self.last_result will be set by do_shell() which is called by run_editor()
47524806
self.run_editor(args.file_path)
47534807

47544808
def run_editor(self, file_path: Optional[str] = None) -> None:
@@ -4802,6 +4856,7 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
48024856
48034857
:return: True if running of commands should stop
48044858
"""
4859+
self.last_result = False
48054860
expanded_path = os.path.abspath(os.path.expanduser(args.script_path))
48064861

48074862
# Add some protection against accidentally running a Python file. The happens when users
@@ -4835,9 +4890,12 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
48354890
self._script_dir.append(os.path.dirname(expanded_path))
48364891

48374892
if args.transcript:
4893+
# self.last_resort will be set by _generate_transcript()
48384894
self._generate_transcript(script_commands, os.path.expanduser(args.transcript))
48394895
else:
4840-
return self.runcmds_plus_hooks(script_commands, stop_on_keyboard_interrupt=True)
4896+
stop = self.runcmds_plus_hooks(script_commands, stop_on_keyboard_interrupt=True)
4897+
self.last_result = True
4898+
return stop
48414899

48424900
finally:
48434901
with self.sigint_protection:
@@ -4871,6 +4929,7 @@ def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]:
48714929
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
48724930
relative_path = os.path.join(self._current_script_dir or '', file_path)
48734931

4932+
# self.last_result will be set by do_run_script()
48744933
# noinspection PyTypeChecker
48754934
return self.do_run_script(utils.quote_string(relative_path))
48764935

cmd2/py_bridge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ class CommandResult(NamedTuple):
7373
def __bool__(self) -> bool:
7474
"""Returns True if the command succeeded, otherwise False"""
7575

76-
# If data has a __bool__ method, then call it to determine success of command
77-
if self.data is not None and callable(getattr(self.data, '__bool__', None)):
76+
# If data was set, then use it to determine success
77+
if self.data is not None:
7878
return bool(self.data)
7979

8080
# Otherwise check if stderr was filled out

0 commit comments

Comments
 (0)