summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md5
-rwxr-xr-xcmd2/argparse_completer.py5
-rw-r--r--cmd2/cmd2.py23
-rw-r--r--cmd2/parsing.py35
-rw-r--r--docs/unfreefeatures.rst7
-rwxr-xr-xexamples/exit_code.py43
-rwxr-xr-xexamples/tab_autocompletion.py11
-rw-r--r--tasks.py34
-rw-r--r--tests/conftest.py8
-rw-r--r--tests/test_autocompletion.py6
-rw-r--r--tests/test_cmd2.py83
-rw-r--r--tests/test_parsing.py181
12 files changed, 287 insertions, 154 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e08e5a75..9f201664 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
## 0.9.5 (TBD, 2018)
* Bug Fixes
* Fixed bug where ``get_all_commands`` could return non-callable attributes
+* Enhancements
+ * Added ``exit_code`` attribute of ``cmd2.Cmd`` class
+ * Enables applications to return a non-zero exit code when exiting from ``cmdloop``
+ * ``ACHelpFormatter`` now inherits from ``argparse.RawTextHelpFormatter`` to make it easier
+ for formatting help/description text
## 0.9.4 (August 21, 2018)
* Bug Fixes
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 1479a6bf..0e241cd9 100755
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -695,7 +695,7 @@ class AutoCompleter(object):
# noinspection PyCompatibility,PyShadowingBuiltins,PyShadowingBuiltins
-class ACHelpFormatter(argparse.HelpFormatter):
+class ACHelpFormatter(argparse.RawTextHelpFormatter):
"""Custom help formatter to configure ordering of help text"""
def _format_usage(self, usage, actions, groups, prefix) -> str:
@@ -870,9 +870,6 @@ class ACHelpFormatter(argparse.HelpFormatter):
result = super()._format_args(action, default_metavar)
return result
- def _split_lines(self, text: str, width) -> List[str]:
- return text.splitlines()
-
# noinspection PyCompatibility
class ACArgumentParser(argparse.ArgumentParser):
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 83a5a7c8..1c8bb6b7 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -523,6 +523,9 @@ class Cmd(cmd.Cmd):
# This boolean flag determines whether or not the cmd2 application can interact with the clipboard
self.can_clip = can_clip
+ # This determines if a non-zero exit code should be used when exiting the application
+ self.exit_code = None
+
# ----- Methods related to presenting output to the user -----
@property
@@ -1717,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.args, statement.command_and_args
+ return statement.command, statement, 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.
@@ -2565,18 +2568,19 @@ Usage: Usage: unalias [-a] name [name ...]
else:
raise LookupError("Parameter '%s' not supported (type 'set' for list of parameters)." % param)
- set_parser = ACArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
+ set_description = "Sets a settable parameter or shows current settings of parameters.\n"
+ set_description += "\n"
+ set_description += "Accepts abbreviated parameter names so long as there is no ambiguity.\n"
+ set_description += "Call without arguments for a list of settable parameters with their values."
+
+ set_parser = ACArgumentParser(description=set_description)
set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
set_parser.add_argument('settable', nargs=(0, 2), help='[param_name] [value]')
@with_argparser(set_parser)
def do_set(self, args: argparse.Namespace) -> None:
- """Sets a settable parameter or shows current settings of parameters.
-
- Accepts abbreviated parameter names so long as there is no ambiguity.
- Call without arguments for a list of settable parameters with their values.
- """
+ """Sets a settable parameter or shows current settings of parameters"""
try:
param_name, val = args.settable
val = val.strip()
@@ -2887,7 +2891,7 @@ Paths or arguments that contain spaces must be enclosed in quotes
embed(banner1=banner, exit_msg=exit_msg)
load_ipy(bridge)
- history_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
+ history_parser = ACArgumentParser()
history_parser_group = history_parser.add_mutually_exclusive_group()
history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
history_parser_group.add_argument('-e', '--edit', action='store_true',
@@ -3235,6 +3239,9 @@ Script should contain one command per line, just like command would be typed in
func()
self.postloop()
+ if self.exit_code is not None:
+ sys.exit(self.exit_code)
+
###
#
# plugin related functions
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index b98f630d..2a4ae56f 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -5,7 +5,7 @@
import os
import re
import shlex
-from typing import List, Tuple, Dict
+from typing import List, Tuple, Dict, Union
from . import constants
from . import utils
@@ -33,10 +33,10 @@ class Statement(str):
:var multiline_command: if the command is a multiline command, the name of the
command, otherwise None
:type command: str or None
- :var args: the arguments to the command, not including any output
+ :var arg_list: list of arguments to the command, not including any output
redirection or terminators. quoted arguments remain
quoted.
- :type args: str or None
+ :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
@@ -61,7 +61,7 @@ class Statement(str):
*,
raw: str = None,
command: str = None,
- args: str = None,
+ arg_list: List[str] = None,
argv: List[str] = None,
multiline_command: str = None,
terminator: str = None,
@@ -78,7 +78,9 @@ class Statement(str):
stmt = str.__new__(cls, obj)
object.__setattr__(stmt, "raw", raw)
object.__setattr__(stmt, "command", command)
- object.__setattr__(stmt, "args", args)
+ if arg_list is None:
+ arg_list = []
+ object.__setattr__(stmt, "arg_list", arg_list)
if argv is None:
argv = []
object.__setattr__(stmt, "argv", argv)
@@ -96,26 +98,15 @@ class Statement(str):
Quoted arguments remain quoted.
"""
- if self.command and self.args:
- rtn = '{} {}'.format(self.command, self.args)
+ if self.command and self:
+ rtn = '{} {}'.format(self.command, self)
elif self.command:
- # we are trusting that if we get here that self.args is None
+ # there were no arguments to the command
rtn = self.command
else:
rtn = None
return rtn
- @property
- def arg_list(self) -> List[str]:
- """
- Returns a list of the arguments to the command, not including any output
- redirection or terminators. quoted arguments remain quoted.
- """
- if self.args is None:
- return []
-
- return self.args.split()
-
def __setattr__(self, name, value):
"""Statement instances should feel immutable; raise ValueError"""
raise ValueError
@@ -403,7 +394,7 @@ class StatementParser:
statement = Statement('' if args is None else args,
raw=line,
command=command,
- args=args,
+ arg_list=[] if len(argv) <= 1 else argv[1:],
argv=list(map(lambda x: utils.strip_quotes(x), argv)),
multiline_command=multiline_command,
terminator=terminator,
@@ -428,10 +419,9 @@ class StatementParser:
values in the following attributes:
- raw
- command
- - args
Different from parse(), this method does not remove redundant whitespace
- within statement.args. It does however, ensure args does not have
+ within the statement. It does however, ensure statement does not have
leading or trailing whitespace.
"""
# expand shortcuts and aliases
@@ -470,7 +460,6 @@ class StatementParser:
statement = Statement('' if args is None else args,
raw=rawinput,
command=command,
- args=args,
multiline_command=multiline_command,
)
return statement
diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst
index 41144c8f..cd27745d 100644
--- a/docs/unfreefeatures.rst
+++ b/docs/unfreefeatures.rst
@@ -182,3 +182,10 @@ Presents numbered options to user, as bash ``select``.
2. salty
Sauce? 2
wheaties with salty sauce, yum!
+
+
+Exit code to shell
+==================
+The ``self.exit_code`` attribute of your ``cmd2`` application controls
+what exit code is sent to the shell when your application exits from
+``cmdloop()``.
diff --git a/examples/exit_code.py b/examples/exit_code.py
new file mode 100755
index 00000000..8ae2d310
--- /dev/null
+++ b/examples/exit_code.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""A simple example demonstrating the following how to emit a non-zero exit code in your cmd2 application.
+"""
+import cmd2
+import sys
+from typing import List
+
+
+class ReplWithExitCode(cmd2.Cmd):
+ """ Example cmd2 application where we can specify an exit code when existing."""
+
+ def __init__(self):
+ super().__init__()
+
+ @cmd2.with_argument_list
+ def do_exit(self, arg_list: List[str]) -> bool:
+ """Exit the application with an optional exit code.
+
+Usage: exit [exit_code]
+ Where:
+ * exit_code - integer exit code to return to the shell
+"""
+ # If an argument was provided
+ if arg_list:
+ try:
+ self.exit_code = int(arg_list[0])
+ except ValueError:
+ self.perror("{} isn't a valid integer exit code".format(arg_list[0]))
+ self.exit_code = -1
+
+ self._should_quit = True
+ return self._STOP_AND_EXIT
+
+ def postloop(self) -> None:
+ """Hook method executed once when the cmdloop() method is about to return."""
+ code = self.exit_code if self.exit_code is not None else 0
+ self.poutput('{!r} exiting with code: {}'.format(sys.argv[0], code))
+
+
+if __name__ == '__main__':
+ app = ReplWithExitCode()
+ app.cmdloop()
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index 38972358..6a2e683e 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -125,7 +125,9 @@ class TabCompleteExample(cmd2.Cmd):
# - The help output for arguments with multiple flags or with append=True is more concise
# - ACArgumentParser adds the ability to specify ranges of argument counts in 'nargs'
- suggest_parser = argparse_completer.ACArgumentParser()
+ suggest_description = "Suggest command demonstrates argparse customizations.\n"
+ suggest_description += "See hybrid_suggest and orig_suggest to compare the help output."
+ suggest_parser = argparse_completer.ACArgumentParser(description=suggest_description)
suggest_parser.add_argument('-t', '--type', choices=['movie', 'show'], required=True)
suggest_parser.add_argument('-d', '--duration', nargs=(1, 2), action='append',
@@ -136,12 +138,7 @@ class TabCompleteExample(cmd2.Cmd):
@cmd2.with_category(CAT_AUTOCOMPLETE)
@cmd2.with_argparser(suggest_parser)
def do_suggest(self, args) -> None:
- """Suggest command demonstrates argparse customizations
-
- See hybrid_suggest and orig_suggest to compare the help output.
-
-
- """
+ """Suggest command demonstrates argparse customizations"""
if not args.type:
self.do_help('suggest')
diff --git a/tasks.py b/tasks.py
index 583674e9..d6dc43c9 100644
--- a/tasks.py
+++ b/tasks.py
@@ -8,7 +8,9 @@ Make sure you satisfy the following Python module requirements if you are trying
- setuptools >= 39.1.0
"""
import os
+import re
import shutil
+import sys
import invoke
@@ -173,6 +175,34 @@ def clean_all(context):
pass
namespace_clean.add_task(clean_all, 'all')
+@invoke.task
+def tag(context, name='', message=''):
+ "Add a Git tag and push it to origin"
+ # If a tag was provided on the command-line, then add a Git tag and push it to origin
+ if name:
+ context.run('git tag -a {} -m {!r}'.format(name, message))
+ context.run('git push origin {}'.format(name))
+namespace.add_task(tag)
+
+@invoke.task()
+def validatetag(context):
+ "Check to make sure that a tag exists for the current HEAD and it looks like a valid version number"
+ # Validate that a Git tag exists for the current commit HEAD
+ result = context.run("git describe --exact-match --tags $(git log -n1 --pretty='%h')")
+ tag = result.stdout.rstrip()
+
+ # Validate that the Git tag appears to be a valid version number
+ ver_regex = re.compile('(\d+)\.(\d+)\.(\d+)')
+ match = ver_regex.fullmatch(tag)
+ if match is None:
+ print('Tag {!r} does not appear to be a valid version number'.format(tag))
+ sys.exit(-1)
+ else:
+ print('Tag {!r} appears to be a valid version number'.format(tag))
+
+
+namespace.add_task(validatetag)
+
@invoke.task(pre=[clean_all])
def sdist(context):
"Create a source distribution"
@@ -185,13 +215,13 @@ def wheel(context):
context.run('python setup.py bdist_wheel')
namespace.add_task(wheel)
-@invoke.task(pre=[sdist, wheel])
+@invoke.task(pre=[validatetag, sdist, wheel])
def pypi(context):
"Build and upload a distribution to pypi"
context.run('twine upload dist/*')
namespace.add_task(pypi)
-@invoke.task(pre=[sdist, wheel])
+@invoke.task(pre=[validatetag, sdist, wheel])
def pypi_test(context):
"Build and upload a distribution to https://test.pypi.org"
context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*')
diff --git a/tests/conftest.py b/tests/conftest.py
index 3f3b862e..f86a4c63 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -42,14 +42,14 @@ load Runs commands in script file that is encoded as either ASCII
py Invoke python command, shell, or script
pyscript Runs a python script file inside the console
quit Exits this application.
-set Sets a settable parameter or shows current settings of parameters.
+set Sets a settable parameter or shows current settings of parameters
shell Execute a command as if at the OS prompt.
shortcuts Lists shortcuts (aliases) available.
unalias Unsets aliases
"""
# Help text for the history command
-HELP_HISTORY = """usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c] [arg]
+HELP_HISTORY = """Usage: history [arg] [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c]
View, run, edit, save, or clear previously entered commands.
@@ -65,9 +65,9 @@ optional arguments:
-r, --run run selected history items
-e, --edit edit and then run selected history items
-s, --script script format; no separation lines
- -o FILE, --output-file FILE
+ -o, --output-file FILE
output commands to a script file
- -t TRANSCRIPT, --transcript TRANSCRIPT
+ -t, --transcript TRANSCRIPT
output commands and results to a transcript file
-c, --clear clears all history
"""
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index e0a71831..8aa26e0e 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -19,8 +19,8 @@ def cmd2_app():
SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}]
-Suggest command demonstrates argparse customizations See hybrid_suggest and
-orig_suggest to compare the help output.
+Suggest command demonstrates argparse customizations.
+See hybrid_suggest and orig_suggest to compare the help output.
required arguments:
-t, --type {movie, show}
@@ -59,7 +59,7 @@ def test_help_required_group(cmd2_app, capsys):
assert out1 == out2
assert out1[0].startswith('Usage: suggest')
assert out1[1] == ''
- assert out1[2].startswith('Suggest command demonstrates argparse customizations ')
+ assert out1[2].startswith('Suggest command demonstrates argparse customizations.')
assert out1 == normalize(SUGGEST_HELP)
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 13b53224..3aeb9959 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -62,7 +62,7 @@ def test_base_argparse_help(base_app, capsys):
out2 = run_cmd(base_app, 'help set')
assert out1 == out2
- assert out1[0].startswith('usage: set')
+ assert out1[0].startswith('Usage: set')
assert out1[1] == ''
assert out1[2].startswith('Sets a settable parameter')
@@ -71,10 +71,8 @@ def test_base_invalid_option(base_app, capsys):
out, err = capsys.readouterr()
out = normalize(out)
err = normalize(err)
- assert len(err) == 3
- assert len(out) == 15
assert 'Error: unrecognized arguments: -z' in err[0]
- assert out[0] == 'usage: set [-h] [-a] [-l] [settable [settable ...]]'
+ assert out[0] == 'Usage: set settable{0..2} [-h] [-a] [-l]'
def test_base_shortcuts(base_app):
out = run_cmd(base_app, 'shortcuts')
@@ -1252,7 +1250,7 @@ load Runs commands in script file that is encoded as either ASCII
py Invoke python command, shell, or script
pyscript Runs a python script file inside the console
quit Exits this application.
-set Sets a settable parameter or shows current settings of parameters.
+set Sets a settable parameter or shows current settings of parameters
shell Execute a command as if at the OS prompt.
shortcuts Lists shortcuts (aliases) available.
unalias Unsets aliases
@@ -1939,3 +1937,78 @@ def test_get_help_topics(base_app):
# Verify that the base app has no additional help_foo methods
custom_help = base_app.get_help_topics()
assert len(custom_help) == 0
+
+
+class ReplWithExitCode(cmd2.Cmd):
+ """ Example cmd2 application where we can specify an exit code when existing."""
+
+ def __init__(self):
+ super().__init__()
+
+ @cmd2.with_argument_list
+ def do_exit(self, arg_list) -> bool:
+ """Exit the application with an optional exit code.
+
+Usage: exit [exit_code]
+ Where:
+ * exit_code - integer exit code to return to the shell
+"""
+ # If an argument was provided
+ if arg_list:
+ try:
+ self.exit_code = int(arg_list[0])
+ except ValueError:
+ self.perror("{} isn't a valid integer exit code".format(arg_list[0]))
+ self.exit_code = -1
+
+ self._should_quit = True
+ return self._STOP_AND_EXIT
+
+ def postloop(self) -> None:
+ """Hook method executed once when the cmdloop() method is about to return."""
+ code = self.exit_code if self.exit_code is not None else 0
+ self.poutput('exiting with code: {}'.format(code))
+
+@pytest.fixture
+def exit_code_repl():
+ app = ReplWithExitCode()
+ return app
+
+def test_exit_code_default(exit_code_repl):
+ # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
+ app = exit_code_repl
+ app.use_rawinput = True
+ app.stdout = StdOut()
+
+ # Mock out the input call so we don't actually wait for a user's response on stdin
+ m = mock.MagicMock(name='input', return_value='exit')
+ builtins.input = m
+
+ # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ testargs = ["prog"]
+ expected = 'exiting with code: 0\n'
+ with mock.patch.object(sys, 'argv', testargs):
+ # Run the command loop
+ app.cmdloop()
+ out = app.stdout.buffer
+ assert out == expected
+
+def test_exit_code_nonzero(exit_code_repl):
+ # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
+ app = exit_code_repl
+ app.use_rawinput = True
+ app.stdout = StdOut()
+
+ # Mock out the input call so we don't actually wait for a user's response on stdin
+ m = mock.MagicMock(name='input', return_value='exit 23')
+ builtins.input = m
+
+ # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
+ testargs = ["prog"]
+ expected = 'exiting with code: 23\n'
+ with mock.patch.object(sys, 'argv', testargs):
+ # Run the command loop
+ with pytest.raises(SystemExit):
+ app.cmdloop()
+ out = app.stdout.buffer
+ assert out == expected
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index de4c637e..e1dfa982 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -33,8 +33,7 @@ def default_parser():
def test_parse_empty_string_default(default_parser):
statement = default_parser.parse('')
- assert not statement.command
- assert not statement.args
+ assert statement.command is None
assert statement == ''
assert statement.raw == ''
@@ -54,8 +53,7 @@ def test_tokenize_default(default_parser, line, tokens):
def test_parse_empty_string(parser):
statement = parser.parse('')
- assert not statement.command
- assert not statement.args
+ assert statement.command is None
assert statement == ''
assert statement.raw == ''
@@ -98,9 +96,9 @@ def test_command_and_args(parser, tokens, command, args):
def test_parse_single_word(parser, line):
statement = parser.parse(line)
assert statement.command == line
- assert statement.args is None
assert statement == ''
assert statement.argv == [utils.strip_quotes(line)]
+ assert not statement.arg_list
@pytest.mark.parametrize('line,terminator', [
('termbare;', ';'),
@@ -111,9 +109,9 @@ def test_parse_single_word(parser, line):
def test_parse_word_plus_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
- assert statement.args is None
assert statement == ''
assert statement.argv == ['termbare']
+ assert not statement.arg_list
assert statement.terminator == terminator
@pytest.mark.parametrize('line,terminator', [
@@ -125,9 +123,9 @@ def test_parse_word_plus_terminator(parser, line, terminator):
def test_parse_suffix_after_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
- assert statement.args is None
assert statement == ''
assert statement.argv == ['termbare']
+ assert not statement.arg_list
assert statement.terminator == terminator
assert statement.suffix == 'suffx'
@@ -135,72 +133,74 @@ def test_parse_command_with_args(parser):
line = 'command with args'
statement = parser.parse(line)
assert statement.command == 'command'
- assert statement.args == 'with args'
- assert statement == statement.args
+ assert statement == 'with args'
assert statement.argv == ['command', 'with', 'args']
+ assert statement.arg_list == statement.argv[1:]
def test_parse_command_with_quoted_args(parser):
line = 'command with "quoted args" and "some not"'
statement = parser.parse(line)
assert statement.command == 'command'
- assert statement.args == 'with "quoted args" and "some not"'
- assert statement == statement.args
+ assert statement == 'with "quoted args" and "some not"'
assert statement.argv == ['command', 'with', 'quoted args', 'and', 'some not']
+ assert statement.arg_list == ['with', '"quoted args"', 'and', '"some not"']
def test_parse_command_with_args_terminator_and_suffix(parser):
line = 'command with args and terminator; and suffix'
statement = parser.parse(line)
assert statement.command == 'command'
- assert statement.args == "with args and terminator"
+ assert statement == "with args and terminator"
assert statement.argv == ['command', 'with', 'args', 'and', 'terminator']
- assert statement == statement.args
+ assert statement.arg_list == statement.argv[1:]
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.args is None
assert 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.args is None
assert statement.argv == ['hi']
+ assert not statement.arg_list
assert statement == ''
- assert not statement.pipe_to
+ assert statement.pipe_to is None
def test_parse_c_comment_empty(parser):
statement = parser.parse('/* this is | all a comment */')
- assert not statement.command
- assert not statement.args
- assert not statement.pipe_to
+ assert statement.command is None
+ assert statement.pipe_to is None
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.args == '/tmp/*.txt'
- assert not statement.pipe_to
+ assert statement == '/tmp/*.txt'
+ assert statement.pipe_to is None
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.args == '/tmp/*.txt /tmp/*.cfg'
- assert not statement.pipe_to
+ assert statement == '/tmp/*.txt /tmp/*.cfg'
+ assert statement.pipe_to is None
assert statement.argv == ['cat', '/tmp/*.txt', '/tmp/*.cfg']
+ 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.args == 'if "quoted strings /* seem to " start comments?'
+ assert statement == 'if "quoted strings /* seem to " start comments?'
assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?']
- assert statement == statement.args
- assert not statement.pipe_to
+ assert statement.arg_list == ['if', '"quoted strings /* seem to "', 'start', 'comments?']
+ assert statement.pipe_to is None
@pytest.mark.parametrize('line',[
'simple | piped',
@@ -209,27 +209,27 @@ def test_parse_what_if_quoted_strings_seem_to_start_comments(parser):
def test_parse_simple_pipe(parser, line):
statement = parser.parse(line)
assert statement.command == 'simple'
- assert statement.args is None
assert statement == ''
assert statement.argv == ['simple']
+ assert not statement.arg_list
assert statement.pipe_to == ['piped']
def test_parse_double_pipe_is_not_a_pipe(parser):
line = 'double-pipe || is not a pipe'
statement = parser.parse(line)
assert statement.command == 'double-pipe'
- assert statement.args == '|| is not a pipe'
- assert statement == statement.args
+ assert statement == '|| is not a pipe'
assert statement.argv == ['double-pipe', '||', 'is', 'not', 'a', 'pipe']
- assert not statement.pipe_to
+ assert statement.arg_list == statement.argv[1:]
+ assert statement.pipe_to is None
def test_parse_complex_pipe(parser):
line = 'command with args, terminator&sufx | piped'
statement = parser.parse(line)
assert statement.command == 'command'
- assert statement.args == "with args, terminator"
+ assert statement == "with args, terminator"
assert statement.argv == ['command', 'with', 'args,', 'terminator']
- assert statement == statement.args
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == '&'
assert statement.suffix == 'sufx'
assert statement.pipe_to == ['piped']
@@ -243,7 +243,6 @@ def test_parse_complex_pipe(parser):
def test_parse_redirect(parser,line, output):
statement = parser.parse(line)
assert statement.command == 'help'
- assert statement.args is None
assert statement == ''
assert statement.output == output
assert statement.output_to == 'out.txt'
@@ -252,9 +251,9 @@ def test_parse_redirect_with_args(parser):
line = 'output into > afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
- assert statement.args == 'into'
- assert statement == statement.args
+ assert statement == 'into'
assert statement.argv == ['output', 'into']
+ assert statement.arg_list == statement.argv[1:]
assert statement.output == '>'
assert statement.output_to == 'afile.txt'
@@ -262,9 +261,9 @@ def test_parse_redirect_with_dash_in_path(parser):
line = 'output into > python-cmd2/afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
- assert statement.args == 'into'
- assert statement == statement.args
+ assert statement == 'into'
assert statement.argv == ['output', 'into']
+ assert statement.arg_list == statement.argv[1:]
assert statement.output == '>'
assert statement.output_to == 'python-cmd2/afile.txt'
@@ -272,9 +271,9 @@ def test_parse_redirect_append(parser):
line = 'output appended to >> /tmp/afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
- assert statement.args == 'appended to'
- assert statement == statement.args
+ assert statement == 'appended to'
assert statement.argv == ['output', 'appended', 'to']
+ assert statement.arg_list == statement.argv[1:]
assert statement.output == '>>'
assert statement.output_to == '/tmp/afile.txt'
@@ -282,22 +281,22 @@ def test_parse_pipe_and_redirect(parser):
line = 'output into;sufx | pipethrume plz > afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
- assert statement.args == 'into'
- assert statement == statement.args
+ assert statement == 'into'
assert statement.argv == ['output', 'into']
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == ';'
assert statement.suffix == 'sufx'
assert statement.pipe_to == ['pipethrume', 'plz', '>', 'afile.txt']
- assert not statement.output
- assert not statement.output_to
+ assert statement.output is None
+ assert statement.output_to is None
def test_parse_output_to_paste_buffer(parser):
line = 'output to paste buffer >> '
statement = parser.parse(line)
assert statement.command == 'output'
- assert statement.args == 'to paste buffer'
- assert statement == statement.args
+ assert statement == 'to paste buffer'
assert statement.argv == ['output', 'to', 'paste', 'buffer']
+ assert statement.arg_list == statement.argv[1:]
assert statement.output == '>>'
def test_parse_redirect_inside_terminator(parser):
@@ -307,9 +306,9 @@ def test_parse_redirect_inside_terminator(parser):
line = 'has > inside;'
statement = parser.parse(line)
assert statement.command == 'has'
- assert statement.args == '> inside'
- assert statement == statement.args
+ assert statement == '> inside'
assert statement.argv == ['has', '>', 'inside']
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == ';'
@pytest.mark.parametrize('line,terminator',[
@@ -325,9 +324,9 @@ def test_parse_redirect_inside_terminator(parser):
def test_parse_multiple_terminators(parser, line, terminator):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
- assert statement.args == 'with | inside'
- assert statement == statement.args
+ assert statement == 'with | inside'
assert statement.argv == ['multiline', 'with', '|', 'inside']
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == terminator
def test_parse_unfinished_multiliine_command(parser):
@@ -335,10 +334,10 @@ def test_parse_unfinished_multiliine_command(parser):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
- assert statement.args == 'has > inside an unfinished command'
- assert statement == statement.args
+ assert statement == 'has > inside an unfinished command'
assert statement.argv == ['multiline', 'has', '>', 'inside', 'an', 'unfinished', 'command']
- assert not statement.terminator
+ assert statement.arg_list == statement.argv[1:]
+ assert statement.terminator is None
@pytest.mark.parametrize('line,terminator',[
('multiline has > inside;', ';'),
@@ -350,9 +349,9 @@ def test_parse_unfinished_multiliine_command(parser):
def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, terminator):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
- assert statement.args == 'has > inside'
- assert statement == statement.args
+ assert statement == 'has > inside'
assert statement.argv == ['multiline', 'has', '>', 'inside']
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == terminator
def test_parse_multiline_with_incomplete_comment(parser):
@@ -362,8 +361,9 @@ def test_parse_multiline_with_incomplete_comment(parser):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
- assert statement.args == 'command /* with unclosed comment'
+ assert statement == 'command /* with unclosed comment'
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):
@@ -371,9 +371,9 @@ def test_parse_multiline_with_complete_comment(parser):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
- assert statement.args == 'command is done'
- assert statement == statement.args
+ assert statement == 'command is done'
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):
@@ -381,9 +381,9 @@ def test_parse_multiline_terminated_by_empty_line(parser):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
- assert statement.args == 'command ends'
- assert statement == statement.args
+ assert statement == 'command ends'
assert statement.argv == ['multiline', 'command', 'ends']
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == '\n'
@pytest.mark.parametrize('line,terminator',[
@@ -398,9 +398,9 @@ def test_parse_multiline_with_embedded_newline(parser, line, terminator):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
- assert statement.args == 'command "with\nembedded newline"'
- assert statement == statement.args
+ assert statement == 'command "with\nembedded newline"'
assert statement.argv == ['multiline', 'command', 'with\nembedded newline']
+ assert statement.arg_list == ['command', '"with\nembedded newline"']
assert statement.terminator == terminator
def test_parse_multiline_ignores_terminators_in_comments(parser):
@@ -408,34 +408,34 @@ def test_parse_multiline_ignores_terminators_in_comments(parser):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
- assert statement.args == 'command "with term; ends" now'
- assert statement == statement.args
+ assert statement == 'command "with term; ends" now'
assert statement.argv == ['multiline', 'command', 'with term; ends', 'now']
+ assert statement.arg_list == ['command', '"with term; ends"', 'now']
assert statement.terminator == '\n'
def test_parse_command_with_unicode_args(parser):
line = 'drink café'
statement = parser.parse(line)
assert statement.command == 'drink'
- assert statement.args == 'café'
- assert statement == statement.args
+ assert statement == 'café'
assert statement.argv == ['drink', 'café']
+ assert statement.arg_list == statement.argv[1:]
def test_parse_unicode_command(parser):
line = 'café au lait'
statement = parser.parse(line)
assert statement.command == 'café'
- assert statement.args == 'au lait'
- assert statement == statement.args
+ assert statement == 'au lait'
assert statement.argv == ['café', 'au', 'lait']
+ assert statement.arg_list == statement.argv[1:]
def test_parse_redirect_to_unicode_filename(parser):
line = 'dir home > café'
statement = parser.parse(line)
assert statement.command == 'dir'
- assert statement.args == 'home'
- assert statement == statement.args
+ assert statement == 'home'
assert statement.argv == ['dir', 'home']
+ assert statement.arg_list == statement.argv[1:]
assert statement.output == '>'
assert statement.output_to == 'café'
@@ -452,9 +452,9 @@ def test_empty_statement_raises_exception():
app._complete_statement(' ')
@pytest.mark.parametrize('line,command,args', [
- ('helpalias', 'help', None),
+ ('helpalias', 'help', ''),
('helpalias mycommand', 'help', 'mycommand'),
- ('42', 'theanswer', None),
+ ('42', 'theanswer', ''),
('42 arg1 arg2', 'theanswer', 'arg1 arg2'),
('!ls', 'shell', 'ls'),
('!ls -al /tmp', 'shell', 'ls -al /tmp'),
@@ -463,20 +463,15 @@ def test_empty_statement_raises_exception():
def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
statement = parser.parse(line)
assert statement.command == command
- assert statement.args == args
- if statement.args is None:
- assert statement == ''
- else:
- assert statement == statement.args
+ assert statement == args
def test_parse_alias_on_multiline_command(parser):
line = 'anothermultiline has > inside an unfinished command'
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.command == 'multiline'
- assert statement.args == 'has > inside an unfinished command'
- assert statement == statement.args
- assert not statement.terminator
+ assert statement == 'has > inside an unfinished command'
+ assert statement.terminator is None
@pytest.mark.parametrize('line,output', [
('helpalias > out.txt', '>'),
@@ -487,7 +482,6 @@ def test_parse_alias_on_multiline_command(parser):
def test_parse_alias_redirection(parser, line, output):
statement = parser.parse(line)
assert statement.command == 'help'
- assert statement.args is None
assert statement == ''
assert statement.output == output
assert statement.output_to == 'out.txt'
@@ -499,7 +493,6 @@ def test_parse_alias_redirection(parser, line, output):
def test_parse_alias_pipe(parser, line):
statement = parser.parse(line)
assert statement.command == 'help'
- assert statement.args is None
assert statement == ''
assert statement.pipe_to == ['less']
@@ -514,7 +507,6 @@ def test_parse_alias_pipe(parser, line):
def test_parse_alias_terminator_no_whitespace(parser, line):
statement = parser.parse(line)
assert statement.command == 'help'
- assert statement.args is None
assert statement == ''
assert statement.terminator == ';'
@@ -522,8 +514,7 @@ def test_parse_command_only_command_and_args(parser):
line = 'help history'
statement = parser.parse_command_only(line)
assert statement.command == 'help'
- assert statement.args == 'history'
- assert statement == statement.args
+ assert statement == 'history'
assert statement.command_and_args == line
def test_parse_command_only_emptyline(parser):
@@ -533,40 +524,36 @@ def test_parse_command_only_emptyline(parser):
# should be '', to retain backwards compatibility with
# the cmd in the standard library
assert statement.command is None
- assert statement.args is None
assert statement == ''
assert not statement.argv
- assert statement.command_and_args == None
+ assert not statement.arg_list
+ assert statement.command_and_args is None
def test_parse_command_only_strips_line(parser):
line = ' help history '
statement = parser.parse_command_only(line)
assert statement.command == 'help'
- assert statement.args == 'history'
- assert statement == statement.args
+ assert statement == 'history'
assert statement.command_and_args == line.strip()
def test_parse_command_only_expands_alias(parser):
line = 'fake foobar.py'
statement = parser.parse_command_only(line)
assert statement.command == 'pyscript'
- assert statement.args == 'foobar.py'
- assert statement == statement.args
+ assert statement == 'foobar.py'
def test_parse_command_only_expands_shortcuts(parser):
line = '!cat foobar.txt'
statement = parser.parse_command_only(line)
assert statement.command == 'shell'
- assert statement.args == 'cat foobar.txt'
- assert statement == statement.args
+ assert statement == 'cat foobar.txt'
assert statement.command_and_args == 'shell cat foobar.txt'
def test_parse_command_only_quoted_args(parser):
line = 'l "/tmp/directory with spaces/doit.sh"'
statement = parser.parse_command_only(line)
assert statement.command == 'shell'
- assert statement.args == 'ls -al "/tmp/directory with spaces/doit.sh"'
- assert statement == statement.args
+ assert statement == 'ls -al "/tmp/directory with spaces/doit.sh"'
assert statement.command_and_args == line.replace('l', 'shell ls -al')
@pytest.mark.parametrize('line', [
@@ -598,7 +585,6 @@ def test_parse_command_only_specialchars(parser, line):
def test_parse_command_only_none(parser, line):
statement = parser.parse_command_only(line)
assert statement.command is None
- assert statement.args is None
assert statement == ''
def test_parse_command_only_multiline(parser):
@@ -606,8 +592,7 @@ def test_parse_command_only_multiline(parser):
statement = parser.parse_command_only(line)
assert statement.command == 'multiline'
assert statement.multiline_command == 'multiline'
- assert statement.args == 'with partially "open quotes and no terminator'
- assert statement == statement.args
+ assert statement == 'with partially "open quotes and no terminator'
assert statement.command_and_args == line
@@ -617,7 +602,7 @@ def test_statement_initialization(parser):
assert string == statement
assert statement.raw is None
assert statement.command is None
- assert statement.args is None
+ assert isinstance(statement.arg_list, list)
assert isinstance(statement.argv, list)
assert not statement.argv
assert statement.multiline_command is None