Skip to content

Commit eb86c73

Browse files
authored
Merge pull request #631 from python-cmd2/comment_overhaul
Comment overhaul
2 parents de70108 + 1a2465a commit eb86c73

File tree

9 files changed

+84
-153
lines changed

9 files changed

+84
-153
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
``AutoCompleter`` which has since developed a dependency on ``cmd2`` methods.
1212
* Removed ability to call commands in ``pyscript`` as if they were functions (e.g ``app.help()``) in favor
1313
of only supporting one ``pyscript`` interface. This simplifies future maintenance.
14+
* No longer supporting C-style comments. Hash (#) is the only valid comment marker.
15+
* No longer supporting comments embedded in a command. Only command line input where the first
16+
non-whitespace character is a # will be treated as a comment. This means any # character appearing
17+
later in the command will be treated as a literal. The same applies to a # in the middle of a multiline
18+
command, even if it is the first character on a line.
19+
* \# this is a comment
20+
* this # is not a comment
1421

1522
## 0.9.10 (February 22, 2019)
1623
* Bug Fixes

CODEOWNERS

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
1515

1616
# cmd2 code
17-
cmd2/__init__.py @tleonhardt @kotfu
18-
cmd2/arg*.py @anselor
19-
cmd2/cmd2.py @tleonhardt @kmvanbrunt @kotfu
20-
cmd2/constants.py @kotfu
21-
cmd2/parsing.py @kotfu @kmvanbrunt
22-
cmd2/pyscript*.py @anselor
23-
cmd2/rl_utils.py @kmvanbrunt
24-
cmd2/transcript.py @kotfu
25-
cmd2/utils.py @tleonhardt @kotfu @kmvanbrunt
17+
cmd2/__init__.py @tleonhardt @kotfu
18+
cmd2/argparse_completer.py @anselor @kmvanbrunt
19+
cmd2/clipboard.py @tleonhardt
20+
cmd2/cmd2.py @tleonhardt @kmvanbrunt @kotfu
21+
cmd2/constants.py @kotfu
22+
cmd2/parsing.py @kotfu @kmvanbrunt
23+
cmd2/pyscript_bridge.py @anselor @kmvanbrunt
24+
cmd2/rl_utils.py @kmvanbrunt
25+
cmd2/transcript.py @kotfu
26+
cmd2/utils.py @tleonhardt @kotfu @kmvanbrunt
2627

2728
# Sphinx documentation
2829
docs/* @tleonhardt @kotfu

cmd2/cmd2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def parse_quoted_string(string: str, preserve_quotes: bool) -> List[str]:
160160
lexed_arglist = string
161161
else:
162162
# Use shlex to split the command line into a list of arguments based on shell rules
163-
lexed_arglist = shlex.split(string, posix=False)
163+
lexed_arglist = shlex.split(string, comments=False, posix=False)
164164

165165
if not preserve_quotes:
166166
lexed_arglist = [utils.strip_quotes(arg) for arg in lexed_arglist]
@@ -761,7 +761,7 @@ def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[Li
761761
while True:
762762
try:
763763
# Use non-POSIX parsing to keep the quotes around the tokens
764-
initial_tokens = shlex.split(tmp_line[:tmp_endidx], posix=False)
764+
initial_tokens = shlex.split(tmp_line[:tmp_endidx], comments=False, posix=False)
765765

766766
# If the cursor is at an empty token outside of a quoted string,
767767
# then that is the token being completed. Add it to the list.
@@ -2283,7 +2283,7 @@ def alias_list(self, args: argparse.Namespace) -> None:
22832283
" would for the actual command the alias resolves to.\n"
22842284
"\n"
22852285
"Examples:\n"
2286-
" alias ls !ls -lF\n"
2286+
" alias create ls !ls -lF\n"
22872287
" alias create show_log !cat \"log file.txt\"\n"
22882288
" alias create save_results print_results \">\" out.txt\n")
22892289

cmd2/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
REDIRECTION_APPEND = '>>'
1313
REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT]
1414
REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND]
15+
COMMENT_CHAR = '#'
1516

1617
# Regular expression to match ANSI escape codes
1718
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')

cmd2/parsing.py

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -236,33 +236,6 @@ def __init__(
236236
else:
237237
self.shortcuts = shortcuts
238238

239-
# this regular expression matches C-style comments and quoted
240-
# strings, i.e. stuff between single or double quote marks
241-
# it's used with _comment_replacer() to strip out the C-style
242-
# comments, while leaving C-style comments that are inside either
243-
# double or single quotes.
244-
#
245-
# this big regular expression can be broken down into 3 regular
246-
# expressions that are OR'ed together with a pipe character
247-
#
248-
# /\*.*\*/ Matches C-style comments (i.e. /* comment */)
249-
# does not match unclosed comments.
250-
# \'(?:\\.|[^\\\'])*\' Matches a single quoted string, allowing
251-
# for embedded backslash escaped single quote
252-
# marks.
253-
# "(?:\\.|[^\\"])*" Matches a double quoted string, allowing
254-
# for embedded backslash escaped double quote
255-
# marks.
256-
#
257-
# by way of reminder the (?:...) regular expression syntax is just
258-
# a non-capturing version of regular parenthesis. We need the non-
259-
# capturing syntax because _comment_replacer() looks at match
260-
# groups
261-
self.comment_pattern = re.compile(
262-
r'/\*.*\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
263-
re.DOTALL | re.MULTILINE
264-
)
265-
266239
# commands have to be a word, so make a regular expression
267240
# that matches the first word in the line. This regex has three
268241
# parts:
@@ -315,6 +288,9 @@ def is_valid_command(self, word: str) -> Tuple[bool, str]:
315288
if not word:
316289
return False, 'cannot be an empty string'
317290

291+
if word.startswith(constants.COMMENT_CHAR):
292+
return False, 'cannot start with the comment character'
293+
318294
for (shortcut, _) in self.shortcuts:
319295
if word.startswith(shortcut):
320296
# Build an error string with all shortcuts listed
@@ -338,24 +314,23 @@ def is_valid_command(self, word: str) -> Tuple[bool, str]:
338314
def tokenize(self, line: str) -> List[str]:
339315
"""Lex a string into a list of tokens.
340316
341-
Comments are removed, and shortcuts and aliases are expanded.
317+
shortcuts and aliases are expanded and comments are removed
342318
343319
Raises ValueError if there are unclosed quotation marks.
344320
"""
345321

346-
# strip C-style comments
347-
# shlex will handle the python/shell style comments for us
348-
line = re.sub(self.comment_pattern, self._comment_replacer, line)
349-
350322
# expand shortcuts and aliases
351323
line = self._expand(line)
352324

325+
# check if this line is a comment
326+
if line.strip().startswith(constants.COMMENT_CHAR):
327+
return []
328+
353329
# split on whitespace
354-
lexer = shlex.shlex(line, posix=False)
355-
lexer.whitespace_split = True
330+
tokens = shlex.split(line, comments=False, posix=False)
356331

357332
# custom lexing
358-
tokens = self._split_on_punctuation(list(lexer))
333+
tokens = self._split_on_punctuation(tokens)
359334
return tokens
360335

361336
def parse(self, line: str) -> Statement:
@@ -610,15 +585,6 @@ def _command_and_args(tokens: List[str]) -> Tuple[str, str]:
610585

611586
return command, args
612587

613-
@staticmethod
614-
def _comment_replacer(match):
615-
matched_string = match.group(0)
616-
if matched_string.startswith('/'):
617-
# the matched string was a comment, so remove it
618-
return ''
619-
# the matched string was a quoted string, return the match
620-
return matched_string
621-
622588
def _split_on_punctuation(self, tokens: List[str]) -> List[str]:
623589
"""Further splits tokens from a command line using punctuation characters
624590

docs/freefeatures.rst

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,16 @@ Simply include one command per line, typed exactly as you would inside a ``cmd2`
2929
Comments
3030
========
3131

32-
Comments are omitted from the argument list
33-
before it is passed to a ``do_`` method. By
34-
default, both Python-style and C-style comments
35-
are recognized. Comments can be useful in :ref:`scripts`, but would
36-
be pointless within an interactive session.
32+
Any command line input where the first non-whitespace character is a # will be treated as a comment.
33+
This means any # character appearing later in the command will be treated as a literal. The same
34+
applies to a # in the middle of a multiline command, even if it is the first character on a line.
3735

38-
::
39-
40-
def do_speak(self, arg):
41-
self.stdout.write(arg + '\n')
36+
Comments can be useful in :ref:`scripts`, but would be pointless within an interactive session.
4237

4338
::
4439

45-
(Cmd) speak it was /* not */ delicious! # Yuck!
46-
it was delicious!
47-
48-
.. _arg_print: https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py
40+
(Cmd) # this is a comment
41+
(Cmd) this # is not a comment
4942

5043
Startup Initialization Script
5144
=============================
@@ -209,9 +202,9 @@ is superior for doing this in two primary ways:
209202
- it has the ability to pass command-line arguments to the scripts invoked
210203

211204
There are no disadvantages to using ``pyscript`` as opposed to ``py run()``. A simple example
212-
of using ``pyscript`` is shown below along with the **examples/arg_printer.py** script::
205+
of using ``pyscript`` is shown below along with the arg_printer_ script::
213206

214-
(Cmd) pyscript examples/arg_printer.py foo bar baz
207+
(Cmd) pyscript examples/scripts/arg_printer.py foo bar baz
215208
Running Python script 'arg_printer.py' which was called with 3 arguments
216209
arg 1: 'foo'
217210
arg 2: 'bar'
@@ -224,11 +217,12 @@ of using ``pyscript`` is shown below along with the **examples/arg_printer.py**
224217

225218
When using this decorator, you can then put arguments in quotes like so (NOTE: the ``do_pyscript`` method uses this decorator::
226219

227-
(Cmd) pyscript examples/arg_printer.py hello '23 fnord'
220+
(Cmd) pyscript examples/scripts/arg_printer.py hello '23 fnord'
228221
Running Python script 'arg_printer.py' which was called with 2 arguments
229222
arg 1: 'hello'
230223
arg 2: '23 fnord'
231224

225+
.. _arg_printer: https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/arg_printer.py
232226

233227
IPython (optional)
234228
==================

tests/test_argparse.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,6 @@ def test_argparse_with_list_and_empty_doc(argparse_app):
141141
out = run_cmd(argparse_app, 'speak -s hello world!')
142142
assert out == ['HELLO WORLD!']
143143

144-
def test_argparse_comment_stripping(argparse_app):
145-
out = run_cmd(argparse_app, 'speak it was /* not */ delicious! # Yuck!')
146-
assert out == ['it was delicious!']
147-
148144
def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app):
149145
out = run_cmd(argparse_app, "speak 'This is a' -s test of the emergency broadcast system!")
150146
assert out == ['THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!']

tests/test_cmd2.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
from unittest import mock
2525

2626
import cmd2
27-
from cmd2 import clipboard
28-
from cmd2 import utils
27+
from cmd2 import clipboard, constants, utils
2928
from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
3029
HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
3130

@@ -1828,6 +1827,7 @@ def test_poutput_color_never(base_app):
18281827
# These are invalid names for aliases and macros
18291828
invalid_command_name = [
18301829
'""', # Blank name
1830+
constants.COMMENT_CHAR,
18311831
'!no_shortcut',
18321832
'">"',
18331833
'"no>pe"',
@@ -1900,6 +1900,17 @@ def test_alias_create_with_macro_name(base_app, capsys):
19001900
out, err = capsys.readouterr()
19011901
assert "Alias cannot have the same name as a macro" in err
19021902

1903+
def test_alias_that_resolves_into_comment(base_app, capsys):
1904+
# Create the alias
1905+
out = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah')
1906+
assert out == normalize("Alias 'fake' created")
1907+
1908+
# Use the alias
1909+
run_cmd(base_app, 'fake')
1910+
out, err = capsys.readouterr()
1911+
assert not out
1912+
assert not err
1913+
19031914
def test_alias_list_invalid_alias(base_app, capsys):
19041915
# Look up invalid alias
19051916
out = run_cmd(base_app, 'alias list invalid')
@@ -2056,6 +2067,17 @@ def test_macro_create_with_missing_unicode_arg_nums(base_app, capsys):
20562067
out, err = capsys.readouterr()
20572068
assert "Not all numbers between 1 and 3" in err
20582069

2070+
def test_macro_that_resolves_into_comment(base_app, capsys):
2071+
# Create the macro
2072+
out = run_cmd(base_app, 'macro create fake {1} blah blah')
2073+
assert out == normalize("Macro 'fake' created")
2074+
2075+
# Use the macro
2076+
run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR)
2077+
out, err = capsys.readouterr()
2078+
assert not out
2079+
assert not err
2080+
20592081
def test_macro_list_invalid_macro(base_app, capsys):
20602082
# Look up invalid macro
20612083
run_cmd(base_app, 'macro list invalid')

0 commit comments

Comments
 (0)