summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md7
-rw-r--r--CODEOWNERS19
-rw-r--r--cmd2/cmd2.py6
-rw-r--r--cmd2/constants.py1
-rw-r--r--cmd2/parsing.py54
-rw-r--r--docs/freefeatures.rst25
-rw-r--r--tests/test_argparse.py4
-rw-r--r--tests/test_cmd2.py26
-rw-r--r--tests/test_parsing.py94
9 files changed, 83 insertions, 153 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fad8ca2e..4903a89f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,13 @@
``AutoCompleter`` which has since developed a dependency on ``cmd2`` methods.
* Removed ability to call commands in ``pyscript`` as if they were functions (e.g ``app.help()``) in favor
of only supporting one ``pyscript`` interface. This simplifies future maintenance.
+ * No longer supporting C-style comments. Hash (#) is the only valid comment marker.
+ * No longer supporting comments embedded in a command. Only command line input where the first
+ non-whitespace character is a # will be treated as a comment. This means any # character appearing
+ later in the command will be treated as a literal. The same applies to a # in the middle of a multiline
+ command, even if it is the first character on a line.
+ * \# this is a comment
+ * this # is not a comment
## 0.9.10 (February 22, 2019)
* Bug Fixes
diff --git a/CODEOWNERS b/CODEOWNERS
index 9a87d8a4..0192ef0f 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -14,15 +14,16 @@
#docs/* docs@example.com
# cmd2 code
-cmd2/__init__.py @tleonhardt @kotfu
-cmd2/arg*.py @anselor
-cmd2/cmd2.py @tleonhardt @kmvanbrunt @kotfu
-cmd2/constants.py @kotfu
-cmd2/parsing.py @kotfu @kmvanbrunt
-cmd2/pyscript*.py @anselor
-cmd2/rl_utils.py @kmvanbrunt
-cmd2/transcript.py @kotfu
-cmd2/utils.py @tleonhardt @kotfu @kmvanbrunt
+cmd2/__init__.py @tleonhardt @kotfu
+cmd2/argparse_completer.py @anselor @kmvanbrunt
+cmd2/clipboard.py @tleonhardt
+cmd2/cmd2.py @tleonhardt @kmvanbrunt @kotfu
+cmd2/constants.py @kotfu
+cmd2/parsing.py @kotfu @kmvanbrunt
+cmd2/pyscript_bridge.py @anselor @kmvanbrunt
+cmd2/rl_utils.py @kmvanbrunt
+cmd2/transcript.py @kotfu
+cmd2/utils.py @tleonhardt @kotfu @kmvanbrunt
# Sphinx documentation
docs/* @tleonhardt @kotfu
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 1167e529..1bfeba1d 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -160,7 +160,7 @@ def parse_quoted_string(string: str, preserve_quotes: bool) -> List[str]:
lexed_arglist = string
else:
# Use shlex to split the command line into a list of arguments based on shell rules
- lexed_arglist = shlex.split(string, posix=False)
+ lexed_arglist = shlex.split(string, comments=False, posix=False)
if not preserve_quotes:
lexed_arglist = [utils.strip_quotes(arg) for arg in lexed_arglist]
@@ -766,7 +766,7 @@ class Cmd(cmd.Cmd):
while True:
try:
# Use non-POSIX parsing to keep the quotes around the tokens
- initial_tokens = shlex.split(tmp_line[:tmp_endidx], posix=False)
+ initial_tokens = shlex.split(tmp_line[:tmp_endidx], comments=False, posix=False)
# If the cursor is at an empty token outside of a quoted string,
# then that is the token being completed. Add it to the list.
@@ -2288,7 +2288,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")
diff --git a/cmd2/constants.py b/cmd2/constants.py
index 3c133b70..3e35a542 100644
--- a/cmd2/constants.py
+++ b/cmd2/constants.py
@@ -12,6 +12,7 @@ REDIRECTION_OUTPUT = '>'
REDIRECTION_APPEND = '>>'
REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT]
REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND]
+COMMENT_CHAR = '#'
# Regular expression to match ANSI escape codes
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index d4f82ac9..bd3a6900 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -236,33 +236,6 @@ class StatementParser:
else:
self.shortcuts = shortcuts
- # this regular expression matches C-style comments and quoted
- # strings, i.e. stuff between single or double quote marks
- # it's used with _comment_replacer() to strip out the C-style
- # comments, while leaving C-style comments that are inside either
- # double or single quotes.
- #
- # this big regular expression can be broken down into 3 regular
- # expressions that are OR'ed together with a pipe character
- #
- # /\*.*\*/ Matches C-style comments (i.e. /* comment */)
- # does not match unclosed comments.
- # \'(?:\\.|[^\\\'])*\' Matches a single quoted string, allowing
- # for embedded backslash escaped single quote
- # marks.
- # "(?:\\.|[^\\"])*" Matches a double quoted string, allowing
- # for embedded backslash escaped double quote
- # marks.
- #
- # by way of reminder the (?:...) regular expression syntax is just
- # a non-capturing version of regular parenthesis. We need the non-
- # capturing syntax because _comment_replacer() looks at match
- # groups
- self.comment_pattern = re.compile(
- r'/\*.*\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
- re.DOTALL | re.MULTILINE
- )
-
# commands have to be a word, so make a regular expression
# that matches the first word in the line. This regex has three
# parts:
@@ -315,6 +288,9 @@ class StatementParser:
if not word:
return False, 'cannot be an empty string'
+ if word.startswith(constants.COMMENT_CHAR):
+ return False, 'cannot start with the comment character'
+
for (shortcut, _) in self.shortcuts:
if word.startswith(shortcut):
# Build an error string with all shortcuts listed
@@ -338,24 +314,23 @@ class StatementParser:
def tokenize(self, line: str) -> List[str]:
"""Lex a string into a list of tokens.
- Comments are removed, and shortcuts and aliases are expanded.
+ shortcuts and aliases are expanded and comments are removed
Raises ValueError if there are unclosed quotation marks.
"""
- # strip C-style comments
- # shlex will handle the python/shell style comments for us
- line = re.sub(self.comment_pattern, self._comment_replacer, line)
-
# expand shortcuts and aliases
line = self._expand(line)
+ # check if this line is a comment
+ if line.strip().startswith(constants.COMMENT_CHAR):
+ return []
+
# split on whitespace
- lexer = shlex.shlex(line, posix=False)
- lexer.whitespace_split = True
+ tokens = shlex.split(line, comments=False, posix=False)
# custom lexing
- tokens = self._split_on_punctuation(list(lexer))
+ tokens = self._split_on_punctuation(tokens)
return tokens
def parse(self, line: str) -> Statement:
@@ -610,15 +585,6 @@ class StatementParser:
return command, args
- @staticmethod
- def _comment_replacer(match):
- matched_string = match.group(0)
- if matched_string.startswith('/'):
- # the matched string was a comment, so remove it
- return ''
- # the matched string was a quoted string, return the match
- return matched_string
-
def _split_on_punctuation(self, tokens: List[str]) -> List[str]:
"""Further splits tokens from a command line using punctuation characters
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index b6c7bebd..a34d9fcc 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -29,23 +29,16 @@ Simply include one command per line, typed exactly as you would inside a ``cmd2`
Comments
========
-Comments are omitted from the argument list
-before it is passed to a ``do_`` method. By
-default, both Python-style and C-style comments
-are recognized. Comments can be useful in :ref:`scripts`, but would
-be pointless within an interactive session.
+Any command line input where the first non-whitespace character is a # will be treated as a comment.
+This means any # character appearing later in the command will be treated as a literal. The same
+applies to a # in the middle of a multiline command, even if it is the first character on a line.
-::
-
- def do_speak(self, arg):
- self.stdout.write(arg + '\n')
+Comments can be useful in :ref:`scripts`, but would be pointless within an interactive session.
::
- (Cmd) speak it was /* not */ delicious! # Yuck!
- it was delicious!
-
-.. _arg_print: https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py
+ (Cmd) # this is a comment
+ (Cmd) this # is not a comment
Startup Initialization Script
=============================
@@ -209,9 +202,9 @@ is superior for doing this in two primary ways:
- it has the ability to pass command-line arguments to the scripts invoked
There are no disadvantages to using ``pyscript`` as opposed to ``py run()``. A simple example
-of using ``pyscript`` is shown below along with the **examples/arg_printer.py** script::
+of using ``pyscript`` is shown below along with the arg_printer_ script::
- (Cmd) pyscript examples/arg_printer.py foo bar baz
+ (Cmd) pyscript examples/scripts/arg_printer.py foo bar baz
Running Python script 'arg_printer.py' which was called with 3 arguments
arg 1: 'foo'
arg 2: 'bar'
@@ -226,8 +219,8 @@ of using ``pyscript`` is shown below along with the **examples/arg_printer.py**
$ examples/arg_print.py
(Cmd) lprint foo "bar baz"
- lprint was called with the following list of arguments: ['foo', 'bar baz']
+.. _arg_printer: https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/arg_printer.py
IPython (optional)
==================
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index a055ac72..f5948f03 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -145,10 +145,6 @@ def test_argparse_with_list_and_empty_doc(argparse_app):
out = run_cmd(argparse_app, 'speak -s hello world!')
assert out == ['HELLO WORLD!']
-def test_argparse_comment_stripping(argparse_app):
- out = run_cmd(argparse_app, 'speak it was /* not */ delicious! # Yuck!')
- assert out == ['it was delicious!']
-
def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app):
out = run_cmd(argparse_app, "speak 'This is a' -s test of the emergency broadcast system!")
assert out == ['THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!']
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index faef21f9..af1d41c9 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -24,8 +24,7 @@ except ImportError:
from unittest import mock
import cmd2
-from cmd2 import clipboard
-from cmd2 import utils
+from cmd2 import clipboard, constants, utils
from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
@@ -1828,6 +1827,7 @@ def test_poutput_color_never(base_app):
# These are invalid names for aliases and macros
invalid_command_name = [
'""', # Blank name
+ constants.COMMENT_CHAR,
'!no_shortcut',
'">"',
'"no>pe"',
@@ -1900,6 +1900,17 @@ def test_alias_create_with_macro_name(base_app, capsys):
out, err = capsys.readouterr()
assert "Alias cannot have the same name as a macro" in err
+def test_alias_that_resolves_into_comment(base_app, capsys):
+ # Create the alias
+ out = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah')
+ assert out == normalize("Alias 'fake' created")
+
+ # Use the alias
+ run_cmd(base_app, 'fake')
+ out, err = capsys.readouterr()
+ assert not out
+ assert not err
+
def test_alias_list_invalid_alias(base_app, capsys):
# Look up invalid alias
out = run_cmd(base_app, 'alias list invalid')
@@ -2056,6 +2067,17 @@ def test_macro_create_with_missing_unicode_arg_nums(base_app, capsys):
out, err = capsys.readouterr()
assert "Not all numbers between 1 and 3" in err
+def test_macro_that_resolves_into_comment(base_app, capsys):
+ # Create the macro
+ out = run_cmd(base_app, 'macro create fake {1} blah blah')
+ assert out == normalize("Macro 'fake' created")
+
+ # Use the macro
+ run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR)
+ out, err = capsys.readouterr()
+ assert not out
+ assert not err
+
def test_macro_list_invalid_macro(base_app, capsys):
# Look up invalid macro
run_cmd(base_app, 'macro list invalid')
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 78adf880..de49d3f5 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -11,7 +11,7 @@ import pytest
import cmd2
from cmd2.parsing import StatementParser
-from cmd2 import utils
+from cmd2 import constants, utils
@pytest.fixture
def parser():
@@ -70,8 +70,8 @@ def test_parse_empty_string_default(default_parser):
@pytest.mark.parametrize('line,tokens', [
('command', ['command']),
- ('command /* with some comment */ arg', ['command', 'arg']),
- ('command arg1 arg2 # comment at the end', ['command', 'arg1', 'arg2']),
+ (constants.COMMENT_CHAR + 'comment', []),
+ ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']),
('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']),
('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']),
('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']),
@@ -84,8 +84,8 @@ def test_tokenize_default(default_parser, line, tokens):
@pytest.mark.parametrize('line,tokens', [
('command', ['command']),
- ('command /* with some comment */ arg', ['command', 'arg']),
- ('command arg1 arg2 # comment at the end', ['command', 'arg1', 'arg2']),
+ ('# comment', []),
+ ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']),
('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']),
('l', ['shell', 'ls', '-al']),
('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']),
@@ -193,59 +193,23 @@ def test_parse_command_with_args_terminator_and_suffix(parser):
assert statement.terminator == ';'
assert statement.suffix == 'and suffix'
-def test_parse_hashcomment(parser):
- statement = parser.parse('hi # this is all a comment')
- assert statement.command == 'hi'
- assert statement == ''
- assert statement.args == statement
- assert statement.argv == ['hi']
- assert not statement.arg_list
-
-def test_parse_c_comment(parser):
- statement = parser.parse('hi /* this is | all a comment */')
- assert statement.command == 'hi'
- assert statement == ''
- assert statement.args == statement
- assert statement.argv == ['hi']
- assert not statement.arg_list
- assert not statement.pipe_to
-
-def test_parse_c_comment_empty(parser):
- statement = parser.parse('/* this is | all a comment */')
+def test_parse_comment(parser):
+ statement = parser.parse(constants.COMMENT_CHAR + ' this is all a comment')
assert statement.command == ''
+ assert statement == ''
assert statement.args == statement
- assert not statement.pipe_to
assert not statement.argv
assert not statement.arg_list
- assert statement == ''
-def test_parse_c_comment_no_closing(parser):
- statement = parser.parse('cat /tmp/*.txt')
- assert statement.command == 'cat'
- assert statement == '/tmp/*.txt'
- assert statement.args == statement
- assert not statement.pipe_to
- assert statement.argv == ['cat', '/tmp/*.txt']
- assert statement.arg_list == statement.argv[1:]
-
-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'
+def test_parse_embedded_comment_char(parser):
+ command_str = 'hi ' + constants.COMMENT_CHAR + ' not a comment'
+ statement = parser.parse(command_str)
+ assert statement.command == 'hi'
+ assert statement == constants.COMMENT_CHAR + ' not a comment'
assert statement.args == statement
- assert not statement.pipe_to
- assert statement.argv == ['cat', '/tmp/*.txt', '/tmp/*.cfg']
+ assert statement.argv == command_str.split()
assert statement.arg_list == statement.argv[1:]
-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 == 'if "quoted strings /* seem to " start comments?'
- assert statement.args == statement
- assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?']
- assert statement.arg_list == ['if', '"quoted strings /* seem to "', 'start', 'comments?']
- assert not statement.pipe_to
-
@pytest.mark.parametrize('line',[
'simple | piped',
'simple|piped',
@@ -411,30 +375,6 @@ def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, ter
assert statement.arg_list == statement.argv[1:]
assert statement.terminator == 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 unclosed comment;'
- statement = parser.parse(line)
- assert statement.multiline_command == 'multiline'
- assert statement.command == 'multiline'
- assert statement == 'command /* with unclosed comment'
- assert statement.args == statement
- assert statement.argv == ['multiline', 'command', '/*', 'with', 'unclosed', 'comment']
- assert statement.arg_list == statement.argv[1:]
- assert statement.terminator == ';'
-
-def test_parse_multiline_with_complete_comment(parser):
- line = 'multiline command /* with comment complete */ is done;'
- statement = parser.parse(line)
- assert statement.multiline_command == 'multiline'
- assert statement.command == 'multiline'
- assert statement == 'command is done'
- assert statement.args == statement
- assert statement.argv == ['multiline', 'command', 'is', 'done']
- assert statement.arg_list == statement.argv[1:]
- assert statement.terminator == ';'
-
def test_parse_multiline_terminated_by_empty_line(parser):
line = 'multiline command ends\n\n'
statement = parser.parse(line)
@@ -464,7 +404,7 @@ def test_parse_multiline_with_embedded_newline(parser, line, terminator):
assert statement.arg_list == ['command', '"with\nembedded newline"']
assert statement.terminator == terminator
-def test_parse_multiline_ignores_terminators_in_comments(parser):
+def test_parse_multiline_ignores_terminators_in_quotes(parser):
line = 'multiline command "with term; ends" now\n\n'
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
@@ -762,6 +702,10 @@ def test_is_valid_command_invalid(parser):
valid, errmsg = parser.is_valid_command('')
assert not valid and 'cannot be an empty string' in errmsg
+ # Start with the comment character
+ valid, errmsg = parser.is_valid_command(constants.COMMENT_CHAR)
+ assert not valid and 'cannot start with the comment character' in errmsg
+
# Starts with shortcut
valid, errmsg = parser.is_valid_command('!ls')
assert not valid and 'cannot start with a shortcut' in errmsg