summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py183
-rw-r--r--cmd2/history.py167
-rw-r--r--cmd2/parsing.py20
-rw-r--r--tests/conftest.py16
-rw-r--r--tests/test_cmd2.py314
-rw-r--r--tests/test_history.py410
-rw-r--r--tests/transcripts/from_cmdloop.txt18
7 files changed, 651 insertions, 477 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 24e140fd..c404ee1d 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -49,6 +49,7 @@ from . import utils
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
@@ -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.
@@ -2007,7 +1984,7 @@ class Cmd(cmd.Cmd):
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)
@@ -2070,7 +2047,7 @@ class Cmd(cmd.Cmd):
"""
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:
@@ -2283,7 +2260,7 @@ class Cmd(cmd.Cmd):
" would for the actual command the alias resolves to.\n"
"\n"
"Examples:\n"
- " alias ls !ls -lF\n"
+ " alias create ls !ls -lF\n"
" alias create show_log !cat \"log file.txt\"\n"
" alias create save_results print_results \">\" out.txt\n")
@@ -3188,18 +3165,27 @@ class Cmd(cmd.Cmd):
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"
@@ -3211,6 +3197,19 @@ class Cmd(cmd.Cmd):
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()
@@ -3279,10 +3278,7 @@ class Cmd(cmd.Cmd):
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."""
@@ -3807,113 +3803,6 @@ class Cmd(cmd.Cmd):
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:
diff --git a/cmd2/history.py b/cmd2/history.py
new file mode 100644
index 00000000..26ff5d4d
--- /dev/null
+++ b/cmd2/history.py
@@ -0,0 +1,167 @@
+# coding=utf-8
+"""
+History management classes
+"""
+
+import re
+
+from typing import List, Optional, Union
+
+from . import utils
+from .parsing import Statement
+
+
+class HistoryItem(str):
+ """Class used to represent one command in the History list"""
+ listformat = ' {:>4} {}\n'
+ ex_listformat = ' {:>4}x {}\n'
+
+ def __new__(cls, statement: Statement):
+ """Create a new instance of HistoryItem
+
+ We must override __new__ because we are subclassing `str` which is
+ immutable and takes a different number of arguments as Statement.
+ """
+ hi = super().__new__(cls, statement.raw)
+ hi.statement = statement
+ hi.idx = None
+ return hi
+
+ @property
+ def expanded(self) -> str:
+ """Return the command as run which includes shortcuts and aliases resolved plus any changes made in hooks"""
+ return self.statement.expanded_command_line
+
+ def pr(self, script=False, expanded=False, verbose=False) -> str:
+ """Represent a HistoryItem in a pretty fashion suitable for printing.
+
+ If you pass verbose=True, script and expanded will be ignored
+
+ :return: pretty print string version of a HistoryItem
+ """
+ if verbose:
+ ret_str = self.listformat.format(self.idx, str(self).rstrip())
+ if self != self.expanded:
+ ret_str += self.ex_listformat.format(self.idx, self.expanded.rstrip())
+ else:
+ if script:
+ # display without entry numbers
+ if expanded:
+ ret_str = self.expanded.rstrip()
+ else:
+ ret_str = str(self)
+ else:
+ # display a numbered list
+ if expanded:
+ ret_str = self.listformat.format(self.idx, self.expanded.rstrip())
+ else:
+ ret_str = self.listformat.format(self.idx, str(self).rstrip())
+ return ret_str
+
+
+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: Statement) -> 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) or finder.search(hi.expanded)
+ 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
+ """
+ srch = utils.norm_fold(getme)
+ return srch in utils.norm_fold(hi) or srch in utils.norm_fold(hi.expanded)
+ return [itm for itm in self if isin(itm)]
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index d4f82ac9..070d3774 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -188,6 +188,26 @@ class Statement(str):
return rtn
@property
+ def expanded_command_line(self) -> str:
+ """Contains command_and_args plus any ending terminator, suffix, and redirection chars"""
+ rtn = self.command_and_args
+ if self.terminator:
+ rtn += self.terminator
+
+ if self.suffix:
+ rtn += ' ' + self.suffix
+
+ if self.pipe_to:
+ rtn += ' | ' + self.pipe_to
+
+ if self.output:
+ rtn += ' ' + self.output
+ if self.output_to:
+ rtn += ' ' + self.output_to
+
+ return rtn
+
+ @property
def argv(self) -> List[str]:
"""a list of arguments a la sys.argv.
diff --git a/tests/conftest.py b/tests/conftest.py
index 223389b9..f41afb64 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -28,7 +28,7 @@ except ImportError:
# Help text for base cmd2.Cmd application
BASE_HELP = """Documented commands (type help <topic>):
========================================
-alias help load py quit shell
+alias help load py quit shell
edit history macro pyscript set shortcuts
""" # noqa: W291
@@ -50,7 +50,8 @@ shortcuts List available shortcuts
"""
# Help text for the history command
-HELP_HISTORY = """Usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c] [arg]
+HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT | -c] [-s] [-x] [-v]
+ [arg]
View, run, edit, save, or clear previously entered commands
@@ -65,12 +66,17 @@ optional arguments:
-h, --help show this help message and exit
-r, --run run selected history items
-e, --edit edit and then run selected history items
- -s, --script output commands in script format
-o, --output-file FILE
- output commands to a script file
+ output commands to a script file, implies -s
-t, --transcript TRANSCRIPT
- output commands and results to a transcript file
+ output commands and results to a transcript file, implies -s
-c, --clear clear all history
+
+formatting:
+ -s, --script output commands in script format, i.e. without command numbers
+ -x, --expanded output expanded commands instead of entered command
+ -v, --verbose display history and include expanded commands if they differ from the typed command
+
"""
# Output from the shortcuts command with default built-in shortcuts
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index faef21f9..8d0d56c6 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -56,10 +56,6 @@ def test_base_help_verbose(base_app):
out = run_cmd(base_app, 'help --verbose')
assert out == expected
-def test_base_help_history(base_app):
- out = run_cmd(base_app, 'help history')
- assert out == normalize(HELP_HISTORY)
-
def test_base_argparse_help(base_app, capsys):
# Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
run_cmd(base_app, 'set -h')
@@ -302,206 +298,6 @@ def test_base_error(base_app):
assert "is not a recognized command" in out[0]
-@pytest.fixture
-def hist():
- from cmd2.cmd2 import History, HistoryItem
- h = History([HistoryItem('first'), HistoryItem('second'), HistoryItem('third'), HistoryItem('fourth')])
- return h
-
-def test_history_span(hist):
- h = hist
- assert h == ['first', 'second', 'third', 'fourth']
- assert h.span('-2..') == ['third', 'fourth']
- assert h.span('2..3') == ['second', 'third'] # Inclusive of end
- assert h.span('3') == ['third']
- assert h.span(':') == h
- assert h.span('2..') == ['second', 'third', 'fourth']
- assert h.span('-1') == ['fourth']
- assert h.span('-2..-3') == ['third', 'second']
- assert h.span('*') == h
-
-def test_history_get(hist):
- h = hist
- assert h == ['first', 'second', 'third', 'fourth']
- assert h.get('') == h
- assert h.get('-2') == h[:-2]
- assert h.get('5') == []
- assert h.get('2-3') == ['second'] # Exclusive of end
- assert h.get('ir') == ['first', 'third'] # Normal string search for all elements containing "ir"
- assert h.get('/i.*d/') == ['third'] # Regex string search "i", then anything, then "d"
-
-def test_base_history(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history')
- expected = normalize("""
--------------------------[1]
-help
--------------------------[2]
-shortcuts
-""")
- assert out == expected
-
- out = run_cmd(base_app, 'history he')
- expected = normalize("""
--------------------------[1]
-help
-""")
- assert out == expected
-
- out = run_cmd(base_app, 'history sh')
- expected = normalize("""
--------------------------[2]
-shortcuts
-""")
- assert out == expected
-
-def test_history_script_format(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history -s')
- expected = normalize("""
-help
-shortcuts
-""")
- assert out == expected
-
-def test_history_with_string_argument(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history help')
- expected = normalize("""
--------------------------[1]
-help
--------------------------[3]
-help history
-""")
- assert out == expected
-
-
-def test_history_with_integer_argument(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history 1')
- expected = normalize("""
--------------------------[1]
-help
-""")
- assert out == expected
-
-
-def test_history_with_integer_span(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history 1..2')
- expected = normalize("""
--------------------------[1]
-help
--------------------------[2]
-shortcuts
-""")
- assert out == expected
-
-def test_history_with_span_start(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history 2:')
- expected = normalize("""
--------------------------[2]
-shortcuts
--------------------------[3]
-help history
-""")
- assert out == expected
-
-def test_history_with_span_end(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history :2')
- expected = normalize("""
--------------------------[1]
-help
--------------------------[2]
-shortcuts
-""")
- assert out == expected
-
-def test_history_with_span_index_error(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'help history')
- run_cmd(base_app, '!ls -hal :')
- out = run_cmd(base_app, 'history "hal :"')
- expected = normalize("""
--------------------------[3]
-!ls -hal :
-""")
- assert out == expected
-
-def test_history_output_file(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
-
- fd, fname = tempfile.mkstemp(prefix='', suffix='.txt')
- os.close(fd)
- run_cmd(base_app, 'history -o "{}"'.format(fname))
- expected = normalize('\n'.join(['help', 'shortcuts', 'help history']))
- with open(fname) as f:
- content = normalize(f.read())
- assert content == expected
-
-def test_history_edit(base_app, monkeypatch):
- # Set a fake editor just to make sure we have one. We aren't really
- # going to call it due to the mock
- base_app.editor = 'fooedit'
-
- # Mock out the subprocess.Popen call so we don't actually open an editor
- m = mock.MagicMock(name='Popen')
- monkeypatch.setattr("subprocess.Popen", m)
-
- # Run help command just so we have a command in history
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'history -e 1')
-
- # We have an editor, so should expect a Popen call
- m.assert_called_once()
-
-def test_history_run_all_commands(base_app):
- # make sure we refuse to run all commands as a default
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history -r')
- # this should generate an error, but we don't currently have a way to
- # capture stderr in these tests. So we assume that if we got nothing on
- # standard out, that the error occurred because if the command executed
- # then we should have a list of shortcuts in our output
- assert out == []
-
-def test_history_run_one_command(base_app):
- expected = run_cmd(base_app, 'help')
- output = run_cmd(base_app, 'history -r 1')
- assert output == expected
-
-def test_history_clear(base_app):
- # Add commands to history
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'alias')
-
- # Make sure history has items
- out = run_cmd(base_app, 'history')
- assert out
-
- # Clear the history
- run_cmd(base_app, 'history --clear')
-
- # Make sure history is empty
- out = run_cmd(base_app, 'history')
- assert out == []
-
-
def test_base_load(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
filename = os.path.join(test_dir, 'script.txt')
@@ -935,34 +731,6 @@ def test_base_py_interactive(base_app):
m.assert_called_once()
-def test_exclude_from_history(base_app, monkeypatch):
- # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
- base_app.editor = 'fooedit'
-
- # Mock out the subprocess.Popen call so we don't actually open an editor
- m = mock.MagicMock(name='Popen')
- monkeypatch.setattr("subprocess.Popen", m)
-
- # Run edit command
- run_cmd(base_app, 'edit')
-
- # Run history command
- run_cmd(base_app, 'history')
-
- # Verify that the history is empty
- out = run_cmd(base_app, 'history')
- assert out == []
-
- # Now run a command which isn't excluded from the history
- run_cmd(base_app, 'help')
-
- # And verify we have a history now ...
- out = run_cmd(base_app, 'history')
- expected = normalize("""-------------------------[1]
-help""")
- assert out == expected
-
-
def test_base_cmdloop_with_queue():
# Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
app = cmd2.Cmd()
@@ -2152,20 +1920,12 @@ def test_parseline(base_app):
assert line == statement.strip()
-def test_readline_remove_history_item(base_app):
- from cmd2.rl_utils import readline
- assert readline.get_current_history_length() == 0
- readline.add_history('this is a test')
- assert readline.get_current_history_length() == 1
- readline.remove_history_item(0)
- assert readline.get_current_history_length() == 0
-
def test_onecmd_raw_str_continue(base_app):
line = "help"
stop = base_app.onecmd(line)
out = base_app.stdout.getvalue()
assert not stop
- assert out.strip() == BASE_HELP.strip()
+ assert normalize(out) == normalize(BASE_HELP)
def test_onecmd_raw_str_quit(base_app):
line = "quit"
@@ -2175,78 +1935,6 @@ def test_onecmd_raw_str_quit(base_app):
assert out == ''
-@pytest.fixture(scope="session")
-def hist_file():
- fd, filename = tempfile.mkstemp(prefix='hist_file', suffix='.txt')
- os.close(fd)
- yield filename
- # teardown code
- try:
- os.remove(filename)
- except FileNotFoundError:
- pass
-
-def test_existing_history_file(hist_file, capsys):
- import atexit
- import readline
-
- # Create the history file before making cmd2 app
- with open(hist_file, 'w'):
- pass
-
- # Create a new cmd2 app
- app = cmd2.Cmd(persistent_history_file=hist_file)
- out, err = capsys.readouterr()
-
- # Make sure there were no errors
- assert err == ''
-
- # Unregister the call to write_history_file that cmd2 did
- atexit.unregister(readline.write_history_file)
-
- # Remove created history file
- os.remove(hist_file)
-
-
-def test_new_history_file(hist_file, capsys):
- import atexit
- import readline
-
- # Remove any existing history file
- try:
- os.remove(hist_file)
- except OSError:
- pass
-
- # Create a new cmd2 app
- app = cmd2.Cmd(persistent_history_file=hist_file)
- out, err = capsys.readouterr()
-
- # Make sure there were no errors
- assert err == ''
-
- # Unregister the call to write_history_file that cmd2 did
- atexit.unregister(readline.write_history_file)
-
- # Remove created history file
- os.remove(hist_file)
-
-def test_bad_history_file_path(capsys, request):
- # Use a directory path as the history file
- test_dir = os.path.dirname(request.module.__file__)
-
- # Create a new cmd2 app
- app = cmd2.Cmd(persistent_history_file=test_dir)
- out, err = capsys.readouterr()
-
- if sys.platform == 'win32':
- # pyreadline masks the read exception. Therefore the bad path error occurs when trying to write the file.
- assert 'readline cannot write' in err
- else:
- # GNU readline raises an exception upon trying to read the directory as a file
- assert 'readline cannot read' in err
-
-
def test_get_all_commands(base_app):
# Verify that the base app has the expected commands
commands = base_app.get_all_commands()
diff --git a/tests/test_history.py b/tests/test_history.py
new file mode 100644
index 00000000..06c57b4c
--- /dev/null
+++ b/tests/test_history.py
@@ -0,0 +1,410 @@
+# coding=utf-8
+# flake8: noqa E302
+"""
+Test history functions of cmd2
+"""
+import tempfile
+import os
+import sys
+
+import pytest
+
+# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
+try:
+ import mock
+except ImportError:
+ from unittest import mock
+
+import cmd2
+from cmd2 import clipboard
+from cmd2 import utils
+from .conftest import run_cmd, normalize, HELP_HISTORY
+
+
+def test_base_help_history(base_app):
+ out = run_cmd(base_app, 'help history')
+ assert out == normalize(HELP_HISTORY)
+
+def test_exclude_from_history(base_app, monkeypatch):
+ # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
+ base_app.editor = 'fooedit'
+
+ # Mock out the subprocess.Popen call so we don't actually open an editor
+ m = mock.MagicMock(name='Popen')
+ monkeypatch.setattr("subprocess.Popen", m)
+
+ # Run edit command
+ run_cmd(base_app, 'edit')
+
+ # Run history command
+ run_cmd(base_app, 'history')
+
+ # Verify that the history is empty
+ out = run_cmd(base_app, 'history')
+ assert out == []
+
+ # Now run a command which isn't excluded from the history
+ run_cmd(base_app, 'help')
+
+ # And verify we have a history now ...
+ out = run_cmd(base_app, 'history')
+ expected = normalize(""" 1 help""")
+ assert out == expected
+
+
+@pytest.fixture
+def hist():
+ from cmd2.parsing import Statement
+ from cmd2.cmd2 import History, HistoryItem
+ h = History([HistoryItem(Statement('', raw='first')),
+ HistoryItem(Statement('', raw='second')),
+ HistoryItem(Statement('', raw='third')),
+ HistoryItem(Statement('', raw='fourth'))])
+ return h
+
+def test_history_class_span(hist):
+ h = hist
+ assert h == ['first', 'second', 'third', 'fourth']
+ assert h.span('-2..') == ['third', 'fourth']
+ assert h.span('2..3') == ['second', 'third'] # Inclusive of end
+ assert h.span('3') == ['third']
+ assert h.span(':') == h
+ assert h.span('2..') == ['second', 'third', 'fourth']
+ assert h.span('-1') == ['fourth']
+ assert h.span('-2..-3') == ['third', 'second']
+ assert h.span('*') == h
+
+def test_history_class_get(hist):
+ h = hist
+ assert h == ['first', 'second', 'third', 'fourth']
+ assert h.get('') == h
+ assert h.get('-2') == h[:-2]
+ assert h.get('5') == []
+ assert h.get('2-3') == ['second'] # Exclusive of end
+ assert h.get('ir') == ['first', 'third'] # Normal string search for all elements containing "ir"
+ assert h.get('/i.*d/') == ['third'] # Regex string search "i", then anything, then "d"
+
+def test_base_history(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history')
+ expected = normalize("""
+ 1 help
+ 2 shortcuts
+""")
+ assert out == expected
+
+ out = run_cmd(base_app, 'history he')
+ expected = normalize("""
+ 1 help
+""")
+ assert out == expected
+
+ out = run_cmd(base_app, 'history sh')
+ expected = normalize("""
+ 2 shortcuts
+""")
+ assert out == expected
+
+def test_history_script_format(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history -s')
+ expected = normalize("""
+help
+shortcuts
+""")
+ assert out == expected
+
+def test_history_with_string_argument(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history help')
+ expected = normalize("""
+ 1 help
+ 3 help history
+""")
+ assert out == expected
+
+def test_history_expanded_with_string_argument(base_app):
+ run_cmd(base_app, 'alias create sc shortcuts')
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'help history')
+ run_cmd(base_app, 'sc')
+ out = run_cmd(base_app, 'history -v shortcuts')
+ expected = normalize("""
+ 1 alias create sc shortcuts
+ 4 sc
+ 4x shortcuts
+""")
+ assert out == expected
+
+def test_history_expanded_with_regex_argument(base_app):
+ run_cmd(base_app, 'alias create sc shortcuts')
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'help history')
+ run_cmd(base_app, 'sc')
+ out = run_cmd(base_app, 'history -v /sh.*cuts/')
+ expected = normalize("""
+ 1 alias create sc shortcuts
+ 4 sc
+ 4x shortcuts
+""")
+ assert out == expected
+
+def test_history_with_integer_argument(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history 1')
+ expected = normalize("""
+ 1 help
+""")
+ assert out == expected
+
+
+def test_history_with_integer_span(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history 1..2')
+ expected = normalize("""
+ 1 help
+ 2 shortcuts
+""")
+ assert out == expected
+
+def test_history_with_span_start(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history 2:')
+ expected = normalize("""
+ 2 shortcuts
+ 3 help history
+""")
+ assert out == expected
+
+def test_history_with_span_end(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history :2')
+ expected = normalize("""
+ 1 help
+ 2 shortcuts
+""")
+ assert out == expected
+
+def test_history_with_span_index_error(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'help history')
+ run_cmd(base_app, '!ls -hal :')
+ out = run_cmd(base_app, 'history "hal :"')
+ expected = normalize("""
+ 3 !ls -hal :
+""")
+ assert out == expected
+
+def test_history_output_file(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+
+ fd, fname = tempfile.mkstemp(prefix='', suffix='.txt')
+ os.close(fd)
+ run_cmd(base_app, 'history -o "{}"'.format(fname))
+ expected = normalize('\n'.join(['help', 'shortcuts', 'help history']))
+ with open(fname) as f:
+ content = normalize(f.read())
+ assert content == expected
+
+def test_history_edit(base_app, monkeypatch):
+ # Set a fake editor just to make sure we have one. We aren't really
+ # going to call it due to the mock
+ base_app.editor = 'fooedit'
+
+ # Mock out the Popen call so we don't actually open an editor
+ m = mock.MagicMock(name='Popen')
+ monkeypatch.setattr("subprocess.Popen", m)
+
+ # Run help command just so we have a command in history
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'history -e 1')
+
+ # We have an editor, so should expect a Popen call
+ m.assert_called_once()
+
+def test_history_run_all_commands(base_app):
+ # make sure we refuse to run all commands as a default
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history -r')
+ # this should generate an error, but we don't currently have a way to
+ # capture stderr in these tests. So we assume that if we got nothing on
+ # standard out, that the error occurred because if the command executed
+ # then we should have a list of shortcuts in our output
+ assert out == []
+
+def test_history_run_one_command(base_app):
+ expected = run_cmd(base_app, 'help')
+ output = run_cmd(base_app, 'history -r 1')
+ assert output == expected
+
+def test_history_clear(base_app):
+ # Add commands to history
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'alias')
+
+ # Make sure history has items
+ out = run_cmd(base_app, 'history')
+ assert out
+
+ # Clear the history
+ run_cmd(base_app, 'history --clear')
+
+ # Make sure history is empty
+ out = run_cmd(base_app, 'history')
+ assert out == []
+
+def test_history_verbose_with_other_options(base_app):
+ # make sure -v shows a usage error if any other options are present
+ options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x']
+ for opt in options_to_test:
+ output = run_cmd(base_app, 'history -v ' + opt)
+ assert len(output) == 3
+ assert output[1].startswith('Usage:')
+
+def test_history_verbose(base_app):
+ # validate function of -v option
+ run_cmd(base_app, 'alias create s shortcuts')
+ run_cmd(base_app, 's')
+ output = run_cmd(base_app, 'history -v')
+ assert len(output) == 3
+ # TODO test for basic formatting once we figure it out
+
+def test_history_script_with_invalid_options(base_app):
+ # make sure -s shows a usage error if -c, -r, -e, -o, or -t are present
+ options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
+ for opt in options_to_test:
+ output = run_cmd(base_app, 'history -s ' + opt)
+ assert len(output) == 3
+ assert output[1].startswith('Usage:')
+
+def test_history_script(base_app):
+ cmds = ['alias create s shortcuts', 's']
+ for cmd in cmds:
+ run_cmd(base_app, cmd)
+ output = run_cmd(base_app, 'history -s')
+ assert output == cmds
+
+def test_history_expanded_with_invalid_options(base_app):
+ # make sure -x shows a usage error if -c, -r, -e, -o, or -t are present
+ options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
+ for opt in options_to_test:
+ output = run_cmd(base_app, 'history -x ' + opt)
+ assert len(output) == 3
+ assert output[1].startswith('Usage:')
+
+def test_history_expanded(base_app):
+ # validate function of -x option
+ cmds = ['alias create s shortcuts', 's']
+ for cmd in cmds:
+ run_cmd(base_app, cmd)
+ output = run_cmd(base_app, 'history -x')
+ expected = [' 1 alias create s shortcuts', ' 2 shortcuts']
+ assert output == expected
+
+def test_history_script_expanded(base_app):
+ # validate function of -s -x options together
+ cmds = ['alias create s shortcuts', 's']
+ for cmd in cmds:
+ run_cmd(base_app, cmd)
+ output = run_cmd(base_app, 'history -sx')
+ expected = ['alias create s shortcuts', 'shortcuts']
+ assert output == expected
+
+
+#####
+#
+# readline tests
+#
+#####
+def test_readline_remove_history_item(base_app):
+ from cmd2.rl_utils import readline
+ assert readline.get_current_history_length() == 0
+ readline.add_history('this is a test')
+ assert readline.get_current_history_length() == 1
+ readline.remove_history_item(0)
+ assert readline.get_current_history_length() == 0
+
+
+@pytest.fixture(scope="session")
+def hist_file():
+ fd, filename = tempfile.mkstemp(prefix='hist_file', suffix='.txt')
+ os.close(fd)
+ yield filename
+ # teardown code
+ try:
+ os.remove(filename)
+ except FileNotFoundError:
+ pass
+
+def test_existing_history_file(hist_file, capsys):
+ import atexit
+ import readline
+
+ # Create the history file before making cmd2 app
+ with open(hist_file, 'w'):
+ pass
+
+ # Create a new cmd2 app
+ app = cmd2.Cmd(persistent_history_file=hist_file)
+ out, err = capsys.readouterr()
+
+ # Make sure there were no errors
+ assert err == ''
+
+ # Unregister the call to write_history_file that cmd2 did
+ atexit.unregister(readline.write_history_file)
+
+ # Remove created history file
+ os.remove(hist_file)
+
+def test_new_history_file(hist_file, capsys):
+ import atexit
+ import readline
+
+ # Remove any existing history file
+ try:
+ os.remove(hist_file)
+ except OSError:
+ pass
+
+ # Create a new cmd2 app
+ app = cmd2.Cmd(persistent_history_file=hist_file)
+ out, err = capsys.readouterr()
+
+ # Make sure there were no errors
+ assert err == ''
+
+ # Unregister the call to write_history_file that cmd2 did
+ atexit.unregister(readline.write_history_file)
+
+ # Remove created history file
+ os.remove(hist_file)
+
+def test_bad_history_file_path(capsys, request):
+ # Use a directory path as the history file
+ test_dir = os.path.dirname(request.module.__file__)
+
+ # Create a new cmd2 app
+ app = cmd2.Cmd(persistent_history_file=test_dir)
+ out, err = capsys.readouterr()
+
+ if sys.platform == 'win32':
+ # pyreadline masks the read exception. Therefore the bad path error occurs when trying to write the file.
+ assert 'readline cannot write' in err
+ else:
+ # GNU readline raises an exception upon trying to read the directory as a file
+ assert 'readline cannot read' in err
+
diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt
index 8c0dd007..871b71f1 100644
--- a/tests/transcripts/from_cmdloop.txt
+++ b/tests/transcripts/from_cmdloop.txt
@@ -35,18 +35,12 @@ OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
(Cmd) history
--------------------------[1]
-help
--------------------------[2]
-help say
--------------------------[3]
-say goodnight, Gracie
--------------------------[4]
-say -ps --repeat=5 goodnight, Gracie
--------------------------[5]
-set maxrepeats 5
--------------------------[6]
-say -ps --repeat=5 goodnight, Gracie
+ 1 help
+ 2 help say
+ 3 say goodnight, Gracie
+ 4 say -ps --repeat=5 goodnight, Gracie
+ 5 set maxrepeats 5
+ 6 say -ps --repeat=5 goodnight, Gracie
(Cmd) history -r 4
say -ps --repeat=5 goodnight, Gracie
OODNIGHT, GRACIEGAY