Skip to content

Commit dddf5d0

Browse files
authored
Merge pull request #635 from python-cmd2/history
Improvements to history command
2 parents eb86c73 + 6370022 commit dddf5d0

File tree

10 files changed

+678
-481
lines changed

10 files changed

+678
-481
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## 0.9.11 (TBD, 2019)
2+
* Bug Fixes
3+
* Fixed bug in how **history** command deals with multiline commands when output to a script
24
* Enhancements
5+
* Improvements to the **history** command
6+
* Simplified the display format and made it more similar to **bash**
7+
* Added **-x**, **--expanded** flag
8+
* output expanded commands instead of entered command (expands aliases, macros, and shortcuts)
9+
* Added **-v**, **--verbose** flag
10+
* display history and include expanded commands if they differ from the typed command
311
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
412
* Potentially breaking changes
513
* Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now

cmd2/cmd2.py

Lines changed: 44 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
5050
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
5151
from .parsing import StatementParser, Statement, Macro, MacroArg
52+
from .history import History, HistoryItem
5253

5354
# Set up readline
5455
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt
@@ -290,30 +291,6 @@ class EmptyStatement(Exception):
290291
pass
291292

292293

293-
class HistoryItem(str):
294-
"""Class used to represent an item in the History list.
295-
296-
Thin wrapper around str class which adds a custom format for printing. It
297-
also keeps track of its index in the list as well as a lowercase
298-
representation of itself for convenience/efficiency.
299-
300-
"""
301-
listformat = '-------------------------[{}]\n{}\n'
302-
303-
# noinspection PyUnusedLocal
304-
def __init__(self, instr: str) -> None:
305-
str.__init__(self)
306-
self.lowercase = self.lower()
307-
self.idx = None
308-
309-
def pr(self) -> str:
310-
"""Represent a HistoryItem in a pretty fashion suitable for printing.
311-
312-
:return: pretty print string version of a HistoryItem
313-
"""
314-
return self.listformat.format(self.idx, str(self).rstrip())
315-
316-
317294
class Cmd(cmd.Cmd):
318295
"""An easy but powerful framework for writing line-oriented command interpreters.
319296
@@ -325,7 +302,7 @@ class Cmd(cmd.Cmd):
325302
# Attributes used to configure the StatementParser, best not to change these at runtime
326303
multiline_commands = []
327304
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
328-
terminators = [';']
305+
terminators = [constants.MULTILINE_TERMINATOR]
329306

330307
# Attributes which are NOT dynamically settable at runtime
331308
allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
@@ -2007,7 +1984,7 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
20071984
if func:
20081985
# Since we have a valid command store it in the history
20091986
if statement.command not in self.exclude_from_history:
2010-
self.history.append(statement.raw)
1987+
self.history.append(statement)
20111988

20121989
stop = func(statement)
20131990

@@ -2070,7 +2047,7 @@ def default(self, statement: Statement) -> Optional[bool]:
20702047
"""
20712048
if self.default_to_shell:
20722049
if 'shell' not in self.exclude_from_history:
2073-
self.history.append(statement.raw)
2050+
self.history.append(statement)
20742051

20752052
return self.do_shell(statement.command_and_args)
20762053
else:
@@ -3188,18 +3165,27 @@ def load_ipy(app):
31883165
load_ipy(bridge)
31893166

31903167
history_parser = ACArgumentParser()
3191-
history_parser_group = history_parser.add_mutually_exclusive_group()
3192-
history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
3193-
history_parser_group.add_argument('-e', '--edit', action='store_true',
3168+
history_action_group = history_parser.add_mutually_exclusive_group()
3169+
history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
3170+
history_action_group.add_argument('-e', '--edit', action='store_true',
31943171
help='edit and then run selected history items')
3195-
history_parser_group.add_argument('-s', '--script', action='store_true', help='output commands in script format')
3196-
setattr(history_parser_group.add_argument('-o', '--output-file', metavar='FILE',
3197-
help='output commands to a script file'),
3172+
setattr(history_action_group.add_argument('-o', '--output-file', metavar='FILE',
3173+
help='output commands to a script file, implies -s'),
31983174
ACTION_ARG_CHOICES, ('path_complete',))
3199-
setattr(history_parser_group.add_argument('-t', '--transcript',
3200-
help='output commands and results to a transcript file'),
3175+
setattr(history_action_group.add_argument('-t', '--transcript',
3176+
help='output commands and results to a transcript file, implies -s'),
32013177
ACTION_ARG_CHOICES, ('path_complete',))
3202-
history_parser_group.add_argument('-c', '--clear', action="store_true", help='clear all history')
3178+
history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')
3179+
3180+
history_format_group = history_parser.add_argument_group(title='formatting')
3181+
history_script_help = 'output commands in script format, i.e. without command numbers'
3182+
history_format_group.add_argument('-s', '--script', action='store_true', help=history_script_help)
3183+
history_expand_help = 'output expanded commands instead of entered command'
3184+
history_format_group.add_argument('-x', '--expanded', action='store_true', help=history_expand_help)
3185+
history_format_group.add_argument('-v', '--verbose', action='store_true',
3186+
help='display history and include expanded commands if they'
3187+
' differ from the typed command')
3188+
32033189
history_arg_help = ("empty all history items\n"
32043190
"a one history item by number\n"
32053191
"a..b, a:b, a:, ..b items by indices (inclusive)\n"
@@ -3211,6 +3197,19 @@ def load_ipy(app):
32113197
def do_history(self, args: argparse.Namespace) -> None:
32123198
"""View, run, edit, save, or clear previously entered commands"""
32133199

3200+
# -v must be used alone with no other options
3201+
if args.verbose:
3202+
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
3203+
self.poutput("-v can not be used with any other options")
3204+
self.poutput(self.history_parser.format_usage())
3205+
return
3206+
3207+
# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
3208+
if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript):
3209+
self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t")
3210+
self.poutput(self.history_parser.format_usage())
3211+
return
3212+
32143213
if args.clear:
32153214
# Clear command and readline history
32163215
self.history.clear()
@@ -3257,7 +3256,10 @@ def do_history(self, args: argparse.Namespace) -> None:
32573256
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
32583257
with os.fdopen(fd, 'w') as fobj:
32593258
for command in history:
3260-
fobj.write('{}\n'.format(command))
3259+
if command.statement.multiline_command:
3260+
fobj.write('{}\n'.format(command.expanded.rstrip()))
3261+
else:
3262+
fobj.write('{}\n'.format(command))
32613263
try:
32623264
self.do_edit(fname)
32633265
self.do_load(fname)
@@ -3269,7 +3271,10 @@ def do_history(self, args: argparse.Namespace) -> None:
32693271
try:
32703272
with open(os.path.expanduser(args.output_file), 'w') as fobj:
32713273
for command in history:
3272-
fobj.write('{}\n'.format(command))
3274+
if command.statement.multiline_command:
3275+
fobj.write('{}\n'.format(command.expanded.rstrip()))
3276+
else:
3277+
fobj.write('{}\n'.format(command))
32733278
plural = 's' if len(history) > 1 else ''
32743279
self.pfeedback('{} command{} saved to {}'.format(len(history), plural, args.output_file))
32753280
except Exception as e:
@@ -3279,10 +3284,7 @@ def do_history(self, args: argparse.Namespace) -> None:
32793284
else:
32803285
# Display the history items retrieved
32813286
for hi in history:
3282-
if args.script:
3283-
self.poutput(hi)
3284-
else:
3285-
self.poutput(hi.pr())
3287+
self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose))
32863288

32873289
def _generate_transcript(self, history: List[HistoryItem], transcript_file: str) -> None:
32883290
"""Generate a transcript file from a given history of commands."""
@@ -3807,113 +3809,6 @@ def register_cmdfinalization_hook(self, func: Callable[[plugin.CommandFinalizati
38073809
self._cmdfinalization_hooks.append(func)
38083810

38093811

3810-
class History(list):
3811-
""" A list of HistoryItems that knows how to respond to user requests. """
3812-
3813-
# noinspection PyMethodMayBeStatic
3814-
def _zero_based_index(self, onebased: int) -> int:
3815-
"""Convert a one-based index to a zero-based index."""
3816-
result = onebased
3817-
if result > 0:
3818-
result -= 1
3819-
return result
3820-
3821-
def _to_index(self, raw: str) -> Optional[int]:
3822-
if raw:
3823-
result = self._zero_based_index(int(raw))
3824-
else:
3825-
result = None
3826-
return result
3827-
3828-
spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$')
3829-
3830-
def span(self, raw: str) -> List[HistoryItem]:
3831-
"""Parses the input string search for a span pattern and if if found, returns a slice from the History list.
3832-
3833-
:param raw: string potentially containing a span of the forms a..b, a:b, a:, ..b
3834-
:return: slice from the History list
3835-
"""
3836-
if raw.lower() in ('*', '-', 'all'):
3837-
raw = ':'
3838-
results = self.spanpattern.search(raw)
3839-
if not results:
3840-
raise IndexError
3841-
if not results.group('separator'):
3842-
return [self[self._to_index(results.group('start'))]]
3843-
start = self._to_index(results.group('start')) or 0 # Ensure start is not None
3844-
end = self._to_index(results.group('end'))
3845-
reverse = False
3846-
if end is not None:
3847-
if end < start:
3848-
(start, end) = (end, start)
3849-
reverse = True
3850-
end += 1
3851-
result = self[start:end]
3852-
if reverse:
3853-
result.reverse()
3854-
return result
3855-
3856-
rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*-\s*(?P<end>[\d]+)?\s*$')
3857-
3858-
def append(self, new: str) -> None:
3859-
"""Append a HistoryItem to end of the History list
3860-
3861-
:param new: command line to convert to HistoryItem and add to the end of the History list
3862-
"""
3863-
new = HistoryItem(new)
3864-
list.append(self, new)
3865-
new.idx = len(self)
3866-
3867-
def get(self, getme: Optional[Union[int, str]] = None) -> List[HistoryItem]:
3868-
"""Get an item or items from the History list using 1-based indexing.
3869-
3870-
:param getme: optional item(s) to get (either an integer index or string to search for)
3871-
:return: list of HistoryItems matching the retrieval criteria
3872-
"""
3873-
if not getme:
3874-
return self
3875-
try:
3876-
getme = int(getme)
3877-
if getme < 0:
3878-
return self[:(-1 * getme)]
3879-
else:
3880-
return [self[getme - 1]]
3881-
except IndexError:
3882-
return []
3883-
except ValueError:
3884-
range_result = self.rangePattern.search(getme)
3885-
if range_result:
3886-
start = range_result.group('start') or None
3887-
end = range_result.group('start') or None
3888-
if start:
3889-
start = int(start) - 1
3890-
if end:
3891-
end = int(end)
3892-
return self[start:end]
3893-
3894-
getme = getme.strip()
3895-
3896-
if getme.startswith(r'/') and getme.endswith(r'/'):
3897-
finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE)
3898-
3899-
def isin(hi):
3900-
"""Listcomp filter function for doing a regular expression search of History.
3901-
3902-
:param hi: HistoryItem
3903-
:return: bool - True if search matches
3904-
"""
3905-
return finder.search(hi)
3906-
else:
3907-
def isin(hi):
3908-
"""Listcomp filter function for doing a case-insensitive string search of History.
3909-
3910-
:param hi: HistoryItem
3911-
:return: bool - True if search matches
3912-
"""
3913-
return utils.norm_fold(getme) in utils.norm_fold(hi)
3914-
return [itm for itm in self if isin(itm)]
3915-
3916-
39173812
class Statekeeper(object):
39183813
"""Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
39193814
def __init__(self, obj: Any, attribs: Iterable) -> None:

cmd2/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT]
1414
REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND]
1515
COMMENT_CHAR = '#'
16+
MULTILINE_TERMINATOR = ';'
1617

1718
# Regular expression to match ANSI escape codes
1819
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')

0 commit comments

Comments
 (0)