summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2/cmd2.py197
-rw-r--r--cmd2/parsing.py6
-rw-r--r--tests/test_cmd2.py87
-rw-r--r--tests/test_parsing.py346
-rw-r--r--tests/test_shlexparsing.py284
5 files changed, 358 insertions, 562 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 43973b64..41f46d5c 100755
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -52,7 +52,7 @@ import pyperclip
from .rl_utils import rl_force_redisplay, readline, rl_type, RlType
from .argparse_completer import AutoCompleter, ACArgumentParser
-from cmd2.parsing import CommandParser
+from cmd2.parsing import StatementParser
if rl_type == RlType.PYREADLINE:
@@ -348,23 +348,6 @@ def write_to_paste_buffer(txt: str) -> None:
"""
pyperclip.copy(txt)
-def replace_with_file_contents(fname: str) -> str:
- """Action to perform when successfully matching parse element definition for inputFrom parser.
-
- :param fname: filename
- :return: contents of file "fname"
- """
- try:
- # Any outer quotes are not part of the filename
- unquoted_file = strip_quotes(fname[0])
- with open(os.path.expanduser(unquoted_file)) as source_file:
- result = source_file.read()
- except IOError:
- result = '< %s' % fname[0] # wasn't a file after all
-
- # TODO: IF pyparsing input parser logic gets fixed to support empty file, add support to get from paste buffer
- return result
-
class EmbeddedConsoleExit(SystemExit):
"""Custom exception class for use with the py command."""
@@ -646,7 +629,7 @@ class Cmd(cmd.Cmd):
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
"""
- # Attributes used to configure the ParserManager (all are not dynamically settable at runtime)
+ # Attributes used to configure the StatementParser, best not to change these at runtime
blankLinesAllowed = False
multilineCommands = []
redirector = '>' # for sending output to file
@@ -739,7 +722,7 @@ class Cmd(cmd.Cmd):
self.history = History()
self.pystate = {}
self.keywords = self.reserved_words + [fname[3:] for fname in dir(self) if fname.startswith('do_')]
- self.command_parser = CommandParser(
+ self.statement_parser = StatementParser(
quotes=QUOTES,
allow_redirection=self.allow_redirection,
redirection_chars=REDIRECTION_CHARS,
@@ -2121,10 +2104,7 @@ class Cmd(cmd.Cmd):
pipe runs out. We can't refactor it because we need to retain
backwards compatibility with the standard library version of cmd.
"""
- #if not line or (not pyparsing.Or(self.commentGrammars).setParseAction(lambda x: '').transformString(line)):
- # raise EmptyStatement()
- # statement = self.parser_manager.parsed(line) # deleteme
- statement = self.command_parser.parseString(line)
+ statement = self.statement_parser.parse(line)
while statement.multilineCommand and not statement.terminator:
if not self.quit_on_sigint:
try:
@@ -2139,7 +2119,7 @@ class Cmd(cmd.Cmd):
line = '{}\n{}'.format(statement.raw, newline)
except KeyboardInterrupt:
self.poutput('^C')
- statement = self.command_parser.parseString('')
+ statement = self.statement_parser.parse('')
break
else:
newline = self.pseudo_raw_input(self.continuation_prompt)
@@ -2151,7 +2131,7 @@ class Cmd(cmd.Cmd):
newline = '\n'
self.poutput(newline)
line = '{}\n{}'.format(statement.raw, newline)
- statement = self.command_parser.parseString(line)
+ statement = self.statement_parser.parse(line)
if not statement.command:
raise EmptyStatement()
@@ -2160,7 +2140,7 @@ class Cmd(cmd.Cmd):
def _redirect_output(self, statement):
"""Handles output redirection for >, >>, and |.
- :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance
+ :param statement: Statement - a parsed statement from the user
"""
if statement.pipeTo:
self.kept_state = Statekeeper(self, ('stdout',))
@@ -2208,7 +2188,7 @@ class Cmd(cmd.Cmd):
def _restore_output(self, statement):
"""Handles restoring state after output redirection as well as the actual pipe operation if present.
- :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance
+ :param statement: Statement object which contains the parsed input from the user
"""
# If we have redirected output to a file or the clipboard or piped it to a shell command, then restore state
if self.kept_state is not None:
@@ -3312,167 +3292,6 @@ Script should contain one command per line, just like command would be typed in
self.postloop()
-# noinspection PyPep8Naming
-class ParserManager:
- """
- Class which encapsulates all of the pyparsing parser functionality for cmd2 in a single location.
- """
- def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars, commentInProgress,
- blankLinesAllowed, prefixParser, preparse, postparse, aliases, shortcuts):
- """Creates and uses parsers for user input according to app's parameters."""
-
- self.commentGrammars = commentGrammars
- self.preparse = preparse
- self.postparse = postparse
- self.aliases = aliases
- self.shortcuts = shortcuts
-
- self.main_parser = self._build_main_parser(redirector=redirector, terminators=terminators,
- multilineCommands=multilineCommands, legalChars=legalChars,
- commentInProgress=commentInProgress,
- blankLinesAllowed=blankLinesAllowed, prefixParser=prefixParser)
- self.input_source_parser = self._build_input_source_parser(legalChars=legalChars,
- commentInProgress=commentInProgress)
-
- def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars, commentInProgress,
- blankLinesAllowed, prefixParser):
- """Builds a PyParsing parser for interpreting user commands."""
-
- # Build several parsing components that are eventually compiled into overall parser
- output_destination_parser = (pyparsing.Literal(redirector * 2) |
- (pyparsing.WordStart() + redirector) |
- pyparsing.Regex('[^=]' + redirector))('output')
-
- terminator_parser = pyparsing.Or(
- [(hasattr(t, 'parseString') and t) or pyparsing.Literal(t) for t in terminators])('terminator')
- string_end = pyparsing.stringEnd ^ '\nEOF'
- multilineCommand = pyparsing.Or(
- [pyparsing.Keyword(c, caseless=False) for c in multilineCommands])('multilineCommand')
- oneline_command = (~multilineCommand + pyparsing.Word(legalChars))('command')
- pipe = pyparsing.Keyword('|', identChars='|')
- do_not_parse = self.commentGrammars | commentInProgress | pyparsing.quotedString
- after_elements = \
- pyparsing.Optional(pipe + pyparsing.SkipTo(output_destination_parser ^ string_end,
- ignore=do_not_parse)('pipeTo')) + \
- pyparsing.Optional(output_destination_parser +
- pyparsing.SkipTo(string_end, ignore=do_not_parse).
- setParseAction(lambda x: strip_quotes(x[0].strip()))('outputTo'))
-
- multilineCommand.setParseAction(lambda x: x[0])
- oneline_command.setParseAction(lambda x: x[0])
-
- if blankLinesAllowed:
- blankLineTerminationParser = pyparsing.NoMatch
- else:
- blankLineTerminator = (pyparsing.lineEnd + pyparsing.lineEnd)('terminator')
- blankLineTerminator.setResultsName('terminator')
- blankLineTerminationParser = ((multilineCommand ^ oneline_command) +
- pyparsing.SkipTo(blankLineTerminator, ignore=do_not_parse).setParseAction(
- lambda x: x[0].strip())('args') + blankLineTerminator)('statement')
-
- multilineParser = (((multilineCommand ^ oneline_command) +
- pyparsing.SkipTo(terminator_parser,
- ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('args') +
- terminator_parser)('statement') +
- pyparsing.SkipTo(output_destination_parser ^ pipe ^ string_end,
- ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('suffix') +
- after_elements)
- multilineParser.ignore(commentInProgress)
-
- singleLineParser = ((oneline_command +
- pyparsing.SkipTo(terminator_parser ^ string_end ^ pipe ^ output_destination_parser,
- ignore=do_not_parse).setParseAction(
- lambda x: x[0].strip())('args'))('statement') +
- pyparsing.Optional(terminator_parser) + after_elements)
-
- blankLineTerminationParser = blankLineTerminationParser.setResultsName('statement')
-
- parser = prefixParser + (
- string_end |
- multilineParser |
- singleLineParser |
- blankLineTerminationParser |
- multilineCommand + pyparsing.SkipTo(string_end, ignore=do_not_parse)
- )
- parser.ignore(self.commentGrammars)
- return parser
-
- @staticmethod
- def _build_input_source_parser(legalChars, commentInProgress):
- """Builds a PyParsing parser for alternate user input sources (from file, pipe, etc.)"""
-
- input_mark = pyparsing.Literal('<')
- input_mark.setParseAction(lambda x: '')
-
- # Also allow spaces, slashes, and quotes
- file_name = pyparsing.Word(legalChars + ' /\\"\'')
-
- input_from = file_name('inputFrom')
- input_from.setParseAction(replace_with_file_contents)
- # a not-entirely-satisfactory way of distinguishing < as in "import from" from <
- # as in "lesser than"
- inputParser = input_mark + pyparsing.Optional(input_from) + pyparsing.Optional('>') + \
- pyparsing.Optional(file_name) + (pyparsing.stringEnd | '|')
- inputParser.ignore(commentInProgress)
- return inputParser
-
- def parsed(self, raw):
- """ This function is where the actual parsing of each line occurs.
-
- :param raw: str - the line of text as it was entered
- :return: ParsedString - custom subclass of str with extra attributes
- """
- if isinstance(raw, ParsedString):
- p = raw
- else:
- # preparse is an overridable hook; default makes no changes
- s = self.preparse(raw)
- s = self.input_source_parser.transformString(s.lstrip())
- s = self.commentGrammars.transformString(s)
-
- # Make a copy of aliases so we can edit it
- tmp_aliases = list(self.aliases.keys())
- keep_expanding = len(tmp_aliases) > 0
-
- # Expand aliases
- while keep_expanding:
- for cur_alias in tmp_aliases:
- keep_expanding = False
-
- if s == cur_alias or s.startswith(cur_alias + ' '):
- s = s.replace(cur_alias, self.aliases[cur_alias], 1)
-
- # Do not expand the same alias more than once
- tmp_aliases.remove(cur_alias)
- keep_expanding = len(tmp_aliases) > 0
- break
-
- # Expand command shortcut to its full command name
- for (shortcut, expansion) in self.shortcuts:
- if s.startswith(shortcut):
- # If the next character after the shortcut isn't a space, then insert one
- shortcut_len = len(shortcut)
- if len(s) == shortcut_len or s[shortcut_len] != ' ':
- expansion += ' '
-
- # Expand the shortcut
- s = s.replace(shortcut, expansion, 1)
- break
-
- try:
- result = self.main_parser.parseString(s)
- except pyparsing.ParseException:
- # If we have a parsing failure, treat it is an empty command and move to next prompt
- result = self.main_parser.parseString('')
- result['raw'] = raw
- result['command'] = result.multilineCommand or result.command
- result = self.postparse(result)
- p = ParsedString(result.args)
- p.parsed = result
- p.parser = self.parsed
- return p
-
-
class HistoryItem(str):
"""Class used to represent an item in the History list.
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index ffeb8bbe..9204305b 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -1,6 +1,6 @@
#
# -*- coding: utf-8 -*-
-"""Command parsing classes for cmd2"""
+"""Statement parsing classes for cmd2"""
import re
import shlex
@@ -32,7 +32,7 @@ class Statement(str):
self.output = None
self.outputTo = None
-class CommandParser():
+class StatementParser():
"""Parse raw text into command components.
Shortcuts is a list of tuples with each tuple containing the shortcut and the expansion.
@@ -55,7 +55,7 @@ class CommandParser():
self.aliases = aliases
self.shortcuts = shortcuts
- def parseString(self, rawinput):
+ def parse(self, rawinput):
# strip C-style comments
# shlex will handle the python/shell style comments for us
def replacer(match):
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 27168af1..0a4dfbc6 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -114,6 +114,49 @@ def test_base_show_readonly(base_app):
assert out == expected
+def test_cast():
+ cast = cmd2.cmd2.cast
+
+ # Boolean
+ assert cast(True, True) == True
+ assert cast(True, False) == False
+ assert cast(True, 0) == False
+ assert cast(True, 1) == True
+ assert cast(True, 'on') == True
+ assert cast(True, 'off') == False
+ assert cast(True, 'ON') == True
+ assert cast(True, 'OFF') == False
+ assert cast(True, 'y') == True
+ assert cast(True, 'n') == False
+ assert cast(True, 't') == True
+ assert cast(True, 'f') == False
+
+ # Non-boolean same type
+ assert cast(1, 5) == 5
+ assert cast(3.4, 2.7) == 2.7
+ assert cast('foo', 'bar') == 'bar'
+ assert cast([1,2], [3,4]) == [3,4]
+
+def test_cast_problems(capsys):
+ cast = cmd2.cmd2.cast
+
+ expected = 'Problem setting parameter (now {}) to {}; incorrect type?\n'
+
+ # Boolean current, with new value not convertible to bool
+ current = True
+ new = [True, True]
+ assert cast(current, new) == current
+ out, err = capsys.readouterr()
+ assert out == expected.format(current, new)
+
+ # Non-boolean current, with new value not convertible to current type
+ current = 1
+ new = 'octopus'
+ assert cast(current, new) == current
+ out, err = capsys.readouterr()
+ assert out == expected.format(current, new)
+
+
def test_base_set(base_app):
out = run_cmd(base_app, 'set quiet True')
expected = normalize("""
@@ -217,6 +260,34 @@ def test_base_error(base_app):
assert out == ["*** Unknown syntax: meow"]
+@pytest.fixture
+def hist():
+ from cmd2.cmd2 import HistoryItem
+ h = cmd2.cmd2.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')
@@ -604,18 +675,6 @@ def test_allow_redirection(base_app):
# Verify that no file got created
assert not os.path.exists(filename)
-
-def test_input_redirection(base_app, request):
- test_dir = os.path.dirname(request.module.__file__)
- filename = os.path.join(test_dir, 'redirect.txt')
-
- # NOTE: File 'redirect.txt" contains 1 word "history"
-
- # Verify that redirecting input from a file works
- out = run_cmd(base_app, 'help < {}'.format(filename))
- assert out == normalize(HELP_HISTORY)
-
-
def test_pipe_to_shell(base_app, capsys):
if sys.platform == "win32":
# Windows
@@ -965,7 +1024,7 @@ def test_default_to_shell_good(capsys):
line = 'dir'
else:
line = 'ls'
- statement = app.command_parser.parseString(line)
+ statement = app.statement_parser.parse(line)
retval = app.default(statement)
assert not retval
out, err = capsys.readouterr()
@@ -975,7 +1034,7 @@ def test_default_to_shell_failure(capsys):
app = cmd2.Cmd()
app.default_to_shell = True
line = 'ls does_not_exist.xyz'
- statement = app.command_parser.parseString(line)
+ statement = app.statement_parser.parse(line)
retval = app.default(statement)
assert not retval
out, err = capsys.readouterr()
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 5825e735..6fa7d629 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -1,90 +1,284 @@
# coding=utf-8
"""
-Unit/functional testing for helper functions/classes in the cmd2.py module.
-
-These are primarily tests related to parsing. Moreover, they are mostly a port of the old doctest tests which were
-problematic because they worked properly for some versions of pyparsing but not for others.
+Test the parsing logic in parsing.py
Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com>
Released under MIT license, see LICENSE file
"""
-import sys
-
import cmd2
+from cmd2.parsing import StatementParser
+
import pytest
@pytest.fixture
-def hist():
- from cmd2.cmd2 import HistoryItem
- h = cmd2.cmd2.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_cast():
- cast = cmd2.cmd2.cast
-
- # Boolean
- assert cast(True, True) == True
- assert cast(True, False) == False
- assert cast(True, 0) == False
- assert cast(True, 1) == True
- assert cast(True, 'on') == True
- assert cast(True, 'off') == False
- assert cast(True, 'ON') == True
- assert cast(True, 'OFF') == False
- assert cast(True, 'y') == True
- assert cast(True, 'n') == False
- assert cast(True, 't') == True
- assert cast(True, 'f') == False
-
- # Non-boolean same type
- assert cast(1, 5) == 5
- assert cast(3.4, 2.7) == 2.7
- assert cast('foo', 'bar') == 'bar'
- assert cast([1,2], [3,4]) == [3,4]
-
-
-def test_cast_problems(capsys):
- cast = cmd2.cmd2.cast
-
- expected = 'Problem setting parameter (now {}) to {}; incorrect type?\n'
-
- # Boolean current, with new value not convertible to bool
- current = True
- new = [True, True]
- assert cast(current, new) == current
- out, err = capsys.readouterr()
- assert out == expected.format(current, new)
-
- # Non-boolean current, with new value not convertible to current type
- current = 1
- new = 'octopus'
- assert cast(current, new) == current
- out, err = capsys.readouterr()
- assert out == expected.format(current, new)
+def parser():
+ parser = StatementParser(
+ quotes=['"', "'"],
+ allow_redirection=True,
+ redirection_chars=['|', '<', '>'],
+ terminators = [';'],
+ multilineCommands = ['multiline'],
+ aliases = {'helpalias': 'help', '42': 'theanswer'},
+ shortcuts = [('?', 'help'), ('!', 'shell')]
+ )
+ return parser
+
+def test_parse_empty_string(parser):
+ statement = parser.parse('')
+ assert not statement.command
+
+@pytest.mark.parametrize('tokens,command,args', [
+ ( [], None, ''),
+ ( ['command'], 'command', '' ),
+ ( ['command', 'arg1', 'arg2'], 'command', 'arg1 arg2')
+])
+def test_command_and_args(parser, tokens, command, args):
+ (parsed_command, parsed_args) = parser._command_and_args(tokens)
+ assert command == parsed_command
+ assert args == parsed_args
+
+@pytest.mark.parametrize('line', [
+ 'plainword',
+ '"one word"',
+ "'one word'",
+])
+def test_single_word(parser, line):
+ statement = parser.parse(line)
+ assert statement.command == line
+
+def test_word_plus_terminator(parser):
+ line = 'termbare;'
+ statement = parser.parse(line)
+ assert statement.command == 'termbare'
+ assert statement.terminator == ';'
+
+def test_suffix_after_terminator(parser):
+ line = 'termbare; suffx'
+ statement = parser.parse(line)
+ assert statement.command == 'termbare'
+ assert statement.terminator == ';'
+ assert statement.suffix == 'suffx'
+
+def test_command_with_args(parser):
+ line = 'command with args'
+ statement = parser.parse(line)
+ assert statement.command == 'command'
+ assert statement.args == 'with args'
+ assert not statement.pipeTo
+
+def test_parse_command_with_args_terminator_and_suffix(parser):
+ line = 'command with args and terminator; and suffix'
+ statement = parser.parse(line)
+ assert statement.command == 'command'
+ assert statement.args == "with args and terminator"
+ assert statement.terminator == ';'
+ assert statement.suffix == 'and suffix'
+
+def test_hashcomment(parser):
+ statement = parser.parse('hi # this is all a comment')
+ assert statement.command == 'hi'
+ assert not statement.args
+ assert not statement.pipeTo
+
+def test_c_comment(parser):
+ statement = parser.parse('hi /* this is | all a comment */')
+ assert statement.command == 'hi'
+ assert not statement.args
+ assert not statement.pipeTo
+
+def test_c_comment_empty(parser):
+ statement = parser.parse('/* this is | all a comment */')
+ assert not statement.command
+ assert not statement.args
+ assert not statement.pipeTo
+
+def test_parse_what_if_quoted_strings_seem_to_start_comments(parser):
+ statement = parser.parse('what if "quoted strings /* seem to " start comments?')
+ assert statement.command == 'what'
+ assert statement.args == 'if "quoted strings /* seem to " start comments?'
+ assert not statement.pipeTo
+
+def test_simple_piped(parser):
+ statement = parser.parse('simple | piped')
+ assert statement.command == 'simple'
+ assert not statement.args
+ assert statement.pipeTo == 'piped'
+
+def test_double_pipe_is_not_a_pipe(parser):
+ line = 'double-pipe || is not a pipe'
+ statement = parser.parse(line)
+ assert statement.command == 'double-pipe'
+ assert statement.args == '|| is not a pipe'
+ assert not statement.pipeTo
+
+def test_complex_pipe(parser):
+ line = 'command with args, terminator;sufx | piped'
+ statement = parser.parse(line)
+ assert statement.command == 'command'
+ assert statement.args == "with args, terminator"
+ assert statement.terminator == ';'
+ assert statement.suffix == 'sufx'
+ assert statement.pipeTo == 'piped'
+
+def test_output_redirect(parser):
+ line = 'output into > afile.txt'
+ statement = parser.parse(line)
+ assert statement.command == 'output'
+ assert statement.args == 'into'
+ assert statement.output == '>'
+ assert statement.outputTo == 'afile.txt'
+
+def test_output_redirect_with_dash_in_path(parser):
+ line = 'output into > python-cmd2/afile.txt'
+ statement = parser.parse(line)
+ assert statement.command == 'output'
+ assert statement.args == 'into'
+ assert statement.output == '>'
+ assert statement.outputTo == 'python-cmd2/afile.txt'
+
+def test_output_redirect_append(parser):
+ line = 'output appended to >> /tmp/afile.txt'
+ statement = parser.parse(line)
+ assert statement.command == 'output'
+ assert statement.args == 'appended to'
+ assert statement.output == '>>'
+ assert statement.outputTo == '/tmp/afile.txt'
+
+def test_parse_input_redirect(parser):
+ line = '< afile.txt'
+ statement = parser.parse(line)
+ assert statement.inputFrom == 'afile.txt'
+
+def test_parse_input_redirect_after_command(parser):
+ line = 'help < afile.txt'
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert statement.args == ''
+ assert statement.inputFrom == 'afile.txt'
+
+def test_parse_input_redirect_with_dash_in_path(parser):
+ line = '< python-cmd2/afile.txt'
+ statement = parser.parse(line)
+ assert statement.inputFrom == 'python-cmd2/afile.txt'
+
+def test_pipe_and_redirect(parser):
+ line = 'output into;sufx | pipethrume plz > afile.txt'
+ statement = parser.parse(line)
+ assert statement.command == 'output'
+ assert statement.args == 'into'
+ assert statement.terminator == ';'
+ assert statement.suffix == 'sufx'
+ assert statement.pipeTo == 'pipethrume plz'
+ assert statement.output == '>'
+ assert statement.outputTo == 'afile.txt'
+
+def test_parse_output_to_paste_buffer(parser):
+ line = 'output to paste buffer >> '
+ statement = parser.parse(line)
+ assert statement.command == 'output'
+ assert statement.args == 'to paste buffer'
+ assert statement.output == '>>'
+
+def test_has_redirect_inside_terminator(parser):
+ """The terminator designates the end of the commmand/arguments portion. If a redirector
+ occurs before a terminator, then it will be treated as part of the arguments and not as a redirector."""
+ line = 'has > inside;'
+ statement = parser.parse(line)
+ assert statement.command == 'has'
+ assert statement.args == '> inside'
+ assert statement.terminator == ';'
+
+def test_parse_unfinished_multiliine_command(parser):
+ line = 'multiline has > inside an unfinished command'
+ statement = parser.parse(line)
+ assert statement.multilineCommand == 'multiline'
+ assert not statement.args
+ assert not statement.terminator
+
+
+def test_parse_multiline_command_ignores_redirectors_within_it(parser):
+ line = 'multiline has > inside;'
+ statement = parser.parse(line)
+ assert statement.multilineCommand == 'multiline'
+ assert statement.args == 'has > inside'
+ assert statement.terminator == ';'
+
+# def test_parse_multiline_with_incomplete_comment(parser):
+# """A terminator within a comment will be ignored and won't terminate a multiline command.
+# Un-closed comments effectively comment out everything after the start."""
+# line = 'multiline command /* with comment in progress;'
+# statement = parser.parse(line)
+# assert statement.multilineCommand == 'multiline'
+# assert statement.args == 'command'
+# assert not statement.terminator
+
+def test_parse_multiline_with_complete_comment(parser):
+ line = 'multiline command /* with comment complete */ is done;'
+ statement = parser.parse(line)
+ assert statement.multilineCommand == 'multiline'
+ assert statement.args == 'command is done'
+ assert statement.terminator == ';'
+
+def test_parse_multiline_termninated_by_empty_line(parser):
+ line = 'multiline command ends\n\n'
+ statement = parser.parse(line)
+ assert statement.multilineCommand == 'multiline'
+ assert statement.args == 'command ends'
+ assert statement.terminator == '\n'
+
+def test_parse_multiline_ignores_terminators_in_comments(parser):
+ line = 'multiline command "with term; ends" now\n\n'
+ statement = parser.parse(line)
+ assert statement.multilineCommand == 'multiline'
+ assert statement.args == 'command "with term; ends" now'
+ assert statement.terminator == '\n'
+
+def test_parse_command_with_unicode_args(parser):
+ line = 'drink café'
+ statement = parser.parse(line)
+ assert statement.command == 'drink'
+ assert statement.args == 'café'
+
+def test_parse_unicode_command(parser):
+ line = 'café au lait'
+ statement = parser.parse(line)
+ assert statement.command == 'café'
+ assert statement.args == 'au lait'
+
+def test_parse_redirect_to_unicode_filename(parser):
+ line = 'dir home > café'
+ statement = parser.parse(line)
+ assert statement.command == 'dir'
+ assert statement.args == 'home'
+ assert statement.output == '>'
+ assert statement.outputTo == 'café'
+
+def test_parse_input_redirect_from_unicode_filename(parser):
+ line = '< café'
+ statement = parser.parse(line)
+ assert statement.inputFrom == 'café'
+
+def test_empty_statement_raises_exception():
+ app = cmd2.Cmd()
+ with pytest.raises(cmd2.cmd2.EmptyStatement):
+ app._complete_statement('')
+
+ with pytest.raises(cmd2.cmd2.EmptyStatement):
+ app._complete_statement(' ')
+
+@pytest.mark.parametrize('line,command,args', [
+ ('helpalias', 'help', ''),
+ ('helpalias mycommand', 'help', 'mycommand'),
+ ('42', 'theanswer', ''),
+ ('42 arg1 arg2', 'theanswer', 'arg1 arg2'),
+ ('!ls', 'shell', 'ls'),
+ ('!ls -al /tmp', 'shell', 'ls -al /tmp'),
+])
+def test_alias_and_shortcut_expansion(parser, line, command, args):
+ statement = parser.parse(line)
+ assert statement.command == command
+ assert statement.args == args
def test_empty_statement_raises_exception():
app = cmd2.Cmd()
diff --git a/tests/test_shlexparsing.py b/tests/test_shlexparsing.py
index 9c2c8f9b..2ff79641 100644
--- a/tests/test_shlexparsing.py
+++ b/tests/test_shlexparsing.py
@@ -3,13 +3,13 @@
Unit/functional testing for ply based parsing in cmd2
Todo List
-- implement input redirection
- case sensitive flag
- checkout Cmd2.parseline() function which parses and expands shortcuts and such
this code should probably be included in CommandParser
- get rid of legalChars
- move remaining tests in test_parsing.py to test_cmd2.py
- rename test_shlexparsing.py to test_parsing.py
+- look at parsed() - expands aliases and shortcuts, see if it can be refactored
Notes:
@@ -18,287 +18,11 @@ Notes:
- Python/Shell style -> # comment
- we now ignore self.identchars, which breaks backwards compatibility with the cmd in the standard library
-Functions in cmd2.py to be modified:
-- parsed() - expands aliases and shortcuts
Changelog Items:
- if self.default_to_shell is true, then redirection and piping is now properly passed to the shell, previously it was truncated
-- object passed to do_* methods has changed. It no longer is the pyparsing object, it's a new Statement object. A side effect of this is that we now have a clean interface between the parsing logic and the rest of cmd2. If we need to change the parser in the future, we can do it without breaking anything.
+- object passed to do_* methods has changed. It no longer is the pyparsing object, it's a new Statement object. A side effect of this is that we now have a clean interface between the parsing logic and the rest of cmd2. If we need to change the parser in the future, we can do it without breaking anything. The parser is now self.statement_parser instead of self.command_parser.
+- input redirection no longer supported. Use the load command instead.
+- submenus now call all hooks, it used to just call precmd and postcmd
-Bugs fixed:
-- submenus now all all hooks, it used to just call precmd and postcmd
"""
-
-import cmd2
-from cmd2.parsing import CommandParser
-
-import pytest
-
-@pytest.fixture
-def parser():
- parser = CommandParser(
- quotes=['"', "'"],
- allow_redirection=True,
- redirection_chars=['|', '<', '>'],
- terminators = [';'],
- multilineCommands = ['multiline'],
- aliases = {'helpalias': 'help', '42': 'theanswer'},
- shortcuts = [('?', 'help'), ('!', 'shell')]
- )
- return parser
-
-def test_parse_empty_string(parser):
- results = parser.parseString('')
- assert not results.command
-
-@pytest.mark.parametrize('tokens,command,args', [
- ( [], None, ''),
- ( ['command'], 'command', '' ),
- ( ['command', 'arg1', 'arg2'], 'command', 'arg1 arg2')
-])
-def test_command_and_args(parser, tokens, command, args):
- (parsed_command, parsed_args) = parser._command_and_args(tokens)
- assert command == parsed_command
- assert args == parsed_args
-
-@pytest.mark.parametrize('line', [
- 'plainword',
- '"one word"',
- "'one word'",
-])
-def test_single_word(parser, line):
- results = parser.parseString(line)
- assert results.command == line
-
-def test_word_plus_terminator(parser):
- line = 'termbare;'
- results = parser.parseString(line)
- assert results.command == 'termbare'
- assert results.terminator == ';'
-
-def test_suffix_after_terminator(parser):
- line = 'termbare; suffx'
- results = parser.parseString(line)
- assert results.command == 'termbare'
- assert results.terminator == ';'
- assert results.suffix == 'suffx'
-
-def test_command_with_args(parser):
- line = 'command with args'
- results = parser.parseString(line)
- assert results.command == 'command'
- assert results.args == 'with args'
- assert not results.pipeTo
-
-def test_parse_command_with_args_terminator_and_suffix(parser):
- line = 'command with args and terminator; and suffix'
- results = parser.parseString(line)
- assert results.command == 'command'
- assert results.args == "with args and terminator"
- assert results.terminator == ';'
- assert results.suffix == 'and suffix'
-
-def test_hashcomment(parser):
- results = parser.parseString('hi # this is all a comment')
- assert results.command == 'hi'
- assert not results.args
- assert not results.pipeTo
-
-def test_c_comment(parser):
- results = parser.parseString('hi /* this is | all a comment */')
- assert results.command == 'hi'
- assert not results.args
- assert not results.pipeTo
-
-def test_c_comment_empty(parser):
- results = parser.parseString('/* this is | all a comment */')
- assert not results.command
- assert not results.args
- assert not results.pipeTo
-
-def test_parse_what_if_quoted_strings_seem_to_start_comments(parser):
- results = parser.parseString('what if "quoted strings /* seem to " start comments?')
- assert results.command == 'what'
- assert results.args == 'if "quoted strings /* seem to " start comments?'
- assert not results.pipeTo
-
-def test_simple_piped(parser):
- results = parser.parseString('simple | piped')
- assert results.command == 'simple'
- assert not results.args
- assert results.pipeTo == 'piped'
-
-def test_double_pipe_is_not_a_pipe(parser):
- line = 'double-pipe || is not a pipe'
- results = parser.parseString(line)
- assert results.command == 'double-pipe'
- assert results.args == '|| is not a pipe'
- assert not results.pipeTo
-
-def test_complex_pipe(parser):
- line = 'command with args, terminator;sufx | piped'
- results = parser.parseString(line)
- assert results.command == 'command'
- assert results.args == "with args, terminator"
- assert results.terminator == ';'
- assert results.suffix == 'sufx'
- assert results.pipeTo == 'piped'
-
-def test_output_redirect(parser):
- line = 'output into > afile.txt'
- results = parser.parseString(line)
- assert results.command == 'output'
- assert results.args == 'into'
- assert results.output == '>'
- assert results.outputTo == 'afile.txt'
-
-def test_output_redirect_with_dash_in_path(parser):
- line = 'output into > python-cmd2/afile.txt'
- results = parser.parseString(line)
- assert results.command == 'output'
- assert results.args == 'into'
- assert results.output == '>'
- assert results.outputTo == 'python-cmd2/afile.txt'
-
-def test_output_redirect_append(parser):
- line = 'output appended to >> /tmp/afile.txt'
- results = parser.parseString(line)
- assert results.command == 'output'
- assert results.args == 'appended to'
- assert results.output == '>>'
- assert results.outputTo == '/tmp/afile.txt'
-
-def test_parse_input_redirect(parser):
- line = '< afile.txt'
- results = parser.parseString(line)
- assert results.inputFrom == 'afile.txt'
-
-def test_parse_input_redirect_after_command(parser):
- line = 'help < afile.txt'
- results = parser.parseString(line)
- assert results.command == 'help'
- assert results.args == ''
- assert results.inputFrom == 'afile.txt'
-
-def test_parse_input_redirect_with_dash_in_path(parser):
- line = '< python-cmd2/afile.txt'
- results = parser.parseString(line)
- assert results.inputFrom == 'python-cmd2/afile.txt'
-
-def test_pipe_and_redirect(parser):
- line = 'output into;sufx | pipethrume plz > afile.txt'
- results = parser.parseString(line)
- assert results.command == 'output'
- assert results.args == 'into'
- assert results.terminator == ';'
- assert results.suffix == 'sufx'
- assert results.pipeTo == 'pipethrume plz'
- assert results.output == '>'
- assert results.outputTo == 'afile.txt'
-
-def test_parse_output_to_paste_buffer(parser):
- line = 'output to paste buffer >> '
- results = parser.parseString(line)
- assert results.command == 'output'
- assert results.args == 'to paste buffer'
- assert results.output == '>>'
-
-def test_has_redirect_inside_terminator(parser):
- """The terminator designates the end of the commmand/arguments portion. If a redirector
- occurs before a terminator, then it will be treated as part of the arguments and not as a redirector."""
- line = 'has > inside;'
- results = parser.parseString(line)
- assert results.command == 'has'
- assert results.args == '> inside'
- assert results.terminator == ';'
-
-def test_parse_unfinished_multiliine_command(parser):
- line = 'multiline has > inside an unfinished command'
- statement = parser.parseString(line)
- assert statement.multilineCommand == 'multiline'
- assert not statement.args
- assert not statement.terminator
-
-
-def test_parse_multiline_command_ignores_redirectors_within_it(parser):
- line = 'multiline has > inside;'
- results = parser.parseString(line)
- assert results.multilineCommand == 'multiline'
- assert results.args == 'has > inside'
- assert results.terminator == ';'
-
-# def test_parse_multiline_with_incomplete_comment(parser):
-# """A terminator within a comment will be ignored and won't terminate a multiline command.
-# Un-closed comments effectively comment out everything after the start."""
-# line = 'multiline command /* with comment in progress;'
-# statement = parser.parseString(line)
-# assert statement.multilineCommand == 'multiline'
-# assert statement.args == 'command'
-# assert not statement.terminator
-
-def test_parse_multiline_with_complete_comment(parser):
- line = 'multiline command /* with comment complete */ is done;'
- results = parser.parseString(line)
- assert results.multilineCommand == 'multiline'
- assert results.args == 'command is done'
- assert results.terminator == ';'
-
-def test_parse_multiline_termninated_by_empty_line(parser):
- line = 'multiline command ends\n\n'
- results = parser.parseString(line)
- assert results.multilineCommand == 'multiline'
- assert results.args == 'command ends'
- assert results.terminator == '\n'
-
-def test_parse_multiline_ignores_terminators_in_comments(parser):
- line = 'multiline command "with term; ends" now\n\n'
- results = parser.parseString(line)
- assert results.multilineCommand == 'multiline'
- assert results.args == 'command "with term; ends" now'
- assert results.terminator == '\n'
-
-def test_parse_command_with_unicode_args(parser):
- line = 'drink café'
- results = parser.parseString(line)
- assert results.command == 'drink'
- assert results.args == 'café'
-
-def test_parse_unicode_command(parser):
- line = 'café au lait'
- results = parser.parseString(line)
- assert results.command == 'café'
- assert results.args == 'au lait'
-
-def test_parse_redirect_to_unicode_filename(parser):
- line = 'dir home > café'
- results = parser.parseString(line)
- assert results.command == 'dir'
- assert results.args == 'home'
- assert results.output == '>'
- assert results.outputTo == 'café'
-
-def test_parse_input_redirect_from_unicode_filename(parser):
- line = '< café'
- results = parser.parseString(line)
- assert results.inputFrom == 'café'
-
-def test_empty_statement_raises_exception():
- app = cmd2.Cmd()
- with pytest.raises(cmd2.cmd2.EmptyStatement):
- app._complete_statement('')
-
- with pytest.raises(cmd2.cmd2.EmptyStatement):
- app._complete_statement(' ')
-
-@pytest.mark.parametrize('line,command,args', [
- ('helpalias', 'help', ''),
- ('helpalias mycommand', 'help', 'mycommand'),
- ('42', 'theanswer', ''),
- ('42 arg1 arg2', 'theanswer', 'arg1 arg2'),
- ('!ls', 'shell', 'ls'),
- ('!ls -al /tmp', 'shell', 'ls -al /tmp'),
-])
-def test_alias_and_shortcut_expansion(parser, line, command, args):
- statement = parser.parseString(line)
- assert statement.command == command
- assert statement.args == args