diff options
-rw-r--r-- | cmd2/cmd2.py | 15 | ||||
-rw-r--r-- | cmd2/history.py | 46 | ||||
-rw-r--r-- | docs/freefeatures.rst | 2 | ||||
-rw-r--r-- | tests/conftest.py | 3 | ||||
-rw-r--r-- | tests/test_history.py | 78 |
5 files changed, 122 insertions, 22 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e4fe6efa..b13eb45b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -345,8 +345,8 @@ class Cmd(cmd.Cmd): :param completekey: (optional) readline name of a completion key, default to Tab :param stdin: (optional) alternate input file object, if not specified, sys.stdin is used :param stdout: (optional) alternate output file object, if not specified, sys.stdout is used - :param persistent_history_file: (optional) file path to load a persistent readline history from - :param persistent_history_length: (optional) max number of lines which will be written to the history file + :param persistent_history_file: (optional) file path to load a persistent cmd2 command history from + :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 transcript_files: (optional) allows running transcript tests when allow_cli_args is False @@ -3337,6 +3337,8 @@ class Cmd(cmd.Cmd): history_format_group.add_argument('-v', '--verbose', action='store_true', help='display history and include expanded commands if they\n' 'differ from the typed command') + history_format_group.add_argument('-a', '--all', action='store_true', + help='display all commands, including ones persisted from previous sessions') history_arg_help = ("empty all history items\n" "a one history item by number\n" @@ -3389,18 +3391,18 @@ class Cmd(cmd.Cmd): if '..' in arg or ':' in arg: # Get a slice of history - history = self.history.span(arg) + history = self.history.span(arg, args.all) elif arg_is_int: history = [self.history.get(arg)] elif arg.startswith(r'/') and arg.endswith(r'/'): - history = self.history.regex_search(arg) + history = self.history.regex_search(arg, args.all) else: - history = self.history.str_search(arg) + history = self.history.str_search(arg, args.all) else: # If no arg given, then retrieve the entire history cowardly_refuse_to_run = True # Get a copy of the history so it doesn't get mutated while we are using it - history = self.history[:] + history = self.history.span(':', args.all) if args.run: if cowardly_refuse_to_run: @@ -3488,6 +3490,7 @@ class Cmd(cmd.Cmd): return self.history = history + self.history.start_session() self.persistent_history_file = hist_file # populate readline history diff --git a/cmd2/history.py b/cmd2/history.py index ae2e85ad..a36f489e 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -73,8 +73,14 @@ class History(list): regex_search() - return a list of history items which match a given regex get() - return a single element of the list, using 1 based indexing span() - given a 1-based slice, return the appropriate list of history items - """ + def __init__(self, seq=()) -> None: + super().__init__(seq) + self.session_start_index = 0 + + def start_session(self) -> None: + """Start a new session, thereby setting the next index as the first index in the new session.""" + self.session_start_index = len(self) # noinspection PyMethodMayBeStatic def _zero_based_index(self, onebased: Union[int, str]) -> int: @@ -85,12 +91,17 @@ class History(list): return result def append(self, new: Statement) -> None: - """Append a HistoryItem to end of the History list + """Append a HistoryItem to end of the History list. :param new: command line to convert to HistoryItem and add to the end of the History list """ history_item = HistoryItem(new, len(self) + 1) - list.append(self, history_item) + super().append(history_item) + + def clear(self) -> None: + """Remove all items from the History list.""" + super().clear() + self.start_session() def get(self, index: Union[int, str]) -> HistoryItem: """Get item from the History list using 1-based indexing. @@ -133,10 +144,11 @@ class History(list): # spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]\d*)?\s*$') - def span(self, span: str) -> List[HistoryItem]: + def span(self, span: str, include_persisted: bool = False) -> List[HistoryItem]: """Return an index or slice of the History list, :param span: string containing an index or a slice + :param include_persisted: (optional) if True, then retrieve full results including from persisted history :return: a list of HistoryItems This method can accommodate input in any of these forms: @@ -191,19 +203,26 @@ class History(list): # take a slice of the array result = self[start:] elif end is not None and sep is not None: - result = self[:end] + if include_persisted: + result = self[:end] + else: + result = self[self.session_start_index:end] elif start is not None: - # there was no separator so it's either a posative or negative integer + # there was no separator so it's either a positive or negative integer result = [self[start]] else: # we just have a separator, return the whole list - result = self[:] + if include_persisted: + result = self[:] + else: + result = self[self.session_start_index:] return result - def str_search(self, search: str) -> List[HistoryItem]: + def str_search(self, search: str, include_persisted: bool = False) -> List[HistoryItem]: """Find history items which contain a given string :param search: the string to search for + :param include_persisted: (optional) if True, then search full history including from persisted history :return: a list of history items, or an empty list if the string was not found """ def isin(history_item): @@ -212,12 +231,15 @@ class History(list): inraw = sloppy in utils.norm_fold(history_item.raw) inexpanded = sloppy in utils.norm_fold(history_item.expanded) return inraw or inexpanded - return [item for item in self if isin(item)] - def regex_search(self, regex: str) -> List[HistoryItem]: + search_list = self if include_persisted else self[self.session_start_index:] + return [item for item in search_list if isin(item)] + + def regex_search(self, regex: str, include_persisted: bool = False) -> List[HistoryItem]: """Find history items which match a given regular expression :param regex: the regular expression to search for. + :param include_persisted: (optional) if True, then search full history including from persisted history :return: a list of history items, or an empty list if the string was not found """ regex = regex.strip() @@ -228,7 +250,9 @@ class History(list): def isin(hi): """filter function for doing a regular expression search of history""" return finder.search(hi.raw) or finder.search(hi.expanded) - return [itm for itm in self if isin(itm)] + + search_list = self if include_persisted else self[self.session_start_index:] + return [itm for itm in search_list if isin(itm)] def truncate(self, max_length: int) -> None: """Truncate the length of the history, dropping the oldest items if necessary diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst index 11b5de68..05b5391d 100644 --- a/docs/freefeatures.rst +++ b/docs/freefeatures.rst @@ -258,7 +258,7 @@ All cmd_-based applications on systems with the ``readline`` module also provide `Readline Emacs editing mode`_. With this you can, for example, use **Ctrl-r** to search backward through the readline history. -``cmd2`` adds the option of making this readline history persistent via optional arguments to ``cmd2.Cmd.__init__()``: +``cmd2`` adds the option of making this history persistent via optional arguments to ``cmd2.Cmd.__init__()``: .. automethod:: cmd2.cmd2.Cmd.__init__ diff --git a/tests/conftest.py b/tests/conftest.py index 9d55eb4d..769e5a8f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,6 +59,7 @@ shortcuts List available shortcuts # Help text for the history command HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT | -c] [-s] [-x] [-v] + [-a] [arg] View, run, edit, save, or clear previously entered commands @@ -88,7 +89,7 @@ formatting: macros expanded, instead of typed commands -v, --verbose display history and include expanded commands if they differ from the typed command - + -a, --all display all commands, including ones persisted from previous sessions """ # Output from the shortcuts command with default built-in shortcuts diff --git a/tests/test_history.py b/tests/test_history.py index 1956a574..5e01688c 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -43,6 +43,19 @@ def hist(): HistoryItem(Statement('', raw='fourth'),4)]) return h +@pytest.fixture +def persisted_hist(): + from cmd2.parsing import Statement + from cmd2.cmd2 import History, HistoryItem + h = History([HistoryItem(Statement('', raw='first'), 1), + HistoryItem(Statement('', raw='second'), 2), + HistoryItem(Statement('', raw='third'), 3), + HistoryItem(Statement('', raw='fourth'),4)]) + h.start_session() + h.append(Statement('', raw='fifth')) + h.append(Statement('', raw='sixth')) + return h + def test_history_class_span(hist): for tryit in ['*', ':', '-', 'all', 'ALL']: assert hist.span(tryit) == hist @@ -119,6 +132,62 @@ def test_history_class_span(hist): with pytest.raises(ValueError): hist.span(tryit) +def test_persisted_history_span(persisted_hist): + for tryit in ['*', ':', '-', 'all', 'ALL']: + assert persisted_hist.span(tryit, include_persisted=True) == persisted_hist + assert persisted_hist.span(tryit, include_persisted=False) != persisted_hist + + assert persisted_hist.span('3')[0].statement.raw == 'third' + assert persisted_hist.span('-1')[0].statement.raw == 'sixth' + + span = persisted_hist.span('2..') + assert len(span) == 5 + assert span[0].statement.raw == 'second' + assert span[1].statement.raw == 'third' + assert span[2].statement.raw == 'fourth' + assert span[3].statement.raw == 'fifth' + assert span[4].statement.raw == 'sixth' + + span = persisted_hist.span('-2..') + assert len(span) == 2 + assert span[0].statement.raw == 'fifth' + assert span[1].statement.raw == 'sixth' + + span = persisted_hist.span('1..3') + assert len(span) == 3 + assert span[0].statement.raw == 'first' + assert span[1].statement.raw == 'second' + assert span[2].statement.raw == 'third' + + span = persisted_hist.span('2:-1') + assert len(span) == 5 + assert span[0].statement.raw == 'second' + assert span[1].statement.raw == 'third' + assert span[2].statement.raw == 'fourth' + assert span[3].statement.raw == 'fifth' + assert span[4].statement.raw == 'sixth' + + span = persisted_hist.span('-3:4') + assert len(span) == 1 + assert span[0].statement.raw == 'fourth' + + span = persisted_hist.span(':-2', include_persisted=True) + assert len(span) == 5 + assert span[0].statement.raw == 'first' + assert span[1].statement.raw == 'second' + assert span[2].statement.raw == 'third' + assert span[3].statement.raw == 'fourth' + assert span[4].statement.raw == 'fifth' + + span = persisted_hist.span(':-2', include_persisted=False) + assert len(span) == 1 + assert span[0].statement.raw == 'fifth' + + value_errors = ['fred', 'fred:joe', 'a..b', '2 ..', '1 : 3', '1:0', '0:3'] + for tryit in value_errors: + with pytest.raises(ValueError): + persisted_hist.span(tryit) + def test_history_class_get(hist): assert hist.get('1').statement.raw == 'first' assert hist.get(3).statement.raw == 'third' @@ -401,7 +470,8 @@ def test_history_verbose_with_other_options(base_app): options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x'] for opt in options_to_test: out, err = run_cmd(base_app, 'history -v ' + opt) - assert len(out) == 3 + assert len(out) == 4 + assert out[0] == '-v can not be used with any other options' assert out[1].startswith('Usage:') def test_history_verbose(base_app): @@ -417,7 +487,8 @@ def test_history_script_with_invalid_options(base_app): options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] for opt in options_to_test: out, err = run_cmd(base_app, 'history -s ' + opt) - assert len(out) == 3 + assert len(out) == 4 + assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t' assert out[1].startswith('Usage:') def test_history_script(base_app): @@ -432,7 +503,8 @@ def test_history_expanded_with_invalid_options(base_app): options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] for opt in options_to_test: out, err = run_cmd(base_app, 'history -x ' + opt) - assert len(out) == 3 + assert len(out) == 4 + assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t' assert out[1].startswith('Usage:') def test_history_expanded(base_app): |