Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4afb9e7
First version of adding an expanded option to history items
kmvanbrunt Dec 3, 2018
23970df
Merge branch 'master' into history
kmvanbrunt Dec 5, 2018
5b9281f
Merge branch 'master' into history
kmvanbrunt Dec 6, 2018
8d001fe
Merge branch 'master' into history
kmvanbrunt Dec 6, 2018
d9dc236
Merge branch 'master' into history
kmvanbrunt Dec 7, 2018
0ee0769
Merge branch 'master' into history
kmvanbrunt Jan 9, 2019
80327e0
Merge branch 'master' into history
tleonhardt Feb 8, 2019
321a8c7
Extract history classes and test into their own files
kotfu Feb 9, 2019
3911335
Added -x option to history command for #545
kotfu Feb 10, 2019
b7fc503
Move the rest of the history tests into test_history.py
kotfu Feb 10, 2019
740bf75
Fix flake errors
kotfu Feb 10, 2019
e174e23
Fix incorrect example in alias help message
kotfu Feb 17, 2019
0abcb70
expanded history searches with string or regex for #545
kotfu Feb 17, 2019
a3511a6
Merge branch 'master' into history
kmvanbrunt Feb 21, 2019
eecd1c5
Merged master into history branch and fixed merge conflicts
tleonhardt Feb 27, 2019
40b03a5
Fixed unit test which was slow on macOS and hung forever on Windows
tleonhardt Feb 27, 2019
2f7a4ba
Fixed comments
kmvanbrunt Feb 27, 2019
fe4b3fd
Merge branch 'master' into history
tleonhardt Mar 1, 2019
46df1c1
Merged from master and resolved conflicts in cmd2.py
tleonhardt Mar 2, 2019
009cb87
Potential fixes for outstanding multi-line issues in history command
tleonhardt Mar 3, 2019
7349624
Fixed a couple bugs and added unit tests
tleonhardt Mar 3, 2019
c628722
Merged master into history and resolved conflicts
tleonhardt Mar 5, 2019
6370022
Updated CHANGELOG
tleonhardt Mar 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## 0.9.11 (TBD, 2019)
* Bug Fixes
* Fixed bug in how **history** command deals with multiline commands when output to a script
* Enhancements
* Improvements to the **history** command
* Simplified the display format and made it more similar to **bash**
* Added **-x**, **--expanded** flag
* output expanded commands instead of entered command (expands aliases, macros, and shortcuts)
* Added **-v**, **--verbose** flag
* display history and include expanded commands if they differ from the typed command
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
* Potentially breaking changes
* Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now
Expand Down
193 changes: 44 additions & 149 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .parsing import StatementParser, Statement, Macro, MacroArg
from .history import History, HistoryItem

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


class HistoryItem(str):
"""Class used to represent an item in the History list.

Thin wrapper around str class which adds a custom format for printing. It
also keeps track of its index in the list as well as a lowercase
representation of itself for convenience/efficiency.

"""
listformat = '-------------------------[{}]\n{}\n'

# noinspection PyUnusedLocal
def __init__(self, instr: str) -> None:
str.__init__(self)
self.lowercase = self.lower()
self.idx = None

def pr(self) -> str:
"""Represent a HistoryItem in a pretty fashion suitable for printing.

:return: pretty print string version of a HistoryItem
"""
return self.listformat.format(self.idx, str(self).rstrip())


class Cmd(cmd.Cmd):
"""An easy but powerful framework for writing line-oriented command interpreters.

Expand All @@ -325,7 +302,7 @@ class Cmd(cmd.Cmd):
# Attributes used to configure the StatementParser, best not to change these at runtime
multiline_commands = []
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
terminators = [';']
terminators = [constants.MULTILINE_TERMINATOR]

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

stop = func(statement)

Expand Down Expand Up @@ -2070,7 +2047,7 @@ def default(self, statement: Statement) -> Optional[bool]:
"""
if self.default_to_shell:
if 'shell' not in self.exclude_from_history:
self.history.append(statement.raw)
self.history.append(statement)

return self.do_shell(statement.command_and_args)
else:
Expand Down Expand Up @@ -3188,18 +3165,27 @@ def load_ipy(app):
load_ipy(bridge)

history_parser = ACArgumentParser()
history_parser_group = history_parser.add_mutually_exclusive_group()
history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
history_parser_group.add_argument('-e', '--edit', action='store_true',
history_action_group = history_parser.add_mutually_exclusive_group()
history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
history_action_group.add_argument('-e', '--edit', action='store_true',
help='edit and then run selected history items')
history_parser_group.add_argument('-s', '--script', action='store_true', help='output commands in script format')
setattr(history_parser_group.add_argument('-o', '--output-file', metavar='FILE',
help='output commands to a script file'),
setattr(history_action_group.add_argument('-o', '--output-file', metavar='FILE',
help='output commands to a script file, implies -s'),
ACTION_ARG_CHOICES, ('path_complete',))
setattr(history_parser_group.add_argument('-t', '--transcript',
help='output commands and results to a transcript file'),
setattr(history_action_group.add_argument('-t', '--transcript',
help='output commands and results to a transcript file, implies -s'),
ACTION_ARG_CHOICES, ('path_complete',))
history_parser_group.add_argument('-c', '--clear', action="store_true", help='clear all history')
history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')

history_format_group = history_parser.add_argument_group(title='formatting')
history_script_help = 'output commands in script format, i.e. without command numbers'
history_format_group.add_argument('-s', '--script', action='store_true', help=history_script_help)
history_expand_help = 'output expanded commands instead of entered command'
history_format_group.add_argument('-x', '--expanded', action='store_true', help=history_expand_help)
history_format_group.add_argument('-v', '--verbose', action='store_true',
help='display history and include expanded commands if they'
' differ from the typed command')

history_arg_help = ("empty all history items\n"
"a one history item by number\n"
"a..b, a:b, a:, ..b items by indices (inclusive)\n"
Expand All @@ -3211,6 +3197,19 @@ def load_ipy(app):
def do_history(self, args: argparse.Namespace) -> None:
"""View, run, edit, save, or clear previously entered commands"""

# -v must be used alone with no other options
if args.verbose:
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
self.poutput("-v can not be used with any other options")
self.poutput(self.history_parser.format_usage())
return

# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript):
self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t")
self.poutput(self.history_parser.format_usage())
return

if args.clear:
# Clear command and readline history
self.history.clear()
Expand Down Expand Up @@ -3257,7 +3256,10 @@ def do_history(self, args: argparse.Namespace) -> None:
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
with os.fdopen(fd, 'w') as fobj:
for command in history:
fobj.write('{}\n'.format(command))
if command.statement.multiline_command:
fobj.write('{}\n'.format(command.expanded.rstrip()))
else:
fobj.write('{}\n'.format(command))
try:
self.do_edit(fname)
self.do_load(fname)
Expand All @@ -3269,7 +3271,10 @@ def do_history(self, args: argparse.Namespace) -> None:
try:
with open(os.path.expanduser(args.output_file), 'w') as fobj:
for command in history:
fobj.write('{}\n'.format(command))
if command.statement.multiline_command:
fobj.write('{}\n'.format(command.expanded.rstrip()))
else:
fobj.write('{}\n'.format(command))
plural = 's' if len(history) > 1 else ''
self.pfeedback('{} command{} saved to {}'.format(len(history), plural, args.output_file))
except Exception as e:
Expand All @@ -3279,10 +3284,7 @@ def do_history(self, args: argparse.Namespace) -> None:
else:
# Display the history items retrieved
for hi in history:
if args.script:
self.poutput(hi)
else:
self.poutput(hi.pr())
self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose))

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


class History(list):
""" A list of HistoryItems that knows how to respond to user requests. """

# noinspection PyMethodMayBeStatic
def _zero_based_index(self, onebased: int) -> int:
"""Convert a one-based index to a zero-based index."""
result = onebased
if result > 0:
result -= 1
return result

def _to_index(self, raw: str) -> Optional[int]:
if raw:
result = self._zero_based_index(int(raw))
else:
result = None
return result

spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$')

def span(self, raw: str) -> List[HistoryItem]:
"""Parses the input string search for a span pattern and if if found, returns a slice from the History list.

:param raw: string potentially containing a span of the forms a..b, a:b, a:, ..b
:return: slice from the History list
"""
if raw.lower() in ('*', '-', 'all'):
raw = ':'
results = self.spanpattern.search(raw)
if not results:
raise IndexError
if not results.group('separator'):
return [self[self._to_index(results.group('start'))]]
start = self._to_index(results.group('start')) or 0 # Ensure start is not None
end = self._to_index(results.group('end'))
reverse = False
if end is not None:
if end < start:
(start, end) = (end, start)
reverse = True
end += 1
result = self[start:end]
if reverse:
result.reverse()
return result

rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*-\s*(?P<end>[\d]+)?\s*$')

def append(self, new: str) -> None:
"""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
"""
new = HistoryItem(new)
list.append(self, new)
new.idx = len(self)

def get(self, getme: Optional[Union[int, str]] = None) -> List[HistoryItem]:
"""Get an item or items from the History list using 1-based indexing.

:param getme: optional item(s) to get (either an integer index or string to search for)
:return: list of HistoryItems matching the retrieval criteria
"""
if not getme:
return self
try:
getme = int(getme)
if getme < 0:
return self[:(-1 * getme)]
else:
return [self[getme - 1]]
except IndexError:
return []
except ValueError:
range_result = self.rangePattern.search(getme)
if range_result:
start = range_result.group('start') or None
end = range_result.group('start') or None
if start:
start = int(start) - 1
if end:
end = int(end)
return self[start:end]

getme = getme.strip()

if getme.startswith(r'/') and getme.endswith(r'/'):
finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE)

def isin(hi):
"""Listcomp filter function for doing a regular expression search of History.

:param hi: HistoryItem
:return: bool - True if search matches
"""
return finder.search(hi)
else:
def isin(hi):
"""Listcomp filter function for doing a case-insensitive string search of History.

:param hi: HistoryItem
:return: bool - True if search matches
"""
return utils.norm_fold(getme) in utils.norm_fold(hi)
return [itm for itm in self if isin(itm)]


class Statekeeper(object):
"""Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
def __init__(self, obj: Any, attribs: Iterable) -> None:
Expand Down
1 change: 1 addition & 0 deletions cmd2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT]
REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND]
COMMENT_CHAR = '#'
MULTILINE_TERMINATOR = ';'

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