Skip to content

Commit d1a970b

Browse files
committed
Merged master into this branch and resolved conflicts in CHANGELOG
2 parents f765be2 + dddf5d0 commit d1a970b

File tree

10 files changed

+677
-481
lines changed

10 files changed

+677
-481
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
## 0.9.11 (TBD, 2019)
22
* Bug Fixes
3+
* Fixed bug in how **history** command deals with multiline commands when output to a script
34
* Fixed a bug when the ``with_argument_list`` decorator is called with the optional ``preserve_quotes`` argument
45
* Enhancements
6+
* Improvements to the **history** command
7+
* Simplified the display format and made it more similar to **bash**
8+
* Added **-x**, **--expanded** flag
9+
* output expanded commands instead of entered command (expands aliases, macros, and shortcuts)
10+
* Added **-v**, **--verbose** flag
11+
* display history and include expanded commands if they differ from the typed command
512
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
613
* Potentially breaking changes
714
* 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
@@ -295,30 +296,6 @@ class EmptyStatement(Exception):
295296
pass
296297

297298

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

335312
# Attributes which are NOT dynamically settable at runtime
336313
allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
@@ -2012,7 +1989,7 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
20121989
if func:
20131990
# Since we have a valid command store it in the history
20141991
if statement.command not in self.exclude_from_history:
2015-
self.history.append(statement.raw)
1992+
self.history.append(statement)
20161993

20171994
stop = func(statement)
20181995

@@ -2075,7 +2052,7 @@ def default(self, statement: Statement) -> Optional[bool]:
20752052
"""
20762053
if self.default_to_shell:
20772054
if 'shell' not in self.exclude_from_history:
2078-
self.history.append(statement.raw)
2055+
self.history.append(statement)
20792056

20802057
return self.do_shell(statement.command_and_args)
20812058
else:
@@ -3193,18 +3170,27 @@ def load_ipy(app):
31933170
load_ipy(bridge)
31943171

31953172
history_parser = ACArgumentParser()
3196-
history_parser_group = history_parser.add_mutually_exclusive_group()
3197-
history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
3198-
history_parser_group.add_argument('-e', '--edit', action='store_true',
3173+
history_action_group = history_parser.add_mutually_exclusive_group()
3174+
history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
3175+
history_action_group.add_argument('-e', '--edit', action='store_true',
31993176
help='edit and then run selected history items')
3200-
history_parser_group.add_argument('-s', '--script', action='store_true', help='output commands in script format')
3201-
setattr(history_parser_group.add_argument('-o', '--output-file', metavar='FILE',
3202-
help='output commands to a script file'),
3177+
setattr(history_action_group.add_argument('-o', '--output-file', metavar='FILE',
3178+
help='output commands to a script file, implies -s'),
32033179
ACTION_ARG_CHOICES, ('path_complete',))
3204-
setattr(history_parser_group.add_argument('-t', '--transcript',
3205-
help='output commands and results to a transcript file'),
3180+
setattr(history_action_group.add_argument('-t', '--transcript',
3181+
help='output commands and results to a transcript file, implies -s'),
32063182
ACTION_ARG_CHOICES, ('path_complete',))
3207-
history_parser_group.add_argument('-c', '--clear', action="store_true", help='clear all history')
3183+
history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')
3184+
3185+
history_format_group = history_parser.add_argument_group(title='formatting')
3186+
history_script_help = 'output commands in script format, i.e. without command numbers'
3187+
history_format_group.add_argument('-s', '--script', action='store_true', help=history_script_help)
3188+
history_expand_help = 'output expanded commands instead of entered command'
3189+
history_format_group.add_argument('-x', '--expanded', action='store_true', help=history_expand_help)
3190+
history_format_group.add_argument('-v', '--verbose', action='store_true',
3191+
help='display history and include expanded commands if they'
3192+
' differ from the typed command')
3193+
32083194
history_arg_help = ("empty all history items\n"
32093195
"a one history item by number\n"
32103196
"a..b, a:b, a:, ..b items by indices (inclusive)\n"
@@ -3216,6 +3202,19 @@ def load_ipy(app):
32163202
def do_history(self, args: argparse.Namespace) -> None:
32173203
"""View, run, edit, save, or clear previously entered commands"""
32183204

3205+
# -v must be used alone with no other options
3206+
if args.verbose:
3207+
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
3208+
self.poutput("-v can not be used with any other options")
3209+
self.poutput(self.history_parser.format_usage())
3210+
return
3211+
3212+
# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
3213+
if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript):
3214+
self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t")
3215+
self.poutput(self.history_parser.format_usage())
3216+
return
3217+
32193218
if args.clear:
32203219
# Clear command and readline history
32213220
self.history.clear()
@@ -3262,7 +3261,10 @@ def do_history(self, args: argparse.Namespace) -> None:
32623261
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
32633262
with os.fdopen(fd, 'w') as fobj:
32643263
for command in history:
3265-
fobj.write('{}\n'.format(command))
3264+
if command.statement.multiline_command:
3265+
fobj.write('{}\n'.format(command.expanded.rstrip()))
3266+
else:
3267+
fobj.write('{}\n'.format(command))
32663268
try:
32673269
self.do_edit(fname)
32683270
self.do_load(fname)
@@ -3274,7 +3276,10 @@ def do_history(self, args: argparse.Namespace) -> None:
32743276
try:
32753277
with open(os.path.expanduser(args.output_file), 'w') as fobj:
32763278
for command in history:
3277-
fobj.write('{}\n'.format(command))
3279+
if command.statement.multiline_command:
3280+
fobj.write('{}\n'.format(command.expanded.rstrip()))
3281+
else:
3282+
fobj.write('{}\n'.format(command))
32783283
plural = 's' if len(history) > 1 else ''
32793284
self.pfeedback('{} command{} saved to {}'.format(len(history), plural, args.output_file))
32803285
except Exception as e:
@@ -3284,10 +3289,7 @@ def do_history(self, args: argparse.Namespace) -> None:
32843289
else:
32853290
# Display the history items retrieved
32863291
for hi in history:
3287-
if args.script:
3288-
self.poutput(hi)
3289-
else:
3290-
self.poutput(hi.pr())
3292+
self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose))
32913293

32923294
def _generate_transcript(self, history: List[HistoryItem], transcript_file: str) -> None:
32933295
"""Generate a transcript file from a given history of commands."""
@@ -3812,113 +3814,6 @@ def register_cmdfinalization_hook(self, func: Callable[[plugin.CommandFinalizati
38123814
self._cmdfinalization_hooks.append(func)
38133815

38143816

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