summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2019-05-27 15:45:34 -0400
committerTodd Leonhardt <todd.leonhardt@gmail.com>2019-05-27 15:45:34 -0400
commit9dd00461b22085d3f16b4cc178a222c30fe95d11 (patch)
treee1644047665caf1d15860be388510fe7b7b61d1a
parent916060bde828d8911ec2bbb4db54396212514481 (diff)
downloadcmd2-git-9dd00461b22085d3f16b4cc178a222c30fe95d11.tar.gz
Add the -a/--all flag to the history command for showing all commands including those persisted from previous sessions
Also: - History class has been modified to keep track of the session start index - History class span(), str_search(), and regex_search() methods now take an optional 2nd boolean parameter `include_persisted` which determines whether or not commands persisted from previous sessions should be included by default - If a start index is manually specified, then it automatically includes the full search - Updates unit tests
-rw-r--r--cmd2/cmd2.py15
-rw-r--r--cmd2/history.py46
-rw-r--r--docs/freefeatures.rst2
-rw-r--r--tests/conftest.py3
-rw-r--r--tests/test_history.py78
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):