summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2/cmd2.py32
-rw-r--r--cmd2/parsing.py62
-rw-r--r--tests/test_cmd2.py3
-rw-r--r--tests/test_parsing.py24
4 files changed, 86 insertions, 35 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 764f3ce7..4bec394b 100755
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -730,22 +730,6 @@ class Cmd(cmd.Cmd):
# If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
self.broken_pipe_warning = ''
- # regular expression to test for invalid characters in aliases
- # we will construct it dynamically, because some of the components
- # like terminator characters, can change
- invalid_items = []
- invalid_items.extend(constants.REDIRECTION_CHARS)
- invalid_items.extend(self.terminators)
- # escape each item so it will for sure get treated as a literal
- invalid_items = [re.escape(x) for x in invalid_items]
- # don't allow whitespace
- invalid_items.append(r'\s')
- # join them up with a pipe to form a regular expression
- # that looks something like r';|>|\||\s'
- expr = '|'.join(invalid_items)
- # and compile it into a pattern
- self.invalid_alias_pattern = re.compile(expr)
-
# If a startup script is provided, then add it in the queue to load
if startup_script is not None:
startup_script = os.path.expanduser(startup_script)
@@ -2396,15 +2380,17 @@ Usage: Usage: alias [name] | [<name> <value>]
name = arglist[0]
value = ' '.join(arglist[1:])
- # Validate the alias to ensure it doesn't include wierd characters
+ # Validate the alias to ensure it doesn't include weird characters
# like terminators, output redirection, or whitespace
- if self.invalid_alias_pattern.search(name):
- self.perror('Alias names can not contain special characters.', traceback_war=False)
- return
+ valid, invalidchars = self.statement_parser.is_valid_command(name)
+ if valid:
+ # Set the alias
+ self.aliases[name] = value
+ self.poutput("Alias {!r} created".format(name))
+ else:
+ errmsg = "Aliases can not contain: {}".format(invalidchars)
+ self.perror(errmsg, traceback_war=False)
- # Set the alias
- self.aliases[name] = value
- self.poutput("Alias {!r} created".format(name))
def complete_alias(self, text, line, begidx, endidx):
""" Tab completion for alias """
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index d7feeb48..3a9b390b 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -141,7 +141,7 @@ class StatementParser:
re.DOTALL | re.MULTILINE
)
- # aliases have to be a word, so make a regular expression
+ # commands have to be a word, so make a regular expression
# that matches the first word in the line. This regex has three
# parts:
# - the '\A\s*' matches the beginning of the string (even
@@ -157,19 +157,51 @@ class StatementParser:
# REDIRECTION_CHARS, one of the terminators, or the end of
# the string (\Z matches the end of the string even if it
# contains multiple lines)
- second_group_items = []
- second_group_items.extend(constants.REDIRECTION_CHARS)
- second_group_items.extend(terminators)
+ #
+ invalid_command_chars = []
+ invalid_command_chars.extend(constants.QUOTES)
+ invalid_command_chars.extend(constants.REDIRECTION_CHARS)
+ invalid_command_chars.extend(terminators)
# escape each item so it will for sure get treated as a literal
- second_group_items = [re.escape(x) for x in second_group_items]
+ second_group_items = [re.escape(x) for x in invalid_command_chars]
# add the whitespace and end of string, not escaped because they
# are not literals
second_group_items.extend([r'\s', r'\Z'])
# join them up with a pipe
second_group = '|'.join(second_group_items)
# build the regular expression
- expr = r'\A\s*(\S+?)({})'.format(second_group)
- self.command_pattern = re.compile(expr)
+ expr = r'\A\s*(\S*?)({})'.format(second_group)
+ self._command_pattern = re.compile(expr)
+
+ def is_valid_command(self, word: str) -> Tuple[bool, str]:
+ """Determine whether a word is a valid alias.
+
+ Aliases can not include redirection characters, whitespace,
+ or termination characters.
+
+ If word is not a valid command, return False and a comma
+ separated string of characters that can not appear in a command.
+ This string is suitable for inclusion in an error message of your
+ choice:
+
+ valid, invalidchars = statement_parser.is_valid_command('>')
+ if not valid:
+ errmsg = "Aliases can not contain: {}".format(invalidchars)
+ """
+ valid = False
+
+ errmsg = 'whitespace, quotes, '
+ errchars = []
+ errchars.extend(constants.REDIRECTION_CHARS)
+ errchars.extend(self.terminators)
+ errmsg += ', '.join([shlex.quote(x) for x in errchars])
+
+ match = self._command_pattern.search(word)
+ if match:
+ if word == match.group(1):
+ valid = True
+ errmsg = None
+ return valid, errmsg
def tokenize(self, line: str) -> List[str]:
"""Lex a string into a list of tokens.
@@ -344,16 +376,24 @@ class StatementParser:
command = None
args = None
- match = self.command_pattern.search(line)
+ match = self._command_pattern.search(line)
if match:
# we got a match, extract the command
command = match.group(1)
- # the command_pattern regex is designed to match the spaces
+ # 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 wierd like '>'. args should be None if we couldn't
+ # parse a command
+ if not command or not args:
+ args = None
# build the statement
# string representation of args must be an empty string instead of
@@ -375,11 +415,11 @@ class StatementParser:
for cur_alias in tmp_aliases:
keep_expanding = False
# apply our regex to line
- match = self.command_pattern.search(line)
+ match = self._command_pattern.search(line)
if match:
# we got a match, extract the command
command = match.group(1)
- if command == cur_alias:
+ if command and command == cur_alias:
# rebuild line with the expanded alias
line = self.aliases[cur_alias] + match.group(2) + line[match.end(2):]
tmp_aliases.remove(cur_alias)
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index c3c9a29f..bc76505f 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1707,10 +1707,11 @@ def test_unalias_non_existing(base_app, capsys):
@pytest.mark.parametrize('alias_name', [
'">"',
- '"no>pe"'
+ '"no>pe"',
'"no spaces"',
'"nopipe|"',
'"noterm;"',
+ 'noembedded"quotes',
])
def test_create_invalid_alias(base_app, alias_name, capsys):
run_cmd(base_app, 'alias {} help'.format(alias_name))
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index ad4d31cd..bfb55b23 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -439,3 +439,27 @@ def test_parse_command_only_quoted_args(parser):
assert statement.command == 'shell'
assert statement.args == 'ls -al "/tmp/directory with spaces/doit.sh"'
assert statement.command_and_args == line.replace('l', 'shell ls -al')
+
+@pytest.mark.parametrize('line', [
+ 'helpalias > out.txt',
+ 'helpalias>out.txt',
+ 'helpalias >> out.txt',
+ 'helpalias>>out.txt',
+ 'help|less',
+ 'helpalias;',
+])
+def test_parse_command_only_specialchars(parser, line):
+ statement = parser.parse_command_only(line)
+ assert statement.command == 'help'
+
+@pytest.mark.parametrize('line', [
+ ';',
+ '>',
+ "'",
+ '"',
+ '|',
+])
+def test_parse_command_only_none(parser, line):
+ statement = parser.parse_command_only(line)
+ assert statement.command == None
+ assert statement.args == None