summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md3
-rwxr-xr-xREADME.md6
-rw-r--r--azure-pipelines.yml56
-rw-r--r--cmd2/cmd2.py140
-rw-r--r--cmd2/parsing.py278
-rw-r--r--docs/argument_processing.rst43
-rwxr-xr-xsetup.py15
-rw-r--r--tests/conftest.py16
-rw-r--r--tests/test_cmd2.py21
-rw-r--r--tests/test_parsing.py398
-rw-r--r--tox.ini87
11 files changed, 628 insertions, 435 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f201664..8d49d34e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
-## 0.9.5 (TBD, 2018)
+## 0.9.5 (September TBD, 2018)
* Bug Fixes
* Fixed bug where ``get_all_commands`` could return non-callable attributes
+ * Fixed bug where **alias** command was dropping quotes around arguments
* Enhancements
* Added ``exit_code`` attribute of ``cmd2.Cmd`` class
* Enables applications to return a non-zero exit code when exiting from ``cmdloop``
diff --git a/README.md b/README.md
index b2eb314c..8cb356a2 100755
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ cmd2: a tool for building interactive command line apps
[![Latest Version](https://img.shields.io/pypi/v/cmd2.svg?style=flat-square&label=latest%20stable%20version)](https://pypi.python.org/pypi/cmd2/)
[![Build status](https://img.shields.io/travis/python-cmd2/cmd2.svg?style=flat-square&label=unix%20build)](https://travis-ci.org/python-cmd2/cmd2)
[![Appveyor build status](https://img.shields.io/appveyor/ci/FedericoCeratto/cmd2.svg?style=flat-square&label=windows%20build)](https://ci.appveyor.com/project/FedericoCeratto/cmd2)
-[![VSTS Build status](https://python-cmd2.visualstudio.com/cmd2/_apis/build/status/cmd2-Python%20package-CI?branch=master)](https://python-cmd2.visualstudio.com/cmd2/_build/latest?definitionId=1&branch=master)
+[![Azure Build status](https://python-cmd2.visualstudio.com/cmd2/_apis/build/status/python-cmd2.cmd2?branch=master)](https://python-cmd2.visualstudio.com/cmd2/_build/latest?definitionId=1&branch=master)
[![codecov](https://codecov.io/gh/python-cmd2/cmd2/branch/master/graph/badge.svg)](https://codecov.io/gh/python-cmd2/cmd2)
[![Documentation Status](https://readthedocs.org/projects/cmd2/badge/?version=latest)](http://cmd2.readthedocs.io/en/latest/?badge=latest)
@@ -43,9 +43,9 @@ Main Features
Python 2.7 support is EOL
-------------------------
-Support for adding new features to the Python 2.7 release of ``cmd2`` was discontinued on April 15, 2018. Bug fixes will be supported for Python 2.7 via 0.8.x until August 31, 2018.
+The last version of cmd2 to support Python 2.7 is [0.8.9](https://pypi.org/project/cmd2/0.8.9/), released on August 21, 2018.
-Supporting Python 2 was an increasing burden on our limited resources. Switching to support only Python 3 will allow
+Supporting Python 2 was an increasing burden on our limited resources. Switching to support only Python 3 is allowing
us to clean up the codebase, remove some cruft, and focus on developing new features.
Installation
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 00000000..374c6b3b
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,56 @@
+# Python package
+# Create and test a Python package on multiple Python versions.
+# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
+# https://docs.microsoft.com/vsts/pipelines/languages/python
+
+jobs:
+
+- job: 'Test'
+
+ # Configure Build Environment to use Azure Pipelines to build Python project using macOS
+ pool:
+ vmImage: 'macOS 10.13' # other options 'Ubuntu 16.04', 'VS2017-Win2016'
+
+ # Run the pipeline with multiple Python versions
+ strategy:
+ matrix:
+ Python34:
+ python.version: '3.4'
+ Python35:
+ python.version: '3.5'
+ Python36:
+ python.version: '3.6'
+ Python37:
+ python.version: '3.7'
+ # Increase the maxParallel value to simultaneously run the job for all versions in the matrix (max 10 for free open-source)
+ maxParallel: 4
+
+ steps:
+ # Set the UsePythonVersion task to reference the matrix variable for its Python version
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '$(python.version)'
+ architecture: 'x64'
+
+ # Install dependencies - install specific PyPI packages with pip, including cmd2 dependencies
+ - script: |
+ python -m pip install --upgrade pip && pip3 install --upgrade setuptools gnureadline
+ pip install -e .
+ displayName: 'Upgrade pip and setuptools'
+ continueOnError: false
+
+ # TODO: Consider adding a lint test to use pycodestyle, flake8, or pylint, to check code style conventions
+
+ # Test - test with pytest, collect coverage metrics with pytest-cov, and publish these metrics to codecov.io
+ - script: |
+ pip install pytest pytest-cov pytest-mock codecov mock
+ py.test tests --cov --junitxml=junit/test-results.xml && codecov
+ displayName: 'Run tests and code coverage'
+ continueOnError: false
+
+ # Publish test results to the Azure DevOps server
+ - task: PublishTestResults@2
+ inputs:
+ testResultsFiles: '**/test-*.xml'
+ testRunTitle: 'Python $(python.version)'
+ condition: succeededOrFailed()
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 37b0d2d9..136328b1 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -629,7 +629,7 @@ class Cmd(cmd.Cmd):
- truncated text is still accessible by scrolling with the right & left arrow keys
- chopping is ideal for displaying wide tabular data as is done in utilities like pgcli
False -> causes lines longer than the screen width to wrap to the next line
- - wrapping is ideal when you want to avoid users having to use horizontal scrolling
+ - wrapping is ideal when you want to keep users from having to use horizontal scrolling
WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
"""
@@ -709,8 +709,7 @@ class Cmd(cmd.Cmd):
elif rl_type == RlType.PYREADLINE:
readline.rl.mode._display_completions = self._display_matches_pyreadline
- def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[Optional[List[str]],
- Optional[List[str]]]:
+ def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]:
"""
Used by tab completion functions to get all tokens through the one being completed
:param line: the current input line with leading whitespace removed
@@ -727,7 +726,7 @@ class Cmd(cmd.Cmd):
The last item in both lists is the token being tab completed
On Failure
- Both items are None
+ Two empty lists
"""
import copy
unclosed_quote = ''
@@ -747,21 +746,21 @@ class Cmd(cmd.Cmd):
if not unclosed_quote and begidx == tmp_endidx:
initial_tokens.append('')
break
- except ValueError:
- # ValueError can be caused by missing closing quote
- if not quotes_to_try:
- # Since we have no more quotes to try, something else
- # is causing the parsing error. Return None since
- # this means the line is malformed.
- return None, None
-
- # Add a closing quote and try to parse again
- unclosed_quote = quotes_to_try[0]
- quotes_to_try = quotes_to_try[1:]
-
- tmp_line = line[:endidx]
- tmp_line += unclosed_quote
- tmp_endidx = endidx + 1
+ except ValueError as ex:
+ # Make sure the exception was due to an unclosed quote and
+ # we haven't exhausted the closing quotes to try
+ if str(ex) == "No closing quotation" and quotes_to_try:
+ # Add a closing quote and try to parse again
+ unclosed_quote = quotes_to_try[0]
+ quotes_to_try = quotes_to_try[1:]
+
+ tmp_line = line[:endidx]
+ tmp_line += unclosed_quote
+ tmp_endidx = endidx + 1
+ else:
+ # The parsing error is not caused by unclosed quotes.
+ # Return empty lists since this means the line is malformed.
+ return [], []
if self.allow_redirection:
@@ -927,7 +926,7 @@ class Cmd(cmd.Cmd):
"""
# Get all tokens through the one being completed
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- if tokens is None:
+ if not tokens:
return []
completions_matches = []
@@ -970,7 +969,7 @@ class Cmd(cmd.Cmd):
"""
# Get all tokens through the one being completed
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- if tokens is None:
+ if not tokens:
return []
matches = []
@@ -1207,7 +1206,7 @@ class Cmd(cmd.Cmd):
# Get all tokens through the one being completed. We want the raw tokens
# so we can tell if redirection strings are quoted and ignore them.
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
- if raw_tokens is None:
+ if not raw_tokens:
return []
if len(raw_tokens) > 1:
@@ -1415,9 +1414,9 @@ class Cmd(cmd.Cmd):
# Get all tokens through the one being completed
tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
- # Either had a parsing error or are trying to complete the command token
+ # Check if we either had a parsing error or are trying to complete the command token
# The latter can happen if " or ' was entered as the command
- if tokens is None or len(tokens) == 1:
+ if len(tokens) <= 1:
self.completion_matches = []
return None
@@ -1567,9 +1566,10 @@ class Cmd(cmd.Cmd):
completer = AutoCompleter(argparser, cmd2_app=self)
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- results = completer.complete_command(tokens, text, line, begidx, endidx)
+ if not tokens:
+ return []
- return results
+ return completer.complete_command(tokens, text, line, begidx, endidx)
def get_all_commands(self) -> List[str]:
"""Returns a list of all commands."""
@@ -1606,7 +1606,7 @@ class Cmd(cmd.Cmd):
# Get all tokens through the one being completed
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
- if tokens is None:
+ if not tokens:
return []
matches = []
@@ -1718,12 +1718,6 @@ class Cmd(cmd.Cmd):
:param stop: bool - True implies the entire application should exit.
:return: bool - True implies the entire application should exit.
"""
- if not sys.platform.startswith('win'):
- # Fix those annoying problems that occur with terminal programs like "less" when you pipe to them
- if self.stdin.isatty():
- import subprocess
- proc = subprocess.Popen(shlex.split('stty sane'))
- proc.communicate()
return stop
def parseline(self, line: str) -> Tuple[str, str, str]:
@@ -1818,6 +1812,14 @@ class Cmd(cmd.Cmd):
def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) -> bool:
"""Run the command finalization hooks"""
+
+ if not sys.platform.startswith('win'):
+ # Fix those annoying problems that occur with terminal programs like "less" when you pipe to them
+ if self.stdin.isatty():
+ import subprocess
+ proc = subprocess.Popen(shlex.split('stty sane'))
+ proc.communicate()
+
try:
data = plugin.CommandFinalizationData(stop, statement)
for func in self._cmdfinalization_hooks:
@@ -2230,8 +2232,7 @@ class Cmd(cmd.Cmd):
return stop
- @with_argument_list
- def do_alias(self, arglist: List[str]) -> None:
+ def do_alias(self, statement: Statement) -> None:
"""Define or display aliases
Usage: Usage: alias [name] | [<name> <value>]
@@ -2250,22 +2251,27 @@ Usage: Usage: alias [name] | [<name> <value>]
Example: alias ls !ls -lF
- If you want to use redirection or pipes in the alias, then either quote the tokens with these
- characters or quote the entire alias value.
+ If you want to use redirection or pipes in the alias, then quote them to prevent
+ the alias command itself from being redirected
Examples:
alias save_results print_results ">" out.txt
- alias save_results print_results "> out.txt"
- alias save_results "print_results > out.txt"
+ alias save_results print_results '>' out.txt
"""
+ # Get alias arguments as a list with quotes preserved
+ alias_arg_list = statement.arg_list
+
# If no args were given, then print a list of current aliases
- if not arglist:
+ if not alias_arg_list:
for cur_alias in self.aliases:
self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
+ return
+
+ # Get the alias name
+ name = alias_arg_list[0]
# The user is looking up an alias
- elif len(arglist) == 1:
- name = arglist[0]
+ if len(alias_arg_list) == 1:
if name in self.aliases:
self.poutput("alias {} {}".format(name, self.aliases[name]))
else:
@@ -2273,8 +2279,16 @@ Usage: Usage: alias [name] | [<name> <value>]
# The user is creating an alias
else:
- name = arglist[0]
- value = ' '.join(arglist[1:])
+ # Unquote redirection and pipes
+ index = 1
+ while index < len(alias_arg_list):
+ unquoted_arg = utils.strip_quotes(alias_arg_list[index])
+ if unquoted_arg in constants.REDIRECTION_TOKENS:
+ alias_arg_list[index] = unquoted_arg
+ index += 1
+
+ # Build the alias value string
+ value = ' '.join(alias_arg_list[1:])
# Validate the alias to ensure it doesn't include weird characters
# like terminators, output redirection, or whitespace
@@ -2334,7 +2348,7 @@ Usage: Usage: unalias [-a] name [name ...]
@with_argument_list
def do_help(self, arglist: List[str]) -> None:
- """List available commands with "help" or detailed help with "help cmd"."""
+ """ List available commands with "help" or detailed help with "help cmd" """
if not arglist or (len(arglist) == 1 and arglist[0] in ('--verbose', '-v')):
verbose = len(arglist) == 1 and arglist[0] in ('--verbose', '-v')
self._help_menu(verbose)
@@ -2473,22 +2487,22 @@ Usage: Usage: unalias [-a] name [name ...]
self.stdout.write("\n")
def do_shortcuts(self, _: str) -> None:
- """Lists shortcuts (aliases) available."""
+ """Lists shortcuts available"""
result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.shortcuts))
self.poutput("Shortcuts for other commands:\n{}\n".format(result))
def do_eof(self, _: str) -> bool:
- """Called when <Ctrl>-D is pressed."""
+ """Called when <Ctrl>-D is pressed"""
# End of script should not exit app, but <Ctrl>-D should.
return self._STOP_AND_EXIT
def do_quit(self, _: str) -> bool:
- """Exits this application."""
+ """Exits this application"""
self._should_quit = True
return self._STOP_AND_EXIT
def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str='Your choice? ') -> str:
- """Presents a numbered menu to the user. Modelled after
+ """Presents a numbered menu to the user. Modeled after
the bash shell's SELECT. Returns the item chosen.
Argument ``opts`` can be:
@@ -2611,24 +2625,20 @@ Usage: Usage: unalias [-a] name [name ...]
param = args.settable[0]
self.show(args, param)
- def do_shell(self, command: str) -> None:
- """Execute a command as if at the OS prompt.
+ def do_shell(self, statement: Statement) -> None:
+ """Execute a command as if at the OS prompt
Usage: shell <command> [arguments]"""
-
import subprocess
- try:
- # Use non-POSIX parsing to keep the quotes around the tokens
- tokens = shlex.split(command, posix=False)
- except ValueError as err:
- self.perror(err, traceback_war=False)
- return
+
+ # Get list of arguments to shell with quotes preserved
+ tokens = statement.arg_list
# Support expanding ~ in quoted paths
for index, _ in enumerate(tokens):
if tokens[index]:
- # Check if the token is quoted. Since shlex.split() passed, there isn't
- # an unclosed quote, so we only need to check the first character.
+ # Check if the token is quoted. Since parsing already passed, there isn't
+ # an unclosed quote. So we only need to check the first character.
first_char = tokens[index][0]
if first_char in constants.QUOTES:
tokens[index] = utils.strip_quotes(tokens[index])
@@ -2914,7 +2924,7 @@ a..b, a:b, a:, ..b items by indices (inclusive)
@with_argparser(history_parser)
def do_history(self, args: argparse.Namespace) -> None:
- """View, run, edit, save, or clear previously entered commands."""
+ """View, run, edit, save, or clear previously entered commands"""
if args.clear:
# Clear command and readline history
@@ -3058,7 +3068,7 @@ a..b, a:b, a:, ..b items by indices (inclusive)
@with_argument_list
def do_edit(self, arglist: List[str]) -> None:
- """Edit a file in a text editor.
+ """Edit a file in a text editor
Usage: edit [file_path]
Where:
@@ -3090,7 +3100,7 @@ The editor used is determined by the ``editor`` settable parameter.
@with_argument_list
def do__relative_load(self, arglist: List[str]) -> None:
- """Runs commands in script file that is encoded as either ASCII or UTF-8 text.
+ """Runs commands in script file that is encoded as either ASCII or UTF-8 text
Usage: _relative_load <file_path>
@@ -3115,13 +3125,13 @@ NOTE: This command is intended to only be used within text file scripts.
self.do_load([relative_path])
def do_eos(self, _: str) -> None:
- """Handles cleanup when a script has finished executing."""
+ """Handles cleanup when a script has finished executing"""
if self._script_dir:
self._script_dir.pop()
@with_argument_list
def do_load(self, arglist: List[str]) -> None:
- """Runs commands in script file that is encoded as either ASCII or UTF-8 text.
+ """Runs commands in script file that is encoded as either ASCII or UTF-8 text
Usage: load <file_path>
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index b67cef10..8edfacb9 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -7,10 +7,13 @@ import re
import shlex
from typing import List, Tuple, Dict
+import attr
+
from . import constants
from . import utils
+@attr.s(frozen=True)
class Statement(str):
"""String subclass with additional attributes to store the results of parsing.
@@ -26,98 +29,137 @@ class Statement(str):
The string portion of the class contains the arguments, but not the command, nor
the output redirection clauses.
- :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
- :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
- redirection or terminators. quoted arguments remain
- quoted.
- :type args: str or None
- :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
- :var suffix: characters appearing after the terminator but before output
- redirection, if any
- :type suffix: str or None
- :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
- :var output_to: if output was redirected, the destination file
- :type output_to: str or None
-
+ Here's some suggestions and best practices for how to use the attributes of this
+ object:
+
+ command - the name of the command, shortcuts and aliases have already been
+ expanded
+
+ args - the arguments to the command, excluding output redirection and command
+ terminators. If the user used quotes in their input, they remain here,
+ and you will have to handle them on your own.
+
+ arg_list - the arguments to the command, excluding output redirection and
+ command terminators. Each argument is represented as an element
+ in the list. Quoted arguments remain quoted. If you want to
+ remove the quotes, use `cmd2.utils.strip_quotes()` or use
+ `argv[1:]`
+
+ command_and_args - join the args and the command together with a space. Output
+ redirection is excluded.
+
+ argv - this is a list of arguments in the style of `sys.argv`. The first element
+ of the list is the command. Subsequent elements of the list contain any
+ additional arguments, with quotes removed, just like bash would. This
+ is very useful if you are going to use `argparse.parse_args()`:
+ ```
+ def do_mycommand(stmt):
+ mycommand_argparser.parse_args(stmt.argv)
+ ...
+ ```
+
+ raw - if you want full access to exactly what the user typed at the input prompt
+ you can get it, but you'll have to parse it on your own, including:
+ - shortcuts and aliases
+ - quoted commands and arguments
+ - output redirection
+ - multi-line command terminator handling
+ if you use multiline commands, all the input will be passed to you in
+ this string, but there will be embedded newlines where
+ the user hit return to continue the command on the next line.
+
+ Tips:
+
+ 1. `argparse` is your friend for anything complex. `cmd2` has two decorators
+ (`with_argparser`, and `with_argparser_and_unknown_args`) which you can use
+ to make your command method receive a namespace of parsed arguments, whether
+ positional or denoted with switches.
+
+ 2. For commands with simple positional arguments, use `args` or `arg_list`
+
+ 3. If you don't want to have to worry about quoted arguments, use
+ argv[1:], which strips them all off for you.
"""
- def __new__(cls,
- obj: object,
- *,
- raw: str = None,
- command: str = None,
- args: 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
- ):
- """Create a new instance of Statement
+ # the arguments, but not the command, nor the output redirection clauses.
+ args = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ # string containing exactly what we input by the user
+ raw = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ # the command, i.e. the first whitespace delimited word
+ command = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ # list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted
+ arg_list = attr.ib(factory=list, validator=attr.validators.instance_of(list), type=List[str])
+
+ # if the command is a multiline command, the name of the command, otherwise empty
+ multiline_command = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ # the character which terminated the multiline command, if there was one
+ terminator = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ # characters appearing after the terminator but before output redirection, if any
+ suffix = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ # if output was piped to a shell command, the shell command as a list of tokens
+ pipe_to = attr.ib(factory=list, validator=attr.validators.instance_of(list), type=List[str])
+
+ # if output was redirected, the redirection token, i.e. '>>'
+ output = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ # if output was redirected, the destination file
+ output_to = attr.ib(default='', validator=attr.validators.instance_of(str), type=str)
+
+ def __new__(cls, value: object, *pos_args, **kw_args):
+ """Create a new instance of Statement.
We must override __new__ because we are subclassing `str` which is
- immutable.
+ immutable and takes a different number of arguments as Statement.
+
+ NOTE: attrs takes care of initializing other members in the __init__ it
+ generates.
"""
- stmt = str.__new__(cls, obj)
- object.__setattr__(stmt, "raw", raw)
- object.__setattr__(stmt, "command", command)
- object.__setattr__(stmt, "args", args)
- 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)
- object.__setattr__(stmt, "pipe_to", pipe_to)
- object.__setattr__(stmt, "output", output)
- object.__setattr__(stmt, "output_to", output_to)
+ stmt = super().__new__(cls, value)
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.
+ Quoted arguments remain quoted. Output redirection and piping are
+ excluded, as are any multiline command terminators.
"""
if self.command and self.args:
rtn = '{} {}'.format(self.command, self.args)
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
+ rtn = ''
return rtn
- def __setattr__(self, name, value):
- """Statement instances should feel immutable; raise ValueError"""
- raise ValueError
+ @property
+ def argv(self) -> List[str]:
+ """a list of arguments a la sys.argv.
- def __delattr__(self, name):
- """Statement instances should feel immutable; raise ValueError"""
- raise ValueError
+ 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
class StatementParser:
"""Parse raw text into command components.
- Shortcuts is a list of tuples with each tuple containing the shortcut and the expansion.
+ Shortcuts is a list of tuples with each tuple containing the shortcut and
+ the expansion.
"""
def __init__(
self,
@@ -231,7 +273,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]:
@@ -268,13 +310,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)
@@ -302,8 +344,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:]
@@ -315,7 +357,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
@@ -336,11 +378,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
@@ -374,26 +416,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,
- args=args,
- argv=list(map(lambda x: utils.strip_quotes(x), argv)),
+ arg_list=arg_list,
multiline_command=multiline_command,
terminator=terminator,
suffix=suffix,
@@ -413,53 +452,50 @@ class StatementParser:
This method is used by tab completion code and therefore must not
generate an exception if there are unclosed quotes.
- The Statement object returned by this method can at most contained
- values in the following attributes:
+ The `Statement` object returned by this method can at most contain values
+ in the following attributes:
+ - args
- raw
- command
- - args
+ - multiline_command
+
+ `Statement.args` includes all output redirection clauses and command
+ terminators.
Different from parse(), this method does not remove redundant whitespace
- within statement.args. It does however, ensure args does not have
- leading or trailing whitespace.
+ within args. However, it does ensure args has no leading or trailing
+ whitespace.
"""
# 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 wierd like '>'. args should be None if we couldn't
+
+ # take everything from the end of the first match group to
+ # the end of the line as the arguments (stripping leading
+ # and trailing spaces)
+ args = line[match.end(1):].strip()
+ # 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,
- args=args,
multiline_command=multiline_command,
)
return statement
@@ -503,12 +539,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]
@@ -528,10 +561,11 @@ class StatementParser:
return matched_string
def _split_on_punctuation(self, tokens: List[str]) -> List[str]:
- """
- # Further splits tokens from a command line using punctuation characters
- # as word breaks when they are in unquoted strings. Each run of punctuation
- # characters is treated as a single token.
+ """Further splits tokens from a command line using punctuation characters
+
+ Punctuation characters are treated as word breaks when they are in
+ unquoted strings. Each run of punctuation characters is treated as a
+ single token.
:param tokens: the tokens as parsed by shlex
:return: the punctuated tokens
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
index 5aef3720..8aed7498 100644
--- a/docs/argument_processing.rst
+++ b/docs/argument_processing.rst
@@ -278,16 +278,16 @@ the help categories with per-command Help Messages::
================================================================================
alias Define or display aliases
config Config command
- edit Edit a file in a text editor.
- help List available commands with "help" or detailed help with "help cmd".
+ edit Edit a file in a text editor
+ help List available commands with "help" or detailed help with "help cmd"
history usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg]
- load Runs commands in script file that is encoded as either ASCII or UTF-8 text.
+ load Runs commands in script file that is encoded as either ASCII or UTF-8 text
py Invoke python command, shell, or script
pyscript Runs a python script file inside the console
- quit Exits this application.
+ quit Exits this application
set usage: set [-h] [-a] [-l] [settable [settable ...]]
- shell Execute a command as if at the OS prompt.
- shortcuts Lists shortcuts (aliases) available.
+ shell Execute a command as if at the OS prompt
+ shortcuts Lists shortcuts available
unalias Unsets aliases
version Version command
@@ -296,7 +296,36 @@ Receiving an argument list
==========================
The default behavior of ``cmd2`` is to pass the user input directly to your
-``do_*`` methods as a string. If you don't want to use the full argument parser support outlined above, you can still have ``cmd2`` apply shell parsing rules to the user input and pass you a list of arguments instead of a string. Apply the ``@with_argument_list`` decorator to those methods that should receive an argument list instead of a string::
+``do_*`` methods as a string. The object passed to your method is actually a
+``Statement`` object, which has additional attributes that may be helpful,
+including ``arg_list`` and ``argv``::
+
+ class CmdLineApp(cmd2.Cmd):
+ """ Example cmd2 application. """
+
+ def do_say(self, statement):
+ # statement contains a string
+ self.poutput(statement)
+
+ def do_speak(self, statement):
+ # statement also has a list of arguments
+ # quoted arguments remain quoted
+ for arg in statement.arg_list:
+ self.poutput(arg)
+
+ def do_articulate(self, statement):
+ # statement.argv contains the command
+ # and the arguments, which have had quotes
+ # stripped
+ for arg in statement.argv:
+ self.poutput(arg)
+
+
+If you don't want to access the additional attributes on the string passed to
+you``do_*`` method you can still have ``cmd2`` apply shell parsing rules to the
+user input and pass you a list of arguments instead of a string. Apply the
+``@with_argument_list`` decorator to those methods that should receive an
+argument list instead of a string::
from cmd2 import with_argument_list
diff --git a/setup.py b/setup.py
index 094229e8..53645eff 100755
--- a/setup.py
+++ b/setup.py
@@ -70,13 +70,14 @@ EXTRAS_REQUIRE = {
":sys_platform!='win32'": ['wcwidth'],
# Python 3.4 and earlier require contextlib2 for temporarily redirecting stderr and stdout
":python_version<'3.5'": ['contextlib2', 'typing'],
- # development only dependencies
- # install with 'pip install -e .[dev]'
- 'dev': [
- # for python 3.5 and earlier we need the third party mock module
- "mock ; python_version<'3.6'",
- 'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'pylint',
- 'sphinx<1.7.7', 'sphinx-rtd-theme', 'sphinx-autobuild', 'invoke', 'twine>=1.11',
+ # Extra dependencies for running unit tests
+ 'test': ["argcomplete ; sys_platform!='win32'", # include argcomplete tests where available
+ "mock ; python_version<'3.6'", # for python 3.5 and earlier we need the third party mock module
+ 'codecov', 'pytest', 'pytest-cov', 'pytest-mock'],
+ # development only dependencies: install with 'pip install -e .[dev]'
+ 'dev': ["mock ; python_version<'3.6'", # for python 3.5 and earlier we need the third party mock module
+ 'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'pylint',
+ 'sphinx', 'sphinx-rtd-theme', 'sphinx-autobuild', 'invoke', 'twine>=1.11',
]
}
diff --git a/tests/conftest.py b/tests/conftest.py
index fb049a8c..c86748e8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -35,23 +35,23 @@ BASE_HELP_VERBOSE = """
Documented commands (type help <topic>):
================================================================================
alias Define or display aliases
-edit Edit a file in a text editor.
-help List available commands with "help" or detailed help with "help cmd".
-history View, run, edit, save, or clear previously entered commands.
-load Runs commands in script file that is encoded as either ASCII or UTF-8 text.
+edit Edit a file in a text editor
+help List available commands with "help" or detailed help with "help cmd"
+history View, run, edit, save, or clear previously entered commands
+load Runs commands in script file that is encoded as either ASCII or UTF-8 text
py Invoke python command, shell, or script
pyscript Runs a python script file inside the console
-quit Exits this application.
+quit Exits this application
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.
+shell Execute a command as if at the OS prompt
+shortcuts Lists shortcuts available
unalias Unsets aliases
"""
# Help text for the history command
HELP_HISTORY = """Usage: history [arg] [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c]
-View, run, edit, save, or clear previously entered commands.
+View, run, edit, save, or clear previously entered commands
positional arguments:
arg empty all history items
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 6fb64b86..e2a3d854 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1235,15 +1235,15 @@ diddly This command does diddly
Other
================================================================================
alias Define or display aliases
-help List available commands with "help" or detailed help with "help cmd".
-history View, run, edit, save, or clear previously entered commands.
-load Runs commands in script file that is encoded as either ASCII or UTF-8 text.
+help List available commands with "help" or detailed help with "help cmd"
+history View, run, edit, save, or clear previously entered commands
+load Runs commands in script file that is encoded as either ASCII or UTF-8 text
py Invoke python command, shell, or script
pyscript Runs a python script file inside the console
-quit Exits this application.
+quit Exits this application
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.
+shell Execute a command as if at the OS prompt
+shortcuts Lists shortcuts available
unalias Unsets aliases
Undocumented commands:
@@ -1750,6 +1750,15 @@ def test_alias(base_app, capsys):
out = run_cmd(base_app, 'alias fake')
assert out == normalize('alias fake pyscript')
+def test_alias_with_quotes(base_app, capsys):
+ # Create the alias
+ out = run_cmd(base_app, 'alias fake help ">" "out file.txt"')
+ assert out == normalize("Alias 'fake' created")
+
+ # Lookup the new alias (Only the redirector should be unquoted)
+ out = run_cmd(base_app, 'alias fake')
+ assert out == normalize('alias fake help > "out file.txt"')
+
def test_alias_lookup_invalid_alias(base_app, capsys):
# Lookup invalid alias
out = run_cmd(base_app, 'alias invalid')
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index de4c637e..9cf9429a 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -5,6 +5,7 @@ Test the parsing logic in parsing.py
Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com>
Released under MIT license, see LICENSE file
"""
+import attr
import pytest
import cmd2
@@ -31,12 +32,40 @@ def default_parser():
parser = StatementParser()
return parser
+
+def test_parse_empty_string(parser):
+ line = ''
+ statement = parser.parse(line)
+ assert statement == ''
+ assert statement.args == statement
+ assert statement.raw == line
+ assert statement.command == ''
+ assert statement.arg_list == []
+ assert statement.multiline_command == ''
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
+ assert statement.command_and_args == line
+ assert statement.argv == statement.arg_list
+
def test_parse_empty_string_default(default_parser):
- statement = default_parser.parse('')
- assert not statement.command
- assert not statement.args
+ line = ''
+ statement = default_parser.parse(line)
assert statement == ''
- assert statement.raw == ''
+ assert statement.args == statement
+ assert statement.raw == line
+ assert statement.command == ''
+ assert statement.arg_list == []
+ assert statement.multiline_command == ''
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
+ assert statement.command_and_args == line
+ assert statement.argv == statement.arg_list
@pytest.mark.parametrize('line,tokens', [
('command', ['command']),
@@ -52,13 +81,6 @@ def test_tokenize_default(default_parser, line, tokens):
tokens_to_test = default_parser.tokenize(line)
assert tokens_to_test == tokens
-def test_parse_empty_string(parser):
- statement = parser.parse('')
- assert not statement.command
- assert not statement.args
- assert statement == ''
- assert statement.raw == ''
-
@pytest.mark.parametrize('line,tokens', [
('command', ['command']),
('command /* with some comment */ arg', ['command', 'arg']),
@@ -81,8 +103,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):
@@ -98,9 +120,18 @@ 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
+ assert statement.args == statement
+ assert statement.raw == line
+ assert statement.multiline_command == ''
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
+ assert statement.command_and_args == line
@pytest.mark.parametrize('line,terminator', [
('termbare;', ';'),
@@ -111,9 +142,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 +156,10 @@ 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.args == statement
assert statement.argv == ['termbare']
+ assert not statement.arg_list
assert statement.terminator == terminator
assert statement.suffix == 'suffx'
@@ -135,71 +167,82 @@ 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.args == statement
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.args == statement
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.args == statement
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.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.args is None
- assert statement.argv == ['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 */')
- assert not statement.command
- assert not statement.args
+ assert statement.command == ''
+ 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.args == '/tmp/*.txt'
+ 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.args == '/tmp/*.txt /tmp/*.cfg'
+ assert statement == '/tmp/*.txt /tmp/*.cfg'
+ assert statement.args == statement
assert not statement.pipe_to
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.args == statement
assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?']
- assert statement == statement.args
+ assert statement.arg_list == ['if', '"quoted strings /* seem to "', 'start', 'comments?']
assert not statement.pipe_to
@pytest.mark.parametrize('line',[
@@ -209,27 +252,30 @@ 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.args == 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.args == statement
assert statement.argv == ['double-pipe', '||', 'is', 'not', 'a', 'pipe']
+ assert statement.arg_list == statement.argv[1:]
assert not statement.pipe_to
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.args == statement
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,8 +289,8 @@ 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.args == statement
assert statement.output == output
assert statement.output_to == 'out.txt'
@@ -252,9 +298,10 @@ 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.args == statement
assert statement.argv == ['output', 'into']
+ assert statement.arg_list == statement.argv[1:]
assert statement.output == '>'
assert statement.output_to == 'afile.txt'
@@ -262,9 +309,10 @@ 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.args == statement
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 +320,10 @@ 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.args == statement
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 +331,24 @@ 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.args == statement
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 == ''
+ assert statement.output_to == ''
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.args == statement
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 +358,10 @@ 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.args == statement
assert statement.argv == ['has', '>', 'inside']
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == ';'
@pytest.mark.parametrize('line,terminator',[
@@ -325,9 +377,10 @@ 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.args == statement
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 +388,11 @@ 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.args == statement
assert statement.argv == ['multiline', 'has', '>', 'inside', 'an', 'unfinished', 'command']
- assert not statement.terminator
+ assert statement.arg_list == statement.argv[1:]
+ assert statement.terminator == ''
@pytest.mark.parametrize('line,terminator',[
('multiline has > inside;', ';'),
@@ -350,9 +404,10 @@ 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.args == statement
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 +417,10 @@ 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.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):
@@ -371,9 +428,10 @@ 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.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):
@@ -381,9 +439,10 @@ 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.args == statement
assert statement.argv == ['multiline', 'command', 'ends']
+ assert statement.arg_list == statement.argv[1:]
assert statement.terminator == '\n'
@pytest.mark.parametrize('line,terminator',[
@@ -398,9 +457,10 @@ 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.args == statement
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 +468,38 @@ 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.args == statement
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.args == statement
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.args == statement
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.args == statement
assert statement.argv == ['dir', 'home']
+ assert statement.arg_list == statement.argv[1:]
assert statement.output == '>'
assert statement.output_to == 'café'
@@ -452,9 +516,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 +527,17 @@ 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
+ assert statement.args == statement
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.args == statement
+ assert statement == 'has > inside an unfinished command'
+ assert statement.terminator == ''
@pytest.mark.parametrize('line,output', [
('helpalias > out.txt', '>'),
@@ -487,8 +548,8 @@ 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.args == statement
assert statement.output == output
assert statement.output_to == 'out.txt'
@@ -499,8 +560,8 @@ 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.args == statement
assert statement.pipe_to == ['less']
@pytest.mark.parametrize('line', [
@@ -514,76 +575,118 @@ 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.args == statement
assert statement.terminator == ';'
def test_parse_command_only_command_and_args(parser):
line = 'help history'
statement = parser.parse_command_only(line)
+ assert statement == 'history'
+ assert statement.args == statement
+ assert statement.arg_list == []
assert statement.command == 'help'
- assert statement.args == 'history'
- assert statement == statement.args
assert statement.command_and_args == line
-
-def test_parse_command_only_emptyline(parser):
- line = ''
- statement = parser.parse_command_only(line)
- # 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.args is None
- assert statement == ''
- assert not statement.argv
- assert statement.command_and_args == None
+ assert statement.multiline_command == ''
+ assert statement.raw == line
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
def test_parse_command_only_strips_line(parser):
line = ' help history '
statement = parser.parse_command_only(line)
+ assert statement == 'history'
+ assert statement.args == statement
+ assert statement.arg_list == []
assert statement.command == 'help'
- assert statement.args == 'history'
- assert statement == statement.args
assert statement.command_and_args == line.strip()
+ assert statement.multiline_command == ''
+ assert statement.raw == line
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
def test_parse_command_only_expands_alias(parser):
- line = 'fake foobar.py'
+ line = 'fake foobar.py "somebody.py'
statement = parser.parse_command_only(line)
+ assert statement == 'foobar.py "somebody.py'
+ assert statement.args == statement
+ assert statement.arg_list == []
assert statement.command == 'pyscript'
- assert statement.args == 'foobar.py'
- assert statement == statement.args
+ assert statement.command_and_args == 'pyscript foobar.py "somebody.py'
+ assert statement.multiline_command == ''
+ assert statement.raw == line
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
def test_parse_command_only_expands_shortcuts(parser):
line = '!cat foobar.txt'
statement = parser.parse_command_only(line)
+ assert statement == 'cat foobar.txt'
+ assert statement.args == statement
+ assert statement.arg_list == []
assert statement.command == 'shell'
- assert statement.args == 'cat foobar.txt'
- assert statement == statement.args
assert statement.command_and_args == 'shell cat foobar.txt'
+ assert statement.multiline_command == ''
+ assert statement.raw == line
+ assert statement.multiline_command == ''
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
def test_parse_command_only_quoted_args(parser):
line = 'l "/tmp/directory with spaces/doit.sh"'
statement = parser.parse_command_only(line)
+ assert statement == 'ls -al "/tmp/directory with spaces/doit.sh"'
+ assert statement.args == statement
+ assert statement.arg_list == []
assert statement.command == 'shell'
- assert statement.args == 'ls -al "/tmp/directory with spaces/doit.sh"'
- assert statement == statement.args
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;',
- 'help ;;',
- 'help; ;;',
+ assert statement.multiline_command == ''
+ assert statement.raw == line
+ assert statement.multiline_command == ''
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
+
+@pytest.mark.parametrize('line,args', [
+ ('helpalias > out.txt', '> out.txt'),
+ ('helpalias>out.txt', '>out.txt'),
+ ('helpalias >> out.txt', '>> out.txt'),
+ ('helpalias>>out.txt', '>>out.txt'),
+ ('help|less', '|less'),
+ ('helpalias;', ';'),
+ ('help ;;', ';;'),
+ ('help; ;;', '; ;;'),
])
-def test_parse_command_only_specialchars(parser, line):
+def test_parse_command_only_specialchars(parser, line, args):
statement = parser.parse_command_only(line)
+ assert statement == args
+ assert statement.args == args
assert statement.command == 'help'
+ assert statement.multiline_command == ''
+ assert statement.raw == line
+ assert statement.multiline_command == ''
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
@pytest.mark.parametrize('line', [
+ '',
';',
';;',
';; ;',
@@ -595,34 +698,59 @@ 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.args is None
assert statement == ''
+ assert statement.args == statement
+ assert statement.arg_list == []
+ assert statement.command == ''
+ assert statement.command_and_args == ''
+ assert statement.multiline_command == ''
+ assert statement.raw == line
+ assert statement.multiline_command == ''
+ assert statement.terminator == ''
+ assert statement.suffix == ''
+ assert statement.pipe_to == []
+ assert statement.output == ''
+ assert statement.output_to == ''
def test_parse_command_only_multiline(parser):
line = 'multiline with partially "open quotes and no terminator'
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
+ assert statement.args == statement
-def test_statement_initialization(parser):
+def test_statement_initialization():
string = 'alias'
statement = cmd2.Statement(string)
assert string == statement
- assert statement.raw is None
- assert statement.command is None
- assert statement.args 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 == ''
+
+
+def test_statement_is_immutable():
+ string = 'foo'
+ statement = cmd2.Statement(string)
+ assert string == statement
+ assert statement.args == statement
+ assert statement.raw == ''
+ with pytest.raises(attr.exceptions.FrozenInstanceError):
+ statement.args = 'bar'
+ with pytest.raises(attr.exceptions.FrozenInstanceError):
+ statement.raw = 'baz'
diff --git a/tox.ini b/tox.ini
index 1e8a4468..19f50697 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,91 +6,16 @@ testpaths = tests
[testenv]
passenv = CI TRAVIS TRAVIS_* APPVEYOR*
-setenv =
- PYTHONPATH={toxinidir}
+setenv = PYTHONPATH={toxinidir}
+extras = test
+commands =
+ py.test {posargs} --cov
+ codecov
[testenv:docs]
basepython = python3.5
deps =
- sphinx<1.7.7
+ sphinx
sphinx-rtd-theme
changedir = docs
commands = sphinx-build -a -W -T -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
-
-[testenv:py34]
-deps =
- codecov
- pyperclip
- pytest
- pytest-cov
- pytest-mock
- argcomplete
- wcwidth
-commands =
- py.test {posargs} --cov
- codecov
-
-[testenv:py35]
-deps =
- mock
- pyperclip
- pytest
- pytest-mock
- argcomplete
- wcwidth
-commands = py.test -v
-
-[testenv:py35-win]
-deps =
- mock
- pyperclip
- pyreadline
- pytest
- pytest-mock
-commands = py.test -v
-
-[testenv:py36]
-deps =
- codecov
- pyperclip
- pytest
- pytest-cov
- pytest-mock
- argcomplete
- wcwidth
-commands =
- py.test {posargs} --cov
- codecov
-
-[testenv:py36-win]
-deps =
- codecov
- pyperclip
- pyreadline
- pytest
- pytest-cov
- pytest-mock
-commands =
- py.test {posargs} --cov
- codecov
-
-[testenv:py37]
-deps =
- pyperclip
- pytest
- pytest-mock
- argcomplete
- wcwidth
-commands = py.test -v
-
-[testenv:py37-win]
-deps =
- codecov
- pyperclip
- pyreadline
- pytest
- pytest-cov
- pytest-mock
-commands =
- py.test {posargs} --cov
- codecov