diff options
-rw-r--r-- | cmd2/cmd2.py | 2 | ||||
-rw-r--r-- | cmd2/parsing.py | 135 | ||||
-rw-r--r-- | tests/test_parsing.py | 57 |
3 files changed, 102 insertions, 92 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 58138c33..a54d813c 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1720,7 +1720,7 @@ class Cmd(cmd.Cmd): :return: tuple containing (command, args, line) """ statement = self.statement_parser.parse_command_only(line) - return statement.command, statement, statement.command_and_args + return statement.command, statement.args, statement.command_and_args def onecmd_plus_hooks(self, line: str) -> bool: """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks. diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 2a4ae56f..3737b736 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -29,46 +29,41 @@ class Statement(str): :var raw: string containing exactly what we input by the user :type raw: str :var command: the command, i.e. the first whitespace delimited word - :type command: str or None + :type command: str :var multiline_command: if the command is a multiline command, the name of the - command, otherwise None - :type command: str or None + command, otherwise empty + :type command: str :var arg_list: list of arguments to the command, not including any output redirection or terminators. quoted arguments remain quoted. :type arg_list: list - :var: argv: a list of arguments a la sys.argv. Quotes, if any, are removed - from the elements of the list, and aliases and shortcuts - are expanded - :type argv: list :var terminator: the character which terminated the multiline command, if there was one - :type terminator: str or None + :type terminator: str :var suffix: characters appearing after the terminator but before output redirection, if any - :type suffix: str or None + :type suffix: str :var pipe_to: if output was piped to a shell command, the shell command as a list of tokens :type pipe_to: list :var output: if output was redirected, the redirection token, i.e. '>>' - :type output: str or None + :type output: str :var output_to: if output was redirected, the destination file - :type output_to: str or None + :type output_to: str """ def __new__(cls, obj: object, *, - raw: str = None, - command: str = None, + raw: str = '', + command: str = '', arg_list: List[str] = None, - argv: List[str] = None, - multiline_command: str = None, - terminator: str = None, - suffix: str = None, - pipe_to: str = None, - output: str = None, - output_to: str = None + multiline_command: str = '', + terminator: str = '', + suffix: str = '', + pipe_to: List[str] = None, + output: str = '', + output_to: str = '' ): """Create a new instance of Statement @@ -81,30 +76,51 @@ class Statement(str): if arg_list is None: arg_list = [] object.__setattr__(stmt, "arg_list", arg_list) - if argv is None: - argv = [] - object.__setattr__(stmt, "argv", argv) object.__setattr__(stmt, "multiline_command", multiline_command) object.__setattr__(stmt, "terminator", terminator) object.__setattr__(stmt, "suffix", suffix) + if pipe_to is None: + pipe_to = [] object.__setattr__(stmt, "pipe_to", pipe_to) object.__setattr__(stmt, "output", output) object.__setattr__(stmt, "output_to", output_to) return stmt @property - def command_and_args(self): + def command_and_args(self) -> str: """Combine command and args with a space separating them. Quoted arguments remain quoted. """ - if self.command and self: - rtn = '{} {}'.format(self.command, self) + if self.command and self.args: + rtn = '{} {}'.format(self.command, self.args) elif self.command: # there were no arguments to the command rtn = self.command else: - rtn = None + rtn = '' + return rtn + + @property + def args(self) -> str: + """the arguments to the command, not including any output redirection or terminators. + + Quoted arguments remain quoted. + """ + return str(self) + + @property + def argv(self) -> List[str]: + """a list of arguments a la sys.argv. Quotes, if any, are removed + from the elements of the list, and aliases and shortcuts are expanded + """ + if self.command: + rtn = [utils.strip_quotes(self.command)] + for cur_token in self.arg_list: + rtn.append(utils.strip_quotes(cur_token)) + else: + rtn = [] + return rtn def __setattr__(self, name, value): @@ -233,7 +249,7 @@ class StatementParser: if match: if word == match.group(1): valid = True - errmsg = None + errmsg = '' return valid, errmsg def tokenize(self, line: str) -> List[str]: @@ -270,13 +286,13 @@ class StatementParser: # handle the special case/hardcoded terminator of a blank line # we have to do this before we tokenize because tokenizing # destroys all unquoted whitespace in the input - terminator = None + terminator = '' if line[-1:] == constants.LINE_FEED: terminator = constants.LINE_FEED - command = None - args = None - argv = None + command = '' + args = '' + arg_list = [] # lex the input into a list of tokens tokens = self.tokenize(line) @@ -304,8 +320,8 @@ class StatementParser: terminator_pos = len(tokens)+1 # everything before the first terminator is the command and the args - argv = tokens[:terminator_pos] - (command, args) = self._command_and_args(argv) + (command, args) = self._command_and_args(tokens[:terminator_pos]) + arg_list = tokens[1:terminator_pos] # we will set the suffix later # remove all the tokens before and including the terminator tokens = tokens[terminator_pos+1:] @@ -317,7 +333,7 @@ class StatementParser: # because redirectors can only be after a terminator command = testcommand args = testargs - argv = tokens + arg_list = tokens[1:] tokens = [] # check for a pipe to a shell process @@ -338,11 +354,11 @@ class StatementParser: tokens = tokens[:pipe_pos] except ValueError: # no pipe in the tokens - pipe_to = None + pipe_to = [] # check for output redirect - output = None - output_to = None + output = '' + output_to = '' try: output_pos = tokens.index(constants.REDIRECTION_OUTPUT) output = constants.REDIRECTION_OUTPUT @@ -376,26 +392,23 @@ class StatementParser: suffix = ' '.join(tokens) else: # no terminator, so whatever is left is the command and the args - suffix = None + suffix = '' if not command: # command could already have been set, if so, don't set it again - argv = tokens - (command, args) = self._command_and_args(argv) + (command, args) = self._command_and_args(tokens) + arg_list = tokens[1:] # set multiline if command in self.multiline_commands: multiline_command = command else: - multiline_command = None + multiline_command = '' # build the statement - # string representation of args must be an empty string instead of - # None for compatibility with standard library cmd - statement = Statement('' if args is None else args, + statement = Statement(args, raw=line, command=command, - arg_list=[] if len(argv) <= 1 else argv[1:], - argv=list(map(lambda x: utils.strip_quotes(x), argv)), + arg_list=arg_list, multiline_command=multiline_command, terminator=terminator, suffix=suffix, @@ -419,6 +432,7 @@ class StatementParser: values in the following attributes: - raw - command + - multiline_command Different from parse(), this method does not remove redundant whitespace within the statement. It does however, ensure statement does not have @@ -427,37 +441,33 @@ class StatementParser: # expand shortcuts and aliases line = self._expand(rawinput) - command = None - args = None + command = '' + args = '' match = self._command_pattern.search(line) if match: # we got a match, extract the command command = match.group(1) - # the match could be an empty string, if so, turn it into none - if not command: - command = None + # the _command_pattern regex is designed to match the spaces # between command and args with a second match group. Using # the end of the second match group ensures that args has # no leading whitespace. The rstrip() makes sure there is # no trailing whitespace args = line[match.end(2):].rstrip() - # if the command is none that means the input was either empty - # or something weird like '>'. args should be None if we couldn't + # if the command is empty that means the input was either empty + # or something weird like '>'. args should be empty if we couldn't # parse a command if not command or not args: - args = None + args = '' # set multiline if command in self.multiline_commands: multiline_command = command else: - multiline_command = None + multiline_command = '' # build the statement - # string representation of args must be an empty string instead of - # None for compatibility with standard library cmd - statement = Statement('' if args is None else args, + statement = Statement(args, raw=rawinput, command=command, multiline_command=multiline_command, @@ -503,12 +513,9 @@ class StatementParser: def _command_and_args(tokens: List[str]) -> Tuple[str, str]: """Given a list of tokens, return a tuple of the command and the args as a string. - - The args string will be '' instead of None to retain backwards compatibility - with cmd in the standard library. """ - command = None - args = None + command = '' + args = '' if tokens: command = tokens[0] diff --git a/tests/test_parsing.py b/tests/test_parsing.py index e1dfa982..d8f80a31 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -33,7 +33,7 @@ def default_parser(): def test_parse_empty_string_default(default_parser): statement = default_parser.parse('') - assert statement.command is None + assert statement.command == '' assert statement == '' assert statement.raw == '' @@ -53,7 +53,7 @@ def test_tokenize_default(default_parser, line, tokens): def test_parse_empty_string(parser): statement = parser.parse('') - assert statement.command is None + assert statement.command == '' assert statement == '' assert statement.raw == '' @@ -79,8 +79,8 @@ def test_tokenize_unclosed_quotes(parser): _ = parser.tokenize('command with "unclosed quotes') @pytest.mark.parametrize('tokens,command,args', [ - ([], None, None), - (['command'], 'command', None), + ([], '', ''), + (['command'], 'command', ''), (['command', 'arg1', 'arg2'], 'command', 'arg1 arg2') ]) def test_command_and_args(parser, tokens, command, args): @@ -168,12 +168,12 @@ def test_parse_c_comment(parser): assert statement.argv == ['hi'] assert not statement.arg_list assert statement == '' - assert statement.pipe_to is None + assert not statement.pipe_to def test_parse_c_comment_empty(parser): statement = parser.parse('/* this is | all a comment */') - assert statement.command is None - assert statement.pipe_to is None + assert statement.command == '' + assert not statement.pipe_to assert not statement.argv assert not statement.arg_list assert statement == '' @@ -182,7 +182,7 @@ def test_parse_c_comment_no_closing(parser): statement = parser.parse('cat /tmp/*.txt') assert statement.command == 'cat' assert statement == '/tmp/*.txt' - assert statement.pipe_to is None + assert not statement.pipe_to assert statement.argv == ['cat', '/tmp/*.txt'] assert statement.arg_list == statement.argv[1:] @@ -190,7 +190,7 @@ def test_parse_c_comment_multiple_opening(parser): statement = parser.parse('cat /tmp/*.txt /tmp/*.cfg') assert statement.command == 'cat' assert statement == '/tmp/*.txt /tmp/*.cfg' - assert statement.pipe_to is None + assert not statement.pipe_to assert statement.argv == ['cat', '/tmp/*.txt', '/tmp/*.cfg'] assert statement.arg_list == statement.argv[1:] @@ -200,7 +200,7 @@ def test_parse_what_if_quoted_strings_seem_to_start_comments(parser): assert statement == 'if "quoted strings /* seem to " start comments?' assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?'] assert statement.arg_list == ['if', '"quoted strings /* seem to "', 'start', 'comments?'] - assert statement.pipe_to is None + assert not statement.pipe_to @pytest.mark.parametrize('line',[ 'simple | piped', @@ -221,7 +221,7 @@ def test_parse_double_pipe_is_not_a_pipe(parser): assert statement == '|| is not a pipe' assert statement.argv == ['double-pipe', '||', 'is', 'not', 'a', 'pipe'] assert statement.arg_list == statement.argv[1:] - assert statement.pipe_to is None + assert not statement.pipe_to def test_parse_complex_pipe(parser): line = 'command with args, terminator&sufx | piped' @@ -287,8 +287,8 @@ def test_parse_pipe_and_redirect(parser): assert statement.terminator == ';' assert statement.suffix == 'sufx' assert statement.pipe_to == ['pipethrume', 'plz', '>', 'afile.txt'] - assert statement.output is None - assert statement.output_to is None + assert statement.output == '' + assert statement.output_to == '' def test_parse_output_to_paste_buffer(parser): line = 'output to paste buffer >> ' @@ -337,7 +337,7 @@ def test_parse_unfinished_multiliine_command(parser): assert statement == 'has > inside an unfinished command' assert statement.argv == ['multiline', 'has', '>', 'inside', 'an', 'unfinished', 'command'] assert statement.arg_list == statement.argv[1:] - assert statement.terminator is None + assert statement.terminator == '' @pytest.mark.parametrize('line,terminator',[ ('multiline has > inside;', ';'), @@ -471,7 +471,7 @@ def test_parse_alias_on_multiline_command(parser): assert statement.multiline_command == 'multiline' assert statement.command == 'multiline' assert statement == 'has > inside an unfinished command' - assert statement.terminator is None + assert statement.terminator == '' @pytest.mark.parametrize('line,output', [ ('helpalias > out.txt', '>'), @@ -523,11 +523,11 @@ def test_parse_command_only_emptyline(parser): # statement is a subclass of str(), the value of the str # should be '', to retain backwards compatibility with # the cmd in the standard library - assert statement.command is None + assert statement.command == '' assert statement == '' assert not statement.argv assert not statement.arg_list - assert statement.command_and_args is None + assert statement.command_and_args == '' def test_parse_command_only_strips_line(parser): line = ' help history ' @@ -582,9 +582,9 @@ def test_parse_command_only_specialchars(parser, line): '"', '|', ]) -def test_parse_command_only_none(parser, line): +def test_parse_command_only_empty(parser, line): statement = parser.parse_command_only(line) - assert statement.command is None + assert statement.command == '' assert statement == '' def test_parse_command_only_multiline(parser): @@ -600,14 +600,17 @@ def test_statement_initialization(parser): string = 'alias' statement = cmd2.Statement(string) assert string == statement - assert statement.raw is None - assert statement.command is None + assert statement.args == statement + assert statement.raw == '' + assert statement.command == '' assert isinstance(statement.arg_list, list) + assert not statement.arg_list assert isinstance(statement.argv, list) assert not statement.argv - assert statement.multiline_command is None - assert statement.terminator is None - assert statement.suffix is None - assert statement.pipe_to is None - assert statement.output is None - assert statement.output_to is None + assert statement.multiline_command == '' + assert statement.terminator == '' + assert statement.suffix == '' + assert isinstance(statement.pipe_to, list) + assert not statement.pipe_to + assert statement.output == '' + assert statement.output_to == '' |