From 64522ccc4291384d7080567dd50061ea4db66668 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 23 May 2019 15:30:39 -0400 Subject: Change load to run scripts immediately instead of queueing commands --- cmd2/cmd2.py | 105 ++++++++++++++++++++++++----------------------------------- 1 file changed, 42 insertions(+), 63 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 9d36e1b4..c11cdeef 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -399,10 +399,10 @@ class Cmd(cmd.Cmd): 'timing': 'Report execution times'} # Commands to exclude from the help menu and tab completion - self.hidden_commands = ['eof', 'eos', '_relative_load'] + self.hidden_commands = ['eof', '_relative_load'] # Commands to exclude from the history command - self.exclude_from_history = '''history edit eof eos'''.split() + self.exclude_from_history = '''history edit eof'''.split() # Command aliases and macros self.macros = dict() @@ -1842,43 +1842,20 @@ class Cmd(cmd.Cmd): def runcmds_plus_hooks(self, cmds: List[str]) -> bool: """Convenience method to run multiple commands by onecmd_plus_hooks. - This method adds the given cmds to the command queue and processes the - queue until completion or an error causes it to abort. Scripts that are - loaded will have their commands added to the queue. Scripts may even - load other scripts recursively. This means, however, that you should not - use this method if there is a running cmdloop or some other event-loop. - This method is only intended to be used in "one-off" scenarios. - - NOTE: You may need this method even if you only have one command. If - that command is a load, then you will need this command to fully process - all the subsequent commands that are loaded from the script file. This - is an improvement over onecmd_plus_hooks, which expects to be used - inside of a command loop which does the processing of loaded commands. - - Example: cmd_obj.runcmds_plus_hooks(['load myscript.txt']) - :param cmds: command strings suitable for onecmd_plus_hooks. :return: True implies the entire application should exit. """ stop = False - self.cmdqueue = list(cmds) + self.cmdqueue try: - while self.cmdqueue and not stop: - line = self.cmdqueue.pop(0) - if self.echo and line != 'eos': + for line in cmds: + if self.echo: self.poutput('{}{}'.format(self.prompt, line)) stop = self.onecmd_plus_hooks(line) + if stop: + break finally: - # Clear out the command queue and script directory stack, just in - # case we hit an error and they were not completed. - self.cmdqueue = [] - self._script_dir = [] - # NOTE: placing this return here inside the finally block will - # swallow exceptions. This is consistent with what is done in - # onecmd_plus_hooks and _cmdloop, although it may not be - # necessary/desired here. return stop def _complete_statement(self, line: str) -> Statement: @@ -2306,8 +2283,7 @@ class Cmd(cmd.Cmd): if self.cmdqueue: # Run command out of cmdqueue if nonempty (populated by load command or commands at invocation) line = self.cmdqueue.pop(0) - - if self.echo and line != 'eos': + if self.echo: self.poutput('{}{}'.format(self.prompt, line)) else: # Otherwise, read a command from stdin @@ -2337,8 +2313,6 @@ class Cmd(cmd.Cmd): readline.rl.mode._display_completions = orig_pyreadline_display self.cmdqueue.clear() - self._script_dir.clear() - return stop # ----- Alias sub-command functions ----- @@ -3437,6 +3411,7 @@ class Cmd(cmd.Cmd): self.perror("If this is what you want to do, specify '1:' as the range of history.", traceback_war=False) else: + # TODO: Call runcmds_plus_hoodks and return its stop value for runme in history: self.pfeedback(runme) if runme: @@ -3452,7 +3427,8 @@ class Cmd(cmd.Cmd): fobj.write('{}\n'.format(command)) try: self.do_edit(fname) - self.do_load(fname) + # TODO: Return stop + stop = self.do_load(fname) except Exception: raise finally: @@ -3579,22 +3555,11 @@ class Cmd(cmd.Cmd): else: return None - @with_argparser(ACArgumentParser(epilog=INTERNAL_COMMAND_EPILOG)) - def do_eos(self, _: argparse.Namespace) -> None: - """Handle cleanup when a script has finished executing""" - if self._script_dir: - self._script_dir.pop() - load_description = ("Run commands in script file that is encoded as either ASCII or UTF-8 text\n" "\n" "Script should contain one command per line, just like the command would be\n" "typed in the console.\n" "\n" - "It loads commands from a script file into a queue and then the normal cmd2\n" - "REPL resumes control and executes the commands in the queue in FIFO order.\n" - "If you attempt to redirect/pipe a load command, it will capture the output\n" - "of the load command itself, not what it adds to the queue.\n" - "\n" "If the -r/--record_transcript flag is used, this command instead records\n" "the output of the script commands to a transcript for testing purposes.\n" ) @@ -3606,46 +3571,58 @@ class Cmd(cmd.Cmd): ACTION_ARG_CHOICES, ('path_complete',)) @with_argparser(load_parser) - def do_load(self, args: argparse.Namespace) -> None: - """Run commands in script file that is encoded as either ASCII or UTF-8 text""" + def do_load(self, args: argparse.Namespace) -> bool: + """ + Run commands in script file that is encoded as either ASCII or UTF-8 text + :return: True if cmdloop() should exit, False otherwise + """ expanded_path = os.path.abspath(os.path.expanduser(args.script_path)) # Make sure the path exists and we can access it if not os.path.exists(expanded_path): self.perror("'{}' does not exist or cannot be accessed".format(expanded_path), traceback_war=False) - return + return False # Make sure expanded_path points to a file if not os.path.isfile(expanded_path): self.perror("'{}' is not a file".format(expanded_path), traceback_war=False) - return + return False # Make sure the file is not empty if os.path.getsize(expanded_path) == 0: self.perror("'{}' is empty".format(expanded_path), traceback_war=False) - return + return False # Make sure the file is ASCII or UTF-8 encoded text if not utils.is_text_file(expanded_path): self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path), traceback_war=False) - return + return False try: - # Read all lines of the script and insert into the head of the - # command queue. Add an "end of script (eos)" command to cleanup the - # self._script_dir list when done. + # Read all lines of the script with open(expanded_path, encoding='utf-8') as target: script_commands = target.read().splitlines() except OSError as ex: # pragma: no cover self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex)) - return + return False - if args.transcript: - self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) - return + stop = False + orig_script_dir_count = len(self._script_dir) - self.cmdqueue = script_commands + ['eos'] + self.cmdqueue - self._script_dir.append(os.path.dirname(expanded_path)) + try: + self._script_dir.append(os.path.dirname(expanded_path)) + + if args.transcript: + self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) + else: + stop = self.runcmds_plus_hooks(script_commands) + + finally: + with self.sigint_protection: + # Check if a script dir was added before an exception occurred + if orig_script_dir_count != len(self._script_dir): + self._script_dir.pop() + return stop relative_load_description = load_description relative_load_description += ("\n\n" @@ -3659,12 +3636,14 @@ class Cmd(cmd.Cmd): relative_load_parser.add_argument('file_path', help='a file path pointing to a script') @with_argparser(relative_load_parser) - def do__relative_load(self, args: argparse.Namespace) -> None: - """Run commands in script file that is encoded as either ASCII or UTF-8 text""" + def do__relative_load(self, args: argparse.Namespace) -> bool: + """ + Run commands in script file that is encoded as either ASCII or UTF-8 text + """ file_path = args.file_path # NOTE: Relative path is an absolute path, it is just relative to the current script directory relative_path = os.path.join(self._current_script_dir or '', file_path) - self.do_load(relative_path) + return self.do_load(relative_path) def run_transcript_tests(self, callargs: List[str]) -> None: """Runs transcript tests for provided file(s). -- cgit v1.2.1 From c3f0794a55e10dfce37fdbd7c1e8f7c1e48cf1b3 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 23 May 2019 15:59:50 -0400 Subject: Returning value of stop from history command when it runs a script --- cmd2/cmd2.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c11cdeef..0ce6cbd1 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3349,7 +3349,7 @@ class Cmd(cmd.Cmd): history_parser.add_argument('arg', nargs='?', help=history_arg_help) @with_argparser(history_parser) - def do_history(self, args: argparse.Namespace) -> None: + def do_history(self, args: argparse.Namespace) -> Optional[bool]: """View, run, edit, save, or clear previously entered commands""" # -v must be used alone with no other options @@ -3411,7 +3411,7 @@ class Cmd(cmd.Cmd): self.perror("If this is what you want to do, specify '1:' as the range of history.", traceback_war=False) else: - # TODO: Call runcmds_plus_hoodks and return its stop value + # TODO: Call runcmds_plus_hooks and return its stop value for runme in history: self.pfeedback(runme) if runme: @@ -3425,14 +3425,15 @@ class Cmd(cmd.Cmd): fobj.write('{}\n'.format(command.expanded.rstrip())) else: fobj.write('{}\n'.format(command)) + stop = False try: self.do_edit(fname) - # TODO: Return stop stop = self.do_load(fname) except Exception: raise finally: os.remove(fname) + return stop elif args.output_file: try: with open(os.path.expanduser(args.output_file), 'w') as fobj: @@ -3620,7 +3621,7 @@ class Cmd(cmd.Cmd): finally: with self.sigint_protection: # Check if a script dir was added before an exception occurred - if orig_script_dir_count != len(self._script_dir): + if orig_script_dir_count != len(self._script_dir): self._script_dir.pop() return stop -- cgit v1.2.1 From d333a992292ddeb40d24d82a6bef0d4ed35db37a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 23 May 2019 22:20:02 -0400 Subject: Fixed spelling --- cmd2/cmd2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 0ce6cbd1..b43ea9f1 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3670,7 +3670,7 @@ class Cmd(cmd.Cmd): self.decolorized_write(sys.stderr, stream.read()) self.poutput('Tests passed', color=Fore.LIGHTGREEN_EX) else: - # Strip off the initial trackeback which isn't particularly useful for end users + # Strip off the initial traceback which isn't particularly useful for end users error_str = stream.read() end_of_trace = error_str.find('AssertionError:') file_offset = error_str[end_of_trace:].find('File ') -- cgit v1.2.1 From c3fe0573e2877c76adc6209453a92ef82ff6b6f2 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 May 2019 12:55:06 -0400 Subject: Removed most return statements from finally blocks to avoid hiding exceptions --- cmd2/cmd2.py | 70 +++++++++++++++++++++++++++--------------------------------- 1 file changed, 31 insertions(+), 39 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index b43ea9f1..32d0ae09 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1718,7 +1718,7 @@ class Cmd(cmd.Cmd): :param pyscript_bridge_call: This should only ever be set to True by PyscriptBridge to signify the beginning of an app() call in a pyscript. It is used to enable/disable the storage of the command's stdout. - :return: True if cmdloop() should exit, False otherwise + :return: True if running of commands should stop """ import datetime @@ -1843,20 +1843,16 @@ class Cmd(cmd.Cmd): """Convenience method to run multiple commands by onecmd_plus_hooks. :param cmds: command strings suitable for onecmd_plus_hooks. - :return: True implies the entire application should exit. - + :return: True if running of commands should stop """ - stop = False - try: - for line in cmds: - if self.echo: - self.poutput('{}{}'.format(self.prompt, line)) + for line in cmds: + if self.echo: + self.poutput('{}{}'.format(self.prompt, line)) - stop = self.onecmd_plus_hooks(line) - if stop: - break - finally: - return stop + if self.onecmd_plus_hooks(line): + return True + + return False def _complete_statement(self, line: str) -> Statement: """Keep accepting lines of input until the command is complete. @@ -2240,14 +2236,12 @@ class Cmd(cmd.Cmd): return line.strip() - def _cmdloop(self) -> bool: + def _cmdloop(self) -> None: """Repeatedly issue a prompt, accept input, parse an initial prefix off the received input, and dispatch to action methods, passing them the remainder of the line as argument. This serves the same role as cmd.cmdloop(). - - :return: True implies the entire application should exit. """ # An almost perfect copy from Cmd; however, the pseudo_raw_input portion # has been split out so that it can be called separately @@ -2277,9 +2271,8 @@ class Cmd(cmd.Cmd): # Enable tab completion readline.parse_and_bind(self.completekey + ": complete") - stop = False try: - while not stop: + while True: if self.cmdqueue: # Run command out of cmdqueue if nonempty (populated by load command or commands at invocation) line = self.cmdqueue.pop(0) @@ -2297,7 +2290,8 @@ class Cmd(cmd.Cmd): line = '' # Run the command along with all associated pre and post hooks - stop = self.onecmd_plus_hooks(line) + if self.onecmd_plus_hooks(line): + break finally: if self.use_rawinput and self.completekey and rl_type != RlType.NONE: @@ -2311,9 +2305,7 @@ class Cmd(cmd.Cmd): elif rl_type == RlType.PYREADLINE: # noinspection PyUnresolvedReferences readline.rl.mode._display_completions = orig_pyreadline_display - self.cmdqueue.clear() - return stop # ----- Alias sub-command functions ----- @@ -3316,7 +3308,9 @@ class Cmd(cmd.Cmd): embed(banner1=banner, exit_msg=exit_msg) load_ipy(bridge) - history_parser = ACArgumentParser() + history_description = "View, run, edit, save, or clear previously entered commands" + + history_parser = ACArgumentParser(description=history_description) history_action_group = history_parser.add_mutually_exclusive_group() history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items') history_action_group.add_argument('-e', '--edit', action='store_true', @@ -3350,7 +3344,10 @@ class Cmd(cmd.Cmd): @with_argparser(history_parser) def do_history(self, args: argparse.Namespace) -> Optional[bool]: - """View, run, edit, save, or clear previously entered commands""" + """ + View, run, edit, save, or clear previously entered commands + :return: True if running of commands should stop + """ # -v must be used alone with no other options if args.verbose: @@ -3425,15 +3422,11 @@ class Cmd(cmd.Cmd): fobj.write('{}\n'.format(command.expanded.rstrip())) else: fobj.write('{}\n'.format(command)) - stop = False try: self.do_edit(fname) - stop = self.do_load(fname) - except Exception: - raise + return self.do_load(fname) finally: os.remove(fname) - return stop elif args.output_file: try: with open(os.path.expanduser(args.output_file), 'w') as fobj: @@ -3572,32 +3565,32 @@ class Cmd(cmd.Cmd): ACTION_ARG_CHOICES, ('path_complete',)) @with_argparser(load_parser) - def do_load(self, args: argparse.Namespace) -> bool: + def do_load(self, args: argparse.Namespace) -> Optional[bool]: """ Run commands in script file that is encoded as either ASCII or UTF-8 text - :return: True if cmdloop() should exit, False otherwise + :return: True if running of commands should stop """ expanded_path = os.path.abspath(os.path.expanduser(args.script_path)) # Make sure the path exists and we can access it if not os.path.exists(expanded_path): self.perror("'{}' does not exist or cannot be accessed".format(expanded_path), traceback_war=False) - return False + return # Make sure expanded_path points to a file if not os.path.isfile(expanded_path): self.perror("'{}' is not a file".format(expanded_path), traceback_war=False) - return False + return # Make sure the file is not empty if os.path.getsize(expanded_path) == 0: self.perror("'{}' is empty".format(expanded_path), traceback_war=False) - return False + return # Make sure the file is ASCII or UTF-8 encoded text if not utils.is_text_file(expanded_path): self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path), traceback_war=False) - return False + return try: # Read all lines of the script @@ -3605,9 +3598,8 @@ class Cmd(cmd.Cmd): script_commands = target.read().splitlines() except OSError as ex: # pragma: no cover self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex)) - return False + return - stop = False orig_script_dir_count = len(self._script_dir) try: @@ -3616,14 +3608,13 @@ class Cmd(cmd.Cmd): if args.transcript: self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) else: - stop = self.runcmds_plus_hooks(script_commands) + return self.runcmds_plus_hooks(script_commands) finally: with self.sigint_protection: # Check if a script dir was added before an exception occurred if orig_script_dir_count != len(self._script_dir): self._script_dir.pop() - return stop relative_load_description = load_description relative_load_description += ("\n\n" @@ -3637,9 +3628,10 @@ class Cmd(cmd.Cmd): relative_load_parser.add_argument('file_path', help='a file path pointing to a script') @with_argparser(relative_load_parser) - def do__relative_load(self, args: argparse.Namespace) -> bool: + def do__relative_load(self, args: argparse.Namespace) -> Optional[bool]: """ Run commands in script file that is encoded as either ASCII or UTF-8 text + :return: True if running of commands should stop """ file_path = args.file_path # NOTE: Relative path is an absolute path, it is just relative to the current script directory -- cgit v1.2.1 From ba4de1da1a7cecaf65c264b31528b82183daacbb Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 May 2019 13:50:38 -0400 Subject: Stopping transcript generation if a command returns True for its stop value --- cmd2/cmd2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 32d0ae09..8911a188 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3491,13 +3491,16 @@ class Cmd(cmd.Cmd): membuf = io.StringIO() self.stdout = membuf # then run the command and let the output go into our buffer - self.onecmd_plus_hooks(history_item) + stop = self.onecmd_plus_hooks(history_item) # rewind the buffer to the beginning membuf.seek(0) # get the output out of the buffer output = membuf.read() # and add the regex-escaped output to the transcript transcript += output.replace('/', r'\/') + # check if we are supposed to stop + if stop: + break finally: with self.sigint_protection: # Restore altered attributes to their original state -- cgit v1.2.1 From a37dd5daf8914ce590440c2ccb2cf45e71a4170b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 May 2019 13:56:52 -0400 Subject: Fixed count in output message printed after transcript generation --- cmd2/cmd2.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8911a188..6efdda2c 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3457,6 +3457,7 @@ class Cmd(cmd.Cmd): traceback_war=False) return + commands_run = 0 try: with self.sigint_protection: # Disable echo while we manually redirect stdout to a StringIO buffer @@ -3490,14 +3491,18 @@ class Cmd(cmd.Cmd): # of the command membuf = io.StringIO() self.stdout = membuf + # then run the command and let the output go into our buffer stop = self.onecmd_plus_hooks(history_item) + commands_run += 1 + # rewind the buffer to the beginning membuf.seek(0) # get the output out of the buffer output = membuf.read() # and add the regex-escaped output to the transcript transcript += output.replace('/', r'\/') + # check if we are supposed to stop if stop: break @@ -3515,12 +3520,12 @@ class Cmd(cmd.Cmd): self.perror('Failed to save transcript: {}'.format(ex), traceback_war=False) else: # and let the user know what we did - if len(history) > 1: + if commands_run > 1: plural = 'commands and their outputs' else: plural = 'command and its output' msg = '{} {} saved to transcript file {!r}' - self.pfeedback(msg.format(len(history), plural, transcript_file)) + self.pfeedback(msg.format(commands_run, plural, transcript_file)) edit_description = ("Edit a file in a text editor\n" "\n" -- cgit v1.2.1 From 1178bda4d68608a2cd6e34a6bd60639debc92cf1 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 May 2019 14:23:54 -0400 Subject: Since transcript generation actually runs the commands, it now returns whether a command returned stop. This means the command loop will stop once transcript generation completes if a command did return stop. Otherwise there is a risk in continuing to run if the application's state has already been marked to close. --- cmd2/cmd2.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6efdda2c..a4d84adf 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3440,14 +3440,17 @@ class Cmd(cmd.Cmd): except Exception as e: self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False) elif args.transcript: - self._generate_transcript(history, args.transcript) + return self._generate_transcript(history, args.transcript) else: # Display the history items retrieved for hi in history: self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose)) - def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None: - """Generate a transcript file from a given history of commands.""" + def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> Optional[bool]: + """ + Generate a transcript file from a given history of commands + :return: True if running of commands should stop + """ import io # Validate the transcript file path to make sure directory exists and write access is available transcript_path = os.path.abspath(os.path.expanduser(transcript_file)) @@ -3458,6 +3461,7 @@ class Cmd(cmd.Cmd): return commands_run = 0 + stop = False try: with self.sigint_protection: # Disable echo while we manually redirect stdout to a StringIO buffer @@ -3527,6 +3531,8 @@ class Cmd(cmd.Cmd): msg = '{} {} saved to transcript file {!r}' self.pfeedback(msg.format(commands_run, plural, transcript_file)) + return stop + edit_description = ("Edit a file in a text editor\n" "\n" "The editor used is determined by a settable parameter. To set it:\n" @@ -3614,7 +3620,7 @@ class Cmd(cmd.Cmd): self._script_dir.append(os.path.dirname(expanded_path)) if args.transcript: - self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) + return self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) else: return self.runcmds_plus_hooks(script_commands) -- cgit v1.2.1 From da5f03fb515d0a751d356a0f664ccb4b3df83eb1 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 May 2019 14:31:29 -0400 Subject: Running commands from history stops if a command returns True for its stop value. The command loop will also terminate. --- cmd2/cmd2.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index a4d84adf..51d5b5b6 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3408,11 +3408,7 @@ class Cmd(cmd.Cmd): self.perror("If this is what you want to do, specify '1:' as the range of history.", traceback_war=False) else: - # TODO: Call runcmds_plus_hooks and return its stop value - for runme in history: - self.pfeedback(runme) - if runme: - self.onecmd_plus_hooks(runme) + return self.runcmds_plus_hooks(history) elif args.edit: import tempfile fd, fname = tempfile.mkstemp(suffix='.txt', text=True) -- cgit v1.2.1 From d1438e07d48a11ef4405305277422b84cbaef06a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 May 2019 15:13:54 -0400 Subject: Removing duplicate functionality by calling runcmds_plus_hooks() to run cmdqueue --- cmd2/cmd2.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 51d5b5b6..d1a5552e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1840,9 +1840,11 @@ class Cmd(cmd.Cmd): self.perror(ex) def runcmds_plus_hooks(self, cmds: List[str]) -> bool: - """Convenience method to run multiple commands by onecmd_plus_hooks. + """ + Used when commands are being run in an automated fashion like text scripts or history replays. + The prompt and command line for each command will be printed if echo is True. - :param cmds: command strings suitable for onecmd_plus_hooks. + :param cmds: command strings suitable for onecmd_plus_hooks :return: True if running of commands should stop """ for line in cmds: @@ -2272,12 +2274,12 @@ class Cmd(cmd.Cmd): readline.parse_and_bind(self.completekey + ": complete") try: - while True: + stop = False + while not stop: if self.cmdqueue: - # Run command out of cmdqueue if nonempty (populated by load command or commands at invocation) - line = self.cmdqueue.pop(0) - if self.echo: - self.poutput('{}{}'.format(self.prompt, line)) + # Run commands out of cmdqueue if nonempty (populated by commands at invocation) + stop = self.runcmds_plus_hooks(self.cmdqueue) + self.cmdqueue.clear() else: # Otherwise, read a command from stdin try: @@ -2289,9 +2291,8 @@ class Cmd(cmd.Cmd): self.poutput('^C') line = '' - # Run the command along with all associated pre and post hooks - if self.onecmd_plus_hooks(line): - break + # Run the command along with all associated pre and post hooks + stop = self.onecmd_plus_hooks(line) finally: if self.use_rawinput and self.completekey and rl_type != RlType.NONE: -- cgit v1.2.1 From c4fd5b6403651ccc89976edd4e04549471b4a23b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 4 Jun 2019 15:55:16 -0400 Subject: Removed _STOP_AND_EXIT cmd2 class member since it was meant to be a boolean constant and was only used internally --- cmd2/cmd2.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d1a5552e..89ea0c0f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -432,9 +432,6 @@ class Cmd(cmd.Cmd): # Built-in commands don't make use of this. It is purely there for user-defined commands and convenience. self._last_result = None - # Codes used for exit conditions - self._STOP_AND_EXIT = True # cmd convention - # Used load command to store the current script dir as a LIFO queue to support _relative_load command self._script_dir = [] @@ -2859,14 +2856,15 @@ class Cmd(cmd.Cmd): @with_argparser(ACArgumentParser(epilog=INTERNAL_COMMAND_EPILOG)) def do_eof(self, _: argparse.Namespace) -> bool: """Called when -D is pressed""" - # End of script should not exit app, but -D should. - return self._STOP_AND_EXIT + # Return True to stop the command loop + return True @with_argparser(ACArgumentParser()) def do_quit(self, _: argparse.Namespace) -> bool: """Exit this application""" self._should_quit = True - return self._STOP_AND_EXIT + # Return True to stop the command loop + return True def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> str: -- cgit v1.2.1 From 5157a6ee2c13d553854f2a78362e957f3d018870 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 6 Jun 2019 16:00:56 -0400 Subject: runcmds_plus_hooks now handles HistoryItems --- cmd2/cmd2.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 50dbcd79..e2ff068e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1807,15 +1807,18 @@ class Cmd(cmd.Cmd): except Exception as ex: self.perror(ex) - def runcmds_plus_hooks(self, cmds: List[str]) -> bool: + def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]]) -> bool: """ Used when commands are being run in an automated fashion like text scripts or history replays. The prompt and command line for each command will be printed if echo is True. - :param cmds: command strings suitable for onecmd_plus_hooks + :param cmds: commands to run :return: True if running of commands should stop """ for line in cmds: + if isinstance(line, HistoryItem): + line = line.raw + if self.echo: self.poutput('{}{}'.format(self.prompt, line)) -- cgit v1.2.1 From 6cdf70823b344b99d6623af19fb618a9c2dbdad4 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 6 Jun 2019 23:27:02 -0400 Subject: Refactored how and when transcript file glob patterns are expanded in order to present a better error message to user --- cmd2/cmd2.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e2ff068e..3b47ee9e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3700,13 +3700,25 @@ class Cmd(cmd.Cmd): relative_path = os.path.join(self._current_script_dir or '', file_path) return self.do_load(relative_path) - def run_transcript_tests(self, callargs: List[str]) -> None: + def _expand_transcripts(self, transcript_paths: List[str]) -> List[str]: + """Expand glob patterns to match transcript files. + + :param transcript_paths: list of transcript file paths (expanded for user), possibly including glob patterns + :return: list of transcript file paths with glob patterns expanded + """ + expanded_transcripts = [] + for fileset in transcript_paths: + for fname in glob.glob(fileset): + expanded_transcripts.append(fname) + return expanded_transcripts + + def run_transcript_tests(self, transcript_paths: List[str]) -> None: """Runs transcript tests for provided file(s). This is called when either -t is provided on the command line or the transcript_files argument is provided during construction of the cmd2.Cmd instance. - :param callargs: list of transcript test file names + :param transcript_paths: list of transcript test file paths """ import unittest from .transcript import Cmd2TestCase @@ -3714,7 +3726,16 @@ class Cmd(cmd.Cmd): class TestMyAppCase(Cmd2TestCase): cmdapp = self - self.__class__.testfiles = callargs + # Expand glob patterns + transcripts_expanded = self._expand_transcripts(transcript_paths) + + # Validate that there is at least one transcript file + if not transcripts_expanded: + self.perror('No test files found - nothing to test', traceback_war=False) + self.exit_code = -1 + return + + self.__class__.testfiles = transcripts_expanded sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main() testcase = TestMyAppCase() stream = utils.StdSim(sys.stderr) @@ -4013,9 +4034,8 @@ class Cmd(cmd.Cmd): # If transcript testing was called for, use other arguments as transcript files if callopts.test: self._transcript_files = callargs - # If commands were supplied at invocation, then add them to the command queue - if callargs: + elif callargs: self.cmdqueue.extend(callargs) # Grab terminal lock before the prompt has been drawn by readline -- cgit v1.2.1 From 16f6bee13c9b568c898a9997e2813b343ca98596 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 6 Jun 2019 23:50:46 -0400 Subject: Extracted duplicated code to utility function --- cmd2/cmd2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 3b47ee9e..520ac33d 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1163,7 +1163,7 @@ class Cmd(cmd.Cmd): # Find every executable file in the user's path that matches the pattern for path in paths: full_path = os.path.join(path, starts_with) - matches = [f for f in glob.glob(full_path + '*') if os.path.isfile(f) and os.access(f, os.X_OK)] + matches = utils.files_from_glob_pattern(full_path + '*', access=os.X_OK) for match in matches: exes_set.add(os.path.basename(match)) @@ -3707,9 +3707,9 @@ class Cmd(cmd.Cmd): :return: list of transcript file paths with glob patterns expanded """ expanded_transcripts = [] - for fileset in transcript_paths: - for fname in glob.glob(fileset): - expanded_transcripts.append(fname) + for pattern in transcript_paths: + files = utils.files_from_glob_pattern(pattern, access=os.R_OK) + expanded_transcripts.extend(files) return expanded_transcripts def run_transcript_tests(self, transcript_paths: List[str]) -> None: -- cgit v1.2.1 From 069b181a887445fde7b66e27e537a9653d70e3dc Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Fri, 7 Jun 2019 00:00:21 -0400 Subject: Moved a new helper function from cmd2.py to utils.py where it probably belonged --- cmd2/cmd2.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 520ac33d..65574095 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3700,18 +3700,6 @@ class Cmd(cmd.Cmd): relative_path = os.path.join(self._current_script_dir or '', file_path) return self.do_load(relative_path) - def _expand_transcripts(self, transcript_paths: List[str]) -> List[str]: - """Expand glob patterns to match transcript files. - - :param transcript_paths: list of transcript file paths (expanded for user), possibly including glob patterns - :return: list of transcript file paths with glob patterns expanded - """ - expanded_transcripts = [] - for pattern in transcript_paths: - files = utils.files_from_glob_pattern(pattern, access=os.R_OK) - expanded_transcripts.extend(files) - return expanded_transcripts - def run_transcript_tests(self, transcript_paths: List[str]) -> None: """Runs transcript tests for provided file(s). @@ -3726,10 +3714,8 @@ class Cmd(cmd.Cmd): class TestMyAppCase(Cmd2TestCase): cmdapp = self - # Expand glob patterns - transcripts_expanded = self._expand_transcripts(transcript_paths) - # Validate that there is at least one transcript file + 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', traceback_war=False) self.exit_code = -1 -- cgit v1.2.1 From ab1ae68d2747b82812b03edbbccfbca39cf1c638 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 7 Jun 2019 16:15:12 -0400 Subject: Removed self._should_quit from cmd2 and add logic to PyscriptBridge to return whether a command returned True for stop. Added stop to CommandResult so pyscripts can now know the return value of a command's do_* function. --- cmd2/cmd2.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 65574095..29f34175 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -425,9 +425,6 @@ class Cmd(cmd.Cmd): shortcuts=shortcuts) self._transcript_files = transcript_files - # Used to enable the ability for a Python script to quit the application - self._should_quit = False - # True if running inside a Python script or interactive console, False otherwise self._in_py = False @@ -2836,7 +2833,6 @@ class Cmd(cmd.Cmd): @with_argparser(ACArgumentParser()) def do_quit(self, _: argparse.Namespace) -> bool: """Exit this application""" - self._should_quit = True # Return True to stop the command loop return True @@ -3053,6 +3049,8 @@ class Cmd(cmd.Cmd): self.perror(err, traceback_war=False) return False + bridge = PyscriptBridge(self) + try: self._in_py = True @@ -3078,7 +3076,6 @@ class Cmd(cmd.Cmd): raise EmbeddedConsoleExit # Set up Python environment - bridge = PyscriptBridge(self) self.pystate[self.pyscript_name] = bridge self.pystate['run'] = py_run self.pystate['quit'] = py_quit @@ -3223,7 +3220,7 @@ class Cmd(cmd.Cmd): finally: self._in_py = False - return self._should_quit + return bridge.stop pyscript_parser = ACArgumentParser() setattr(pyscript_parser.add_argument('script_path', help='path to the script file'), -- cgit v1.2.1 From 77633bc4498d6f11b07ea0a3bd13d4830f809ff8 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 11 Jun 2019 17:39:11 -0400 Subject: Removed support for cmd.cmdqueue allow_cli_args is now an argument to __init__ instead of a cmd2 class member --- cmd2/cmd2.py | 91 ++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 43 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 29f34175..591bc17f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -335,11 +335,12 @@ class Cmd(cmd.Cmd): DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'} DEFAULT_EDITOR = utils.find_editor() - def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent_history_file: str = '', - persistent_history_length: int = 1000, startup_script: Optional[str] = None, use_ipython: bool = False, - transcript_files: Optional[List[str]] = None, allow_redirection: bool = True, - multiline_commands: Optional[List[str]] = None, terminators: Optional[List[str]] = None, - shortcuts: Optional[Dict[str, str]] = None) -> None: + def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *, + persistent_history_file: str = '', persistent_history_length: int = 1000, + startup_script: Optional[str] = None, use_ipython: bool = False, + allow_cli_args: bool = True, transcript_files: Optional[List[str]] = None, + allow_redirection: bool = True, multiline_commands: Optional[List[str]] = None, + terminators: Optional[List[str]] = None, shortcuts: Optional[Dict[str, str]] = None) -> None: """An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package. :param completekey: (optional) readline name of a completion key, default to Tab @@ -349,6 +350,9 @@ class Cmd(cmd.Cmd): :param persistent_history_length: (optional) max number of history items to write to the persistent history file :param startup_script: (optional) file path to a a script to load and execute at startup :param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell + :param allow_cli_args: (optional) if True, then cmd2 will process command line arguments as either + commands to be run or, if -t is specified, transcript files to run. + This should be set to False if your application parses its own arguments. :param transcript_files: (optional) allows running transcript tests when allow_cli_args is False :param allow_redirection: (optional) should output redirection and pipes be allowed :param multiline_commands: (optional) list of commands allowed to accept multi-line input @@ -372,7 +376,6 @@ class Cmd(cmd.Cmd): super().__init__(completekey=completekey, stdin=stdin, stdout=stdout) # Attributes which should NOT be dynamically settable at runtime - self.allow_cli_args = True # Should arguments passed on the command-line be processed as commands? self.default_to_shell = False # Attempt to run unrecognized commands as shell commands self.quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt @@ -423,7 +426,6 @@ class Cmd(cmd.Cmd): terminators=terminators, multiline_commands=multiline_commands, shortcuts=shortcuts) - self._transcript_files = transcript_files # True if running inside a Python script or interactive console, False otherwise self._in_py = False @@ -460,11 +462,33 @@ class Cmd(cmd.Cmd): # If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing self.broken_pipe_warning = '' + # Commands that will run at the beginning of the command loop + self._startup_commands = [] + # If a startup script is provided, then add it in the queue to load if startup_script is not None: startup_script = os.path.abspath(os.path.expanduser(startup_script)) if os.path.exists(startup_script) and os.path.getsize(startup_script) > 0: - self.cmdqueue.append("load '{}'".format(startup_script)) + self._startup_commands.append("load '{}'".format(startup_script)) + + # Transcript files to run instead of interactive command loop + self._transcript_files = None + + # Check for command line args + if allow_cli_args: + parser = argparse.ArgumentParser() + parser.add_argument('-t', '--test', action="store_true", + help='Test against transcript(s) in FILE (wildcards OK)') + callopts, callargs = parser.parse_known_args() + + # If transcript testing was called for, use other arguments as transcript files + if callopts.test: + self._transcript_files = callargs + # If commands were supplied at invocation, then add them to the command queue + elif callargs: + self._startup_commands.extend(callargs) + elif transcript_files: + self._transcript_files = transcript_files # The default key for sorting tab completion matches. This only applies when the matches are not # already marked as sorted by setting self.matches_sorted to True. Its default value performs a @@ -2242,25 +2266,23 @@ class Cmd(cmd.Cmd): readline.parse_and_bind(self.completekey + ": complete") try: - stop = False + # Run startup commands + stop = self.runcmds_plus_hooks(self._startup_commands) + self._startup_commands.clear() + while not stop: - if self.cmdqueue: - # Run commands out of cmdqueue if nonempty (populated by commands at invocation) - stop = self.runcmds_plus_hooks(self.cmdqueue) - self.cmdqueue.clear() - else: - # Otherwise, read a command from stdin - try: - line = self.pseudo_raw_input(self.prompt) - except KeyboardInterrupt as ex: - if self.quit_on_sigint: - raise ex - else: - self.poutput('^C') - line = '' + # Get commands from user + try: + line = self.pseudo_raw_input(self.prompt) + except KeyboardInterrupt as ex: + if self.quit_on_sigint: + raise ex + else: + self.poutput('^C') + line = '' - # Run the command along with all associated pre and post hooks - stop = self.onecmd_plus_hooks(line) + # Run the command along with all associated pre and post hooks + stop = self.onecmd_plus_hooks(line) finally: if self.use_rawinput and self.completekey and rl_type != RlType.NONE: @@ -2274,7 +2296,6 @@ class Cmd(cmd.Cmd): elif rl_type == RlType.PYREADLINE: # noinspection PyUnresolvedReferences readline.rl.mode._display_completions = orig_pyreadline_display - self.cmdqueue.clear() # ----- Alias sub-command functions ----- @@ -2889,10 +2910,8 @@ class Cmd(cmd.Cmd): """ read_only_settings = """ Commands may be terminated with: {} - Arguments at invocation allowed: {} Output redirection and pipes allowed: {}""" - return read_only_settings.format(str(self.statement_parser.terminators), self.allow_cli_args, - self.allow_redirection) + return read_only_settings.format(str(self.statement_parser.terminators), self.allow_redirection) def show(self, args: argparse.Namespace, parameter: str = '') -> None: """Shows current settings of parameters. @@ -3991,7 +4010,6 @@ class Cmd(cmd.Cmd): _cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with the following extra features provided by cmd2: - - commands at invocation - transcript testing - intro banner - exit code @@ -4008,19 +4026,6 @@ class Cmd(cmd.Cmd): original_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - if self.allow_cli_args: - parser = argparse.ArgumentParser() - parser.add_argument('-t', '--test', action="store_true", - help='Test against transcript(s) in FILE (wildcards OK)') - callopts, callargs = parser.parse_known_args() - - # If transcript testing was called for, use other arguments as transcript files - if callopts.test: - self._transcript_files = callargs - # If commands were supplied at invocation, then add them to the command queue - elif callargs: - self.cmdqueue.extend(callargs) - # Grab terminal lock before the prompt has been drawn by readline self.terminal_lock.acquire() -- cgit v1.2.1 From f42cdb2a52a096e5c2627db5eeeae7c2e059fb4a Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 11 Jun 2019 21:47:33 -0400 Subject: Move argparse parsing of CLI args back to cmdloop() from __init__() This is so unit tests pass --- cmd2/cmd2.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 591bc17f..9b397c2b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -472,23 +472,10 @@ class Cmd(cmd.Cmd): self._startup_commands.append("load '{}'".format(startup_script)) # Transcript files to run instead of interactive command loop - self._transcript_files = None + self._transcript_files = transcript_files - # Check for command line args - if allow_cli_args: - parser = argparse.ArgumentParser() - parser.add_argument('-t', '--test', action="store_true", - help='Test against transcript(s) in FILE (wildcards OK)') - callopts, callargs = parser.parse_known_args() - - # If transcript testing was called for, use other arguments as transcript files - if callopts.test: - self._transcript_files = callargs - # If commands were supplied at invocation, then add them to the command queue - elif callargs: - self._startup_commands.extend(callargs) - elif transcript_files: - self._transcript_files = transcript_files + # Should commands at invocation and -t/--test transcript test running be allowed + self._allow_cli_args = allow_cli_args # The default key for sorting tab completion matches. This only applies when the matches are not # already marked as sorted by setting self.matches_sorted to True. Its default value performs a @@ -4026,6 +4013,20 @@ class Cmd(cmd.Cmd): original_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) + # Check for command line args + if self._allow_cli_args: + parser = argparse.ArgumentParser() + parser.add_argument('-t', '--test', action="store_true", + help='Test against transcript(s) in FILE (wildcards OK)') + callopts, callargs = parser.parse_known_args() + + # If transcript testing was called for, use other arguments as transcript files + if callopts.test: + self._transcript_files = callargs + # If commands were supplied at invocation, then add them to the command queue + elif callargs: + self._startup_commands.extend(callargs) + # Grab terminal lock before the prompt has been drawn by readline self.terminal_lock.acquire() -- cgit v1.2.1 From c0f92d1c3d3a00aa82f0ded091fde1e3a16022e8 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 12 Jun 2019 10:44:37 -0400 Subject: Revert "Move argparse parsing of CLI args back to cmdloop() from __init__()" This reverts commit f42cdb2a52a096e5c2627db5eeeae7c2e059fb4a. --- cmd2/cmd2.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 9b397c2b..591bc17f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -472,10 +472,23 @@ class Cmd(cmd.Cmd): self._startup_commands.append("load '{}'".format(startup_script)) # Transcript files to run instead of interactive command loop - self._transcript_files = transcript_files + self._transcript_files = None - # Should commands at invocation and -t/--test transcript test running be allowed - self._allow_cli_args = allow_cli_args + # Check for command line args + if allow_cli_args: + parser = argparse.ArgumentParser() + parser.add_argument('-t', '--test', action="store_true", + help='Test against transcript(s) in FILE (wildcards OK)') + callopts, callargs = parser.parse_known_args() + + # If transcript testing was called for, use other arguments as transcript files + if callopts.test: + self._transcript_files = callargs + # If commands were supplied at invocation, then add them to the command queue + elif callargs: + self._startup_commands.extend(callargs) + elif transcript_files: + self._transcript_files = transcript_files # The default key for sorting tab completion matches. This only applies when the matches are not # already marked as sorted by setting self.matches_sorted to True. Its default value performs a @@ -4013,20 +4026,6 @@ class Cmd(cmd.Cmd): original_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - # Check for command line args - if self._allow_cli_args: - parser = argparse.ArgumentParser() - parser.add_argument('-t', '--test', action="store_true", - help='Test against transcript(s) in FILE (wildcards OK)') - callopts, callargs = parser.parse_known_args() - - # If transcript testing was called for, use other arguments as transcript files - if callopts.test: - self._transcript_files = callargs - # If commands were supplied at invocation, then add them to the command queue - elif callargs: - self._startup_commands.extend(callargs) - # Grab terminal lock before the prompt has been drawn by readline self.terminal_lock.acquire() -- cgit v1.2.1 From 6b8b63686dc1ab871952c75785898c31882ef8a4 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 12 Jun 2019 14:40:05 -0400 Subject: Made constants for color values --- cmd2/cmd2.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 591bc17f..121d39a5 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -42,7 +42,6 @@ from collections import namedtuple from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO import colorama -from colorama import Fore from . import constants from . import plugin @@ -59,7 +58,7 @@ if rl_type == RlType.NONE: # pragma: no cover rl_warning = "Readline features including tab completion have been disabled since no \n" \ "supported version of readline was found. To resolve this, install \n" \ "pyreadline on Windows or gnureadline on Mac.\n\n" - sys.stderr.write(Fore.LIGHTYELLOW_EX + rl_warning + Fore.RESET) + sys.stderr.write(constants.WARNING_COLOR + rl_warning + constants.RESET_COLOR) else: from .rl_utils import rl_force_redisplay, readline @@ -616,7 +615,7 @@ class Cmd(cmd.Cmd): if not msg_str.endswith(end): msg_str += end if color: - msg_str = color + msg_str + Fore.RESET + msg_str = color + msg_str + constants.RESET_COLOR self.decolorized_write(self.stdout, msg_str) except BrokenPipeError: # This occurs if a command's output is being piped to another @@ -627,8 +626,8 @@ class Cmd(cmd.Cmd): if self.broken_pipe_warning: sys.stderr.write(self.broken_pipe_warning) - def perror(self, err: Union[str, Exception], traceback_war: bool = True, err_color: str = Fore.LIGHTRED_EX, - war_color: str = Fore.LIGHTYELLOW_EX) -> None: + def perror(self, err: Union[str, Exception], traceback_war: bool = True, err_color: str = constants.ERROR_COLOR, + war_color: str = constants.WARNING_COLOR) -> None: """ Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists. :param err: an Exception or error message to print out @@ -644,12 +643,12 @@ class Cmd(cmd.Cmd): err_msg = "EXCEPTION of type '{}' occurred with message: '{}'\n".format(type(err).__name__, err) else: err_msg = "{}\n".format(err) - err_msg = err_color + err_msg + Fore.RESET + err_msg = err_color + err_msg + constants.RESET_COLOR self.decolorized_write(sys.stderr, err_msg) if traceback_war and not self.debug: war = "To enable full traceback, run the following command: 'set debug true'\n" - war = war_color + war + Fore.RESET + war = war_color + war + constants.RESET_COLOR self.decolorized_write(sys.stderr, war) def pfeedback(self, msg: str) -> None: @@ -3745,7 +3744,7 @@ class Cmd(cmd.Cmd): test_results = runner.run(testcase) if test_results.wasSuccessful(): self.decolorized_write(sys.stderr, stream.read()) - self.poutput('Tests passed', color=Fore.LIGHTGREEN_EX) + self.poutput('Tests passed', color=constants.SUCCESS_COLOR) else: # Strip off the initial traceback which isn't particularly useful for end users error_str = stream.read() -- cgit v1.2.1 From 57c64c4045e9d3d6e06787b8ea7bafdc14a2fa13 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 12 Jun 2019 14:50:59 -0400 Subject: Fixed UnsupportedOperation on fileno error when a shell command was one of the commands run while generating a transcript --- cmd2/cmd2.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 121d39a5..8106891a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3512,7 +3512,6 @@ class Cmd(cmd.Cmd): Generate a transcript file from a given history of commands :return: True if running of commands should stop """ - import io # Validate the transcript file path to make sure directory exists and write access is available transcript_path = os.path.abspath(os.path.expanduser(transcript_file)) transcript_dir = os.path.dirname(transcript_path) @@ -3554,21 +3553,16 @@ class Cmd(cmd.Cmd): else: command += '{}{}\n'.format(self.continuation_prompt, line) transcript += command - # create a new string buffer and set it to stdout to catch the output - # of the command - membuf = io.StringIO() - self.stdout = membuf + + # Use a StdSim object to capture output + self.stdout = utils.StdSim(self.stdout) # then run the command and let the output go into our buffer stop = self.onecmd_plus_hooks(history_item) commands_run += 1 - # rewind the buffer to the beginning - membuf.seek(0) - # get the output out of the buffer - output = membuf.read() - # and add the regex-escaped output to the transcript - transcript += output.replace('/', r'\/') + # add the regex-escaped output to the transcript + transcript += self.stdout.getvalue().replace('/', r'\/') # check if we are supposed to stop if stop: -- cgit v1.2.1 From 6b38d416a38fdf0d7fec8425d2761800cb891f06 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 12 Jun 2019 15:19:48 -0400 Subject: Transcript generation no longer terminates _cmdloop() when a command returns True for stop --- cmd2/cmd2.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8106891a..d2610cc3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3428,7 +3428,7 @@ class Cmd(cmd.Cmd): except Exception as e: self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False) elif args.transcript: - return self._generate_transcript(history, args.transcript) + self._generate_transcript(history, args.transcript) else: # Display the history items retrieved for hi in history: @@ -3507,10 +3507,9 @@ class Cmd(cmd.Cmd): msg = "can not write persistent history file '{}': {}" self.perror(msg.format(self.persistent_history_file, ex), traceback_war=False) - def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> Optional[bool]: + def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None: """ Generate a transcript file from a given history of commands - :return: True if running of commands should stop """ # Validate the transcript file path to make sure directory exists and write access is available transcript_path = os.path.abspath(os.path.expanduser(transcript_file)) @@ -3521,7 +3520,6 @@ class Cmd(cmd.Cmd): return commands_run = 0 - stop = False try: with self.sigint_protection: # Disable echo while we manually redirect stdout to a StringIO buffer @@ -3573,6 +3571,11 @@ class Cmd(cmd.Cmd): self.echo = saved_echo self.stdout = saved_stdout + # Check if all commands ran + if commands_run < len(history): + warning = "Command {} triggered a stop and ended transcript generation early".format(commands_run) + self.perror(warning, err_color=constants.WARNING_COLOR, traceback_war=False) + # finally, we can write the transcript out to the file try: with open(transcript_file, 'w') as fout: @@ -3588,8 +3591,6 @@ class Cmd(cmd.Cmd): msg = '{} {} saved to transcript file {!r}' self.pfeedback(msg.format(commands_run, plural, transcript_file)) - return stop - edit_description = ("Edit a file in a text editor\n" "\n" "The editor used is determined by a settable parameter. To set it:\n" @@ -3677,7 +3678,7 @@ class Cmd(cmd.Cmd): self._script_dir.append(os.path.dirname(expanded_path)) if args.transcript: - return self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) + self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) else: return self.runcmds_plus_hooks(script_commands) -- cgit v1.2.1 From f659da34769b1721de7888cc2f1b5473eb88df94 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 13 Jun 2019 14:06:57 -0400 Subject: Reverted making constant values for colors. This will be done on a future ticket. --- cmd2/cmd2.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d2610cc3..02462d96 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -42,6 +42,7 @@ from collections import namedtuple from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO import colorama +from colorama import Fore from . import constants from . import plugin @@ -58,7 +59,7 @@ if rl_type == RlType.NONE: # pragma: no cover rl_warning = "Readline features including tab completion have been disabled since no \n" \ "supported version of readline was found. To resolve this, install \n" \ "pyreadline on Windows or gnureadline on Mac.\n\n" - sys.stderr.write(constants.WARNING_COLOR + rl_warning + constants.RESET_COLOR) + sys.stderr.write(Fore.LIGHTYELLOW_EX + rl_warning + Fore.RESET) else: from .rl_utils import rl_force_redisplay, readline @@ -615,7 +616,7 @@ class Cmd(cmd.Cmd): if not msg_str.endswith(end): msg_str += end if color: - msg_str = color + msg_str + constants.RESET_COLOR + msg_str = color + msg_str + Fore.RESET self.decolorized_write(self.stdout, msg_str) except BrokenPipeError: # This occurs if a command's output is being piped to another @@ -626,8 +627,8 @@ class Cmd(cmd.Cmd): if self.broken_pipe_warning: sys.stderr.write(self.broken_pipe_warning) - def perror(self, err: Union[str, Exception], traceback_war: bool = True, err_color: str = constants.ERROR_COLOR, - war_color: str = constants.WARNING_COLOR) -> None: + def perror(self, err: Union[str, Exception], traceback_war: bool = True, err_color: str = Fore.LIGHTRED_EX, + war_color: str = Fore.LIGHTYELLOW_EX) -> None: """ Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists. :param err: an Exception or error message to print out @@ -643,12 +644,12 @@ class Cmd(cmd.Cmd): err_msg = "EXCEPTION of type '{}' occurred with message: '{}'\n".format(type(err).__name__, err) else: err_msg = "{}\n".format(err) - err_msg = err_color + err_msg + constants.RESET_COLOR + err_msg = err_color + err_msg + Fore.RESET self.decolorized_write(sys.stderr, err_msg) if traceback_war and not self.debug: war = "To enable full traceback, run the following command: 'set debug true'\n" - war = war_color + war + constants.RESET_COLOR + war = war_color + war + Fore.RESET self.decolorized_write(sys.stderr, war) def pfeedback(self, msg: str) -> None: @@ -3574,7 +3575,7 @@ class Cmd(cmd.Cmd): # Check if all commands ran if commands_run < len(history): warning = "Command {} triggered a stop and ended transcript generation early".format(commands_run) - self.perror(warning, err_color=constants.WARNING_COLOR, traceback_war=False) + self.perror(warning, err_color=Fore.LIGHTYELLOW_EX, traceback_war=False) # finally, we can write the transcript out to the file try: @@ -3739,7 +3740,7 @@ class Cmd(cmd.Cmd): test_results = runner.run(testcase) if test_results.wasSuccessful(): self.decolorized_write(sys.stderr, stream.read()) - self.poutput('Tests passed', color=constants.SUCCESS_COLOR) + self.poutput('Tests passed', color=Fore.LIGHTGREEN_EX) else: # Strip off the initial traceback which isn't particularly useful for end users error_str = stream.read() -- cgit v1.2.1