summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2018-09-01 19:40:38 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2018-09-01 19:40:38 -0400
commit1a7f408fa0b259b30971cda477d952e202ac4fc2 (patch)
tree25911d3b0b4ee8f91e8fe0277b9e003f7877777a
parent9069df01e5a26b6d06e5e6e189799cc74dffbb91 (diff)
downloadcmd2-git-1a7f408fa0b259b30971cda477d952e202ac4fc2.tar.gz
Using empty strings and lists instead of None for default values in Statment
-rw-r--r--cmd2/cmd2.py2
-rw-r--r--cmd2/parsing.py135
-rw-r--r--tests/test_parsing.py57
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 == ''