From 969dab29bbaed194edce881d60643f5e95a02366 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 11 Jul 2018 02:40:26 -0400 Subject: Added --clear argument to history command Added better error checking when loading readline history file Improved some error messages Changed IOError usages to OSError since they were merged in Python 3.3. --- cmd2/cmd2.py | 81 +++++++++++++++++++++++++++++++++++++++++++---------------- cmd2/utils.py | 4 +-- 2 files changed, 61 insertions(+), 24 deletions(-) (limited to 'cmd2') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 42e00c39..bdb8452a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -369,18 +369,6 @@ class Cmd(cmd.Cmd): except AttributeError: pass - # If persistent readline history is enabled, then read history from file and register to write to file at exit - if persistent_history_file and rl_type != RlType.NONE: - persistent_history_file = os.path.expanduser(persistent_history_file) - try: - readline.read_history_file(persistent_history_file) - # default history len is -1 (infinite), which may grow unruly - readline.set_history_length(persistent_history_length) - except FileNotFoundError: - pass - import atexit - atexit.register(readline.write_history_file, persistent_history_file) - # Call super class constructor super().__init__(completekey=completekey, stdin=stdin, stdout=stdout) @@ -448,6 +436,37 @@ 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 = '' + # Check if history should persist + if persistent_history_file and rl_type != RlType.NONE: + persistent_history_file = os.path.expanduser(persistent_history_file) + read_err = False + + try: + # First try to read any existing history file + readline.read_history_file(persistent_history_file) + except FileNotFoundError: + pass + except OSError as ex: + self.perror("readline cannot read persistent history file '{}': {}".format(persistent_history_file, ex), + traceback_war=False) + read_err = True + + if not read_err: + try: + # Make sure readline is able to write the history file. Doing it this way is a more thorough check + # than trying to open the file with write access since readline's underlying function needs to + # create a temporary file in the same directory and may not have permission. + readline.set_history_length(persistent_history_length) + readline.write_history_file(persistent_history_file) + except OSError as ex: + self.perror("readline cannot write persistent history file '{}': {}". + format(persistent_history_file, ex), traceback_war=False) + else: + # Set history file and register to save our history at exit + import atexit + self.persistent_history_file = persistent_history_file + atexit.register(readline.write_history_file, self.persistent_history_file) + # If a startup script is provided, then add it in the queue to load if startup_script is not None: startup_script = os.path.expanduser(startup_script) @@ -610,7 +629,7 @@ class Cmd(cmd.Cmd): try: self.pipe_proc.stdin.write(msg_str.encode('utf-8', 'replace')) self.pipe_proc.stdin.close() - except (IOError, KeyboardInterrupt): + except (OSError, KeyboardInterrupt): pass # Less doesn't respect ^C, but catches it for its own UI purposes (aborting search etc. inside less) @@ -2569,8 +2588,9 @@ Usage: Usage: unalias [-a] name [name ...] try: with open(filename) as f: interp.runcode(f.read()) - except IOError as e: - self.perror(e) + except OSError as ex: + error_msg = "Error opening script file '{}': {}".format(filename, ex) + self.perror(error_msg, traceback_war=False) bridge = PyscriptBridge(self) self.pystate['run'] = run @@ -2764,6 +2784,7 @@ Paths or arguments that contain spaces must be enclosed in quotes history_parser_group.add_argument('-s', '--script', action='store_true', help='script format; no separation lines') history_parser_group.add_argument('-o', '--output-file', metavar='FILE', help='output commands to a script file') history_parser_group.add_argument('-t', '--transcript', help='output commands and results to a transcript file') + history_parser_group.add_argument('-c', '--clear', action="store_true", help='clears all history') _history_arg_help = """empty all history items a one history item by number a..b, a:b, a:, ..b items by indices (inclusive) @@ -2773,7 +2794,18 @@ a..b, a:b, a:, ..b items by indices (inclusive) @with_argparser(history_parser) def do_history(self, args: argparse.Namespace) -> None: - """View, run, edit, and save previously entered commands.""" + """View, run, edit, save, or clear previously entered commands.""" + + if args.clear: + # Clear command and readline history + self.history.clear() + + if rl_type != RlType.NONE: + readline.clear_history() + if self.persistent_history_file: + os.remove(self.persistent_history_file) + return + # If an argument was supplied, then retrieve partial contents of the history cowardly_refuse_to_run = False if args.arg: @@ -2979,25 +3011,30 @@ Script should contain one command per line, just like command would be typed in """ # If arg is None or arg is an empty string this is an error if not arglist: - self.perror('load command requires a file path:', traceback_war=False) + self.perror('load command requires a file path', traceback_war=False) return file_path = arglist[0].strip() expanded_path = os.path.abspath(os.path.expanduser(file_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 + # Make sure expanded_path points to a file if not os.path.isfile(expanded_path): - self.perror('{} does not exist or is not a file'.format(expanded_path), traceback_war=False) + self.perror("'{}' is not a file".format(expanded_path), traceback_war=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) + self.perror("'{}' is empty".format(expanded_path), traceback_war=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) + self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path), traceback_war=False) return try: @@ -3006,8 +3043,8 @@ Script should contain one command per line, just like command would be typed in # self._script_dir list when done. with open(expanded_path, encoding='utf-8') as target: self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue - except IOError as e: # pragma: no cover - self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e)) + except OSError as ex: # pragma: no cover + self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex)) return self._script_dir.append(os.path.dirname(expanded_path)) diff --git a/cmd2/utils.py b/cmd2/utils.py index 11d48b78..ff8e034e 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -143,7 +143,7 @@ def is_text_file(file_path: str) -> bool: # noinspection PyUnusedLocal if sum(1 for line in f) > 0: valid_text_file = True - except IOError: # pragma: no cover + except OSError: # pragma: no cover pass except UnicodeDecodeError: # The file is not ASCII. Check if it is UTF-8. @@ -153,7 +153,7 @@ def is_text_file(file_path: str) -> bool: # noinspection PyUnusedLocal if sum(1 for line in f) > 0: valid_text_file = True - except IOError: # pragma: no cover + except OSError: # pragma: no cover pass except UnicodeDecodeError: # Not UTF-8 -- cgit v1.2.1 From 6bb7e4c110f8779e550502c251648b7e7af93154 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Wed, 11 Jul 2018 19:46:48 -0400 Subject: Deleted the CmdResult helper class The CmdResult helper class was deprecated in the previous release. This commit deletes it. - The new CommandResult class is an improved replacement Also: - Deleted the namedtuple_with_two_defaults helper function which is now no longer used --- cmd2/cmd2.py | 22 ---------------------- cmd2/utils.py | 16 ---------------- 2 files changed, 38 deletions(-) (limited to 'cmd2') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 44f3a068..2298f4b9 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3220,25 +3220,3 @@ class Statekeeper(object): if self.obj: for attrib in self.attribs: setattr(self.obj, attrib, getattr(self, attrib)) - - -class CmdResult(utils.namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war'])): - """DEPRECATED: Derive a class to store results from a named tuple so we can tweak dunder methods for convenience. - - This is provided as a convenience and an example for one possible way for end users to store results in - the self._last_result attribute of cmd2.Cmd class instances. See the "python_scripting.py" example for how it can - be used to enable conditional control flow. - - Named tuple attributes - ---------------------- - out - this is intended to store normal output data from the command and can be of any type that makes sense - err: str - (optional) this is intended to store an error message and it being non-empty indicates there was an error - Defaults to an empty string - war: str - (optional) this is intended to store a warning message which isn't quite an error, but of note - Defaults to an empty string. - - NOTE: Named tuples are immutable. So the contents are there for access, not for modification. - """ - def __bool__(self) -> bool: - """If err is an empty string, treat the result as a success; otherwise treat it as a failure.""" - return not self.err diff --git a/cmd2/utils.py b/cmd2/utils.py index 11d48b78..1f08b416 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -61,22 +61,6 @@ def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]], return T -def namedtuple_with_two_defaults(typename: str, field_names: Union[str, List[str]], - default_values: collections.Iterable=('', '')): - """Wrapper around namedtuple which lets you treat the last value as optional. - - :param typename: str - type name for the Named tuple - :param field_names: List[str] or space-separated string of field names - :param default_values: (optional) 2-element tuple containing the default values for last 2 parameters in named tuple - Defaults to an empty string for both of them - :return: namedtuple type - """ - T = collections.namedtuple(typename, field_names) - # noinspection PyUnresolvedReferences - T.__new__.__defaults__ = default_values - return T - - def cast(current: Any, new: str) -> Any: """Tries to force a new value into the same type as the current when trying to set the value for a parameter. -- cgit v1.2.1