Skip to content

Commit 9dd0046

Browse files
committed
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
1 parent 916060b commit 9dd0046

File tree

5 files changed

+122
-22
lines changed

5 files changed

+122
-22
lines changed

cmd2/cmd2.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,8 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
345345
:param completekey: (optional) readline name of a completion key, default to Tab
346346
:param stdin: (optional) alternate input file object, if not specified, sys.stdin is used
347347
:param stdout: (optional) alternate output file object, if not specified, sys.stdout is used
348-
:param persistent_history_file: (optional) file path to load a persistent readline history from
349-
:param persistent_history_length: (optional) max number of lines which will be written to the history file
348+
:param persistent_history_file: (optional) file path to load a persistent cmd2 command history from
349+
:param persistent_history_length: (optional) max number of history items to write to the persistent history file
350350
:param startup_script: (optional) file path to a a script to load and execute at startup
351351
:param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell
352352
:param transcript_files: (optional) allows running transcript tests when allow_cli_args is False
@@ -3337,6 +3337,8 @@ def load_ipy(app):
33373337
history_format_group.add_argument('-v', '--verbose', action='store_true',
33383338
help='display history and include expanded commands if they\n'
33393339
'differ from the typed command')
3340+
history_format_group.add_argument('-a', '--all', action='store_true',
3341+
help='display all commands, including ones persisted from previous sessions')
33403342

33413343
history_arg_help = ("empty all history items\n"
33423344
"a one history item by number\n"
@@ -3389,18 +3391,18 @@ def do_history(self, args: argparse.Namespace) -> None:
33893391

33903392
if '..' in arg or ':' in arg:
33913393
# Get a slice of history
3392-
history = self.history.span(arg)
3394+
history = self.history.span(arg, args.all)
33933395
elif arg_is_int:
33943396
history = [self.history.get(arg)]
33953397
elif arg.startswith(r'/') and arg.endswith(r'/'):
3396-
history = self.history.regex_search(arg)
3398+
history = self.history.regex_search(arg, args.all)
33973399
else:
3398-
history = self.history.str_search(arg)
3400+
history = self.history.str_search(arg, args.all)
33993401
else:
34003402
# If no arg given, then retrieve the entire history
34013403
cowardly_refuse_to_run = True
34023404
# Get a copy of the history so it doesn't get mutated while we are using it
3403-
history = self.history[:]
3405+
history = self.history.span(':', args.all)
34043406

34053407
if args.run:
34063408
if cowardly_refuse_to_run:
@@ -3488,6 +3490,7 @@ def _initialize_history(self, hist_file):
34883490
return
34893491

34903492
self.history = history
3493+
self.history.start_session()
34913494
self.persistent_history_file = hist_file
34923495

34933496
# populate readline history

cmd2/history.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@ class History(list):
7373
regex_search() - return a list of history items which match a given regex
7474
get() - return a single element of the list, using 1 based indexing
7575
span() - given a 1-based slice, return the appropriate list of history items
76-
7776
"""
77+
def __init__(self, seq=()) -> None:
78+
super().__init__(seq)
79+
self.session_start_index = 0
80+
81+
def start_session(self) -> None:
82+
"""Start a new session, thereby setting the next index as the first index in the new session."""
83+
self.session_start_index = len(self)
7884

7985
# noinspection PyMethodMayBeStatic
8086
def _zero_based_index(self, onebased: Union[int, str]) -> int:
@@ -85,12 +91,17 @@ def _zero_based_index(self, onebased: Union[int, str]) -> int:
8591
return result
8692

8793
def append(self, new: Statement) -> None:
88-
"""Append a HistoryItem to end of the History list
94+
"""Append a HistoryItem to end of the History list.
8995
9096
:param new: command line to convert to HistoryItem and add to the end of the History list
9197
"""
9298
history_item = HistoryItem(new, len(self) + 1)
93-
list.append(self, history_item)
99+
super().append(history_item)
100+
101+
def clear(self) -> None:
102+
"""Remove all items from the History list."""
103+
super().clear()
104+
self.start_session()
94105

95106
def get(self, index: Union[int, str]) -> HistoryItem:
96107
"""Get item from the History list using 1-based indexing.
@@ -133,10 +144,11 @@ def get(self, index: Union[int, str]) -> HistoryItem:
133144
#
134145
spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]\d*)?\s*$')
135146

136-
def span(self, span: str) -> List[HistoryItem]:
147+
def span(self, span: str, include_persisted: bool = False) -> List[HistoryItem]:
137148
"""Return an index or slice of the History list,
138149
139150
:param span: string containing an index or a slice
151+
:param include_persisted: (optional) if True, then retrieve full results including from persisted history
140152
:return: a list of HistoryItems
141153
142154
This method can accommodate input in any of these forms:
@@ -191,19 +203,26 @@ def span(self, span: str) -> List[HistoryItem]:
191203
# take a slice of the array
192204
result = self[start:]
193205
elif end is not None and sep is not None:
194-
result = self[:end]
206+
if include_persisted:
207+
result = self[:end]
208+
else:
209+
result = self[self.session_start_index:end]
195210
elif start is not None:
196-
# there was no separator so it's either a posative or negative integer
211+
# there was no separator so it's either a positive or negative integer
197212
result = [self[start]]
198213
else:
199214
# we just have a separator, return the whole list
200-
result = self[:]
215+
if include_persisted:
216+
result = self[:]
217+
else:
218+
result = self[self.session_start_index:]
201219
return result
202220

203-
def str_search(self, search: str) -> List[HistoryItem]:
221+
def str_search(self, search: str, include_persisted: bool = False) -> List[HistoryItem]:
204222
"""Find history items which contain a given string
205223
206224
:param search: the string to search for
225+
:param include_persisted: (optional) if True, then search full history including from persisted history
207226
:return: a list of history items, or an empty list if the string was not found
208227
"""
209228
def isin(history_item):
@@ -212,12 +231,15 @@ def isin(history_item):
212231
inraw = sloppy in utils.norm_fold(history_item.raw)
213232
inexpanded = sloppy in utils.norm_fold(history_item.expanded)
214233
return inraw or inexpanded
215-
return [item for item in self if isin(item)]
216234

217-
def regex_search(self, regex: str) -> List[HistoryItem]:
235+
search_list = self if include_persisted else self[self.session_start_index:]
236+
return [item for item in search_list if isin(item)]
237+
238+
def regex_search(self, regex: str, include_persisted: bool = False) -> List[HistoryItem]:
218239
"""Find history items which match a given regular expression
219240
220241
:param regex: the regular expression to search for.
242+
:param include_persisted: (optional) if True, then search full history including from persisted history
221243
:return: a list of history items, or an empty list if the string was not found
222244
"""
223245
regex = regex.strip()
@@ -228,7 +250,9 @@ def regex_search(self, regex: str) -> List[HistoryItem]:
228250
def isin(hi):
229251
"""filter function for doing a regular expression search of history"""
230252
return finder.search(hi.raw) or finder.search(hi.expanded)
231-
return [itm for itm in self if isin(itm)]
253+
254+
search_list = self if include_persisted else self[self.session_start_index:]
255+
return [itm for itm in search_list if isin(itm)]
232256

233257
def truncate(self, max_length: int) -> None:
234258
"""Truncate the length of the history, dropping the oldest items if necessary

docs/freefeatures.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ All cmd_-based applications on systems with the ``readline`` module
258258
also provide `Readline Emacs editing mode`_. With this you can, for example, use **Ctrl-r** to search backward through
259259
the readline history.
260260

261-
``cmd2`` adds the option of making this readline history persistent via optional arguments to ``cmd2.Cmd.__init__()``:
261+
``cmd2`` adds the option of making this history persistent via optional arguments to ``cmd2.Cmd.__init__()``:
262262

263263
.. automethod:: cmd2.cmd2.Cmd.__init__
264264

tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959

6060
# Help text for the history command
6161
HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT | -c] [-s] [-x] [-v]
62+
[-a]
6263
[arg]
6364
6465
View, run, edit, save, or clear previously entered commands
@@ -88,7 +89,7 @@
8889
macros expanded, instead of typed commands
8990
-v, --verbose display history and include expanded commands if they
9091
differ from the typed command
91-
92+
-a, --all display all commands, including ones persisted from previous sessions
9293
"""
9394

9495
# Output from the shortcuts command with default built-in shortcuts

tests/test_history.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ def hist():
4343
HistoryItem(Statement('', raw='fourth'),4)])
4444
return h
4545

46+
@pytest.fixture
47+
def persisted_hist():
48+
from cmd2.parsing import Statement
49+
from cmd2.cmd2 import History, HistoryItem
50+
h = History([HistoryItem(Statement('', raw='first'), 1),
51+
HistoryItem(Statement('', raw='second'), 2),
52+
HistoryItem(Statement('', raw='third'), 3),
53+
HistoryItem(Statement('', raw='fourth'),4)])
54+
h.start_session()
55+
h.append(Statement('', raw='fifth'))
56+
h.append(Statement('', raw='sixth'))
57+
return h
58+
4659
def test_history_class_span(hist):
4760
for tryit in ['*', ':', '-', 'all', 'ALL']:
4861
assert hist.span(tryit) == hist
@@ -119,6 +132,62 @@ def test_history_class_span(hist):
119132
with pytest.raises(ValueError):
120133
hist.span(tryit)
121134

135+
def test_persisted_history_span(persisted_hist):
136+
for tryit in ['*', ':', '-', 'all', 'ALL']:
137+
assert persisted_hist.span(tryit, include_persisted=True) == persisted_hist
138+
assert persisted_hist.span(tryit, include_persisted=False) != persisted_hist
139+
140+
assert persisted_hist.span('3')[0].statement.raw == 'third'
141+
assert persisted_hist.span('-1')[0].statement.raw == 'sixth'
142+
143+
span = persisted_hist.span('2..')
144+
assert len(span) == 5
145+
assert span[0].statement.raw == 'second'
146+
assert span[1].statement.raw == 'third'
147+
assert span[2].statement.raw == 'fourth'
148+
assert span[3].statement.raw == 'fifth'
149+
assert span[4].statement.raw == 'sixth'
150+
151+
span = persisted_hist.span('-2..')
152+
assert len(span) == 2
153+
assert span[0].statement.raw == 'fifth'
154+
assert span[1].statement.raw == 'sixth'
155+
156+
span = persisted_hist.span('1..3')
157+
assert len(span) == 3
158+
assert span[0].statement.raw == 'first'
159+
assert span[1].statement.raw == 'second'
160+
assert span[2].statement.raw == 'third'
161+
162+
span = persisted_hist.span('2:-1')
163+
assert len(span) == 5
164+
assert span[0].statement.raw == 'second'
165+
assert span[1].statement.raw == 'third'
166+
assert span[2].statement.raw == 'fourth'
167+
assert span[3].statement.raw == 'fifth'
168+
assert span[4].statement.raw == 'sixth'
169+
170+
span = persisted_hist.span('-3:4')
171+
assert len(span) == 1
172+
assert span[0].statement.raw == 'fourth'
173+
174+
span = persisted_hist.span(':-2', include_persisted=True)
175+
assert len(span) == 5
176+
assert span[0].statement.raw == 'first'
177+
assert span[1].statement.raw == 'second'
178+
assert span[2].statement.raw == 'third'
179+
assert span[3].statement.raw == 'fourth'
180+
assert span[4].statement.raw == 'fifth'
181+
182+
span = persisted_hist.span(':-2', include_persisted=False)
183+
assert len(span) == 1
184+
assert span[0].statement.raw == 'fifth'
185+
186+
value_errors = ['fred', 'fred:joe', 'a..b', '2 ..', '1 : 3', '1:0', '0:3']
187+
for tryit in value_errors:
188+
with pytest.raises(ValueError):
189+
persisted_hist.span(tryit)
190+
122191
def test_history_class_get(hist):
123192
assert hist.get('1').statement.raw == 'first'
124193
assert hist.get(3).statement.raw == 'third'
@@ -401,7 +470,8 @@ def test_history_verbose_with_other_options(base_app):
401470
options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x']
402471
for opt in options_to_test:
403472
out, err = run_cmd(base_app, 'history -v ' + opt)
404-
assert len(out) == 3
473+
assert len(out) == 4
474+
assert out[0] == '-v can not be used with any other options'
405475
assert out[1].startswith('Usage:')
406476

407477
def test_history_verbose(base_app):
@@ -417,7 +487,8 @@ def test_history_script_with_invalid_options(base_app):
417487
options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
418488
for opt in options_to_test:
419489
out, err = run_cmd(base_app, 'history -s ' + opt)
420-
assert len(out) == 3
490+
assert len(out) == 4
491+
assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t'
421492
assert out[1].startswith('Usage:')
422493

423494
def test_history_script(base_app):
@@ -432,7 +503,8 @@ def test_history_expanded_with_invalid_options(base_app):
432503
options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
433504
for opt in options_to_test:
434505
out, err = run_cmd(base_app, 'history -x ' + opt)
435-
assert len(out) == 3
506+
assert len(out) == 4
507+
assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t'
436508
assert out[1].startswith('Usage:')
437509

438510
def test_history_expanded(base_app):

0 commit comments

Comments
 (0)