summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--cmd2/argcomplete_bridge.py36
-rwxr-xr-xcmd2/argparse_completer.py68
-rwxr-xr-xcmd2/cmd2.py122
-rw-r--r--cmd2/constants.py9
-rw-r--r--cmd2/parsing.py148
-rw-r--r--cmd2/pyscript_bridge.py80
-rw-r--r--cmd2/rl_utils.py10
-rw-r--r--docs/freefeatures.rst24
-rw-r--r--docs/unfreefeatures.rst6
-rwxr-xr-xexamples/subcommands.py4
-rwxr-xr-xexamples/submenus.py109
-rwxr-xr-xexamples/tab_autocompletion.py31
-rw-r--r--tests/pyscript/bar1.py1
-rw-r--r--tests/pyscript/custom_echo.py2
-rw-r--r--tests/pyscript/foo1.py1
-rw-r--r--tests/pyscript/foo2.py1
-rw-r--r--tests/pyscript/foo3.py1
-rw-r--r--tests/pyscript/foo4.py1
-rw-r--r--tests/pyscript/help.py3
-rw-r--r--tests/pyscript/help_media.py1
-rw-r--r--tests/pyscript/media_movies_add1.py1
-rw-r--r--tests/pyscript/media_movies_add2.py1
-rw-r--r--tests/pyscript/media_movies_list1.py3
-rw-r--r--tests/pyscript/media_movies_list2.py3
-rw-r--r--tests/pyscript/media_movies_list3.py3
-rw-r--r--tests/pyscript/media_movies_list4.py1
-rw-r--r--tests/pyscript/media_movies_list5.py1
-rw-r--r--tests/pyscript/media_movies_list6.py1
-rw-r--r--tests/pyscript/media_movies_list7.py1
-rw-r--r--tests/pyscript/pyscript_dir1.py3
-rw-r--r--tests/pyscript/pyscript_dir2.py3
-rw-r--r--tests/scripts/recursive.py1
-rw-r--r--tests/test_autocompletion.py4
-rw-r--r--tests/test_bashcompletion.py8
-rw-r--r--tests/test_cmd2.py20
-rw-r--r--tests/test_completion.py124
-rw-r--r--tests/test_parsing.py183
-rw-r--r--tests/test_pyscript.py36
-rw-r--r--tests/test_submenu.py182
40 files changed, 614 insertions, 625 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91710781..6bb29e48 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
* Changes
* ``strip_ansi()`` and ``strip_quotes()`` functions have moved to new utils module
* Several constants moved to new constants module
+ * Submenu support has been moved to a new [cmd2-submenu](https://github.com/python-cmd2/cmd2-submenu) plugin. If you use submenus, you will need to update your dependencies and modify your imports.
* Deletions (potentially breaking changes)
* Deleted all ``optparse`` code which had previously been deprecated in release 0.8.0
* The ``options`` decorator no longer exists
@@ -28,6 +29,7 @@
* Deleted ``cmd_with_subs_completer``, ``get_subcommands``, and ``get_subcommand_completer``
* Replaced by default AutoCompleter implementation for all commands using argparse
* Deleted support for old method of calling application commands with ``cmd()`` and ``self``
+ * ``cmd2.redirector`` is no longer supported. Output redirection can only be done with '>' or '>>'
* Python 2 no longer supported
* ``cmd2`` now supports Python 3.4+
* Known Issues
diff --git a/cmd2/argcomplete_bridge.py b/cmd2/argcomplete_bridge.py
index a036af1e..824710b0 100644
--- a/cmd2/argcomplete_bridge.py
+++ b/cmd2/argcomplete_bridge.py
@@ -6,11 +6,17 @@ try:
import argcomplete
except ImportError: # pragma: no cover
# not installed, skip the rest of the file
- pass
-
+ DEFAULT_COMPLETER = None
else:
# argcomplete is installed
+ # Newer versions of argcomplete have FilesCompleter at top level, older versions only have it under completers
+ try:
+ DEFAULT_COMPLETER = argcomplete.FilesCompleter()
+ except AttributeError:
+ DEFAULT_COMPLETER = argcomplete.completers.FilesCompleter()
+
+ from cmd2.argparse_completer import ACTION_ARG_CHOICES, ACTION_SUPPRESS_HINT
from contextlib import redirect_stdout
import copy
from io import StringIO
@@ -102,7 +108,7 @@ else:
def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit, output_stream=None,
exclude=None, validator=None, print_suppressed=False, append_space=None,
- default_completer=argcomplete.FilesCompleter()):
+ default_completer=DEFAULT_COMPLETER):
"""
:param argument_parser: The argument parser to autocomplete on
:type argument_parser: :class:`argparse.ArgumentParser`
@@ -140,9 +146,14 @@ else:
added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or
their execution is otherwise desirable.
"""
- self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
- validator=validator, print_suppressed=print_suppressed, append_space=append_space,
- default_completer=default_completer)
+ # Older versions of argcomplete have fewer keyword arguments
+ if sys.version_info >= (3, 5):
+ self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
+ validator=validator, print_suppressed=print_suppressed, append_space=append_space,
+ default_completer=default_completer)
+ else:
+ self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
+ validator=validator, print_suppressed=print_suppressed)
if "_ARGCOMPLETE" not in os.environ:
# not an argument completion invocation
@@ -235,7 +246,10 @@ else:
if comp_type == 63: # type is 63 for second tab press
print(outstr.rstrip(), file=argcomplete.debug_stream, end='')
- output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding))
+ if completions is not None:
+ output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding))
+ else:
+ output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding))
else:
# if completions is None we assume we don't know how to handle it so let bash
# go forward with normal filesystem completion
@@ -243,3 +257,11 @@ else:
output_stream.flush()
argcomplete.debug_stream.flush()
exit_method(0)
+
+
+ def bash_complete(action, show_hint: bool=True):
+ """Helper function to configure an argparse action to fall back to bash completion"""
+ def complete_none(*args, **kwargs):
+ return None
+ setattr(action, ACTION_SUPPRESS_HINT, not show_hint)
+ setattr(action, ACTION_ARG_CHOICES, (complete_none,))
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index a8a0f24a..d98a6eac 100755
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -75,6 +75,7 @@ from .rl_utils import rl_force_redisplay
# attribute that can optionally added to an argparse argument (called an Action) to
# define the completion choices for the argument. You may provide a Collection or a Function.
ACTION_ARG_CHOICES = 'arg_choices'
+ACTION_SUPPRESS_HINT = 'suppress_hint'
class _RangeAction(object):
@@ -261,6 +262,7 @@ class AutoCompleter(object):
sub_completers[subcmd] = AutoCompleter(action.choices[subcmd], subcmd_start,
arg_choices=subcmd_args,
subcmd_args_lookup=subcmd_lookup,
+ tab_for_arg_help=tab_for_arg_help,
cmd2_app=cmd2_app)
sub_commands.append(subcmd)
self._positional_completers[action.dest] = sub_completers
@@ -472,8 +474,23 @@ class AutoCompleter(object):
if action.dest in self._arg_choices:
arg_choices = self._arg_choices[action.dest]
- if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and callable(arg_choices[0]):
- completer = arg_choices[0]
+ # if arg_choices is a tuple
+ # Let's see if it's a custom completion function. If it is, return what it provides
+ # To do this, we make sure the first element is either a callable
+ # or it's the name of a callable in the application
+ if isinstance(arg_choices, tuple) and len(arg_choices) > 0 and \
+ (callable(arg_choices[0]) or
+ (isinstance(arg_choices[0], str) and hasattr(self._cmd2_app, arg_choices[0]) and
+ callable(getattr(self._cmd2_app, arg_choices[0]))
+ )
+ ):
+
+ if callable(arg_choices[0]):
+ completer = arg_choices[0]
+ elif isinstance(arg_choices[0], str) and callable(getattr(self._cmd2_app, arg_choices[0])):
+ completer = getattr(self._cmd2_app, arg_choices[0])
+
+ # extract the positional and keyword arguments from the tuple
list_args = None
kw_args = None
for index in range(1, len(arg_choices)):
@@ -481,14 +498,19 @@ class AutoCompleter(object):
list_args = arg_choices[index]
elif isinstance(arg_choices[index], dict):
kw_args = arg_choices[index]
- if list_args is not None and kw_args is not None:
- return completer(text, line, begidx, endidx, *list_args, **kw_args)
- elif list_args is not None:
- return completer(text, line, begidx, endidx, *list_args)
- elif kw_args is not None:
- return completer(text, line, begidx, endidx, **kw_args)
- else:
- return completer(text, line, begidx, endidx)
+ try:
+ # call the provided function differently depending on the provided positional and keyword arguments
+ if list_args is not None and kw_args is not None:
+ return completer(text, line, begidx, endidx, *list_args, **kw_args)
+ elif list_args is not None:
+ return completer(text, line, begidx, endidx, *list_args)
+ elif kw_args is not None:
+ return completer(text, line, begidx, endidx, **kw_args)
+ else:
+ return completer(text, line, begidx, endidx)
+ except TypeError:
+ # assume this is due to an incorrect function signature, return nothing.
+ return []
else:
return AutoCompleter.basic_complete(text, line, begidx, endidx,
self._resolve_choices_for_arg(action, used_values))
@@ -499,6 +521,16 @@ class AutoCompleter(object):
if action.dest in self._arg_choices:
args = self._arg_choices[action.dest]
+ # is the argument a string? If so, see if we can find an attribute in the
+ # application matching the string.
+ if isinstance(args, str):
+ try:
+ args = getattr(self._cmd2_app, args)
+ except AttributeError:
+ # Couldn't find anything matching the name
+ return []
+
+ # is the provided argument a callable. If so, call it
if callable(args):
try:
if self._cmd2_app is not None:
@@ -525,8 +557,19 @@ class AutoCompleter(object):
return []
def _print_action_help(self, action: argparse.Action) -> None:
+ # is parameter hinting disabled globally?
if not self._tab_for_arg_help:
return
+
+ # is parameter hinting disabled for this parameter?
+ try:
+ suppress_hint = getattr(action, ACTION_SUPPRESS_HINT)
+ except AttributeError:
+ pass
+ else:
+ if suppress_hint:
+ return
+
if action.option_strings:
flags = ', '.join(action.option_strings)
param = ''
@@ -535,7 +578,10 @@ class AutoCompleter(object):
prefix = '{}{}'.format(flags, param)
else:
- prefix = '{}'.format(str(action.dest).upper())
+ if action.dest != SUPPRESS:
+ prefix = '{}'.format(str(action.dest).upper())
+ else:
+ prefix = ''
prefix = ' {0: <{width}} '.format(prefix, width=20)
pref_len = len(prefix)
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 7547c012..6a2f0e02 100755
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -26,6 +26,7 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2
import argparse
import cmd
import collections
+from colorama import Fore
import glob
import os
import platform
@@ -39,26 +40,34 @@ import pyperclip
from . import constants
from . import utils
+from cmd2.parsing import StatementParser, Statement
+
# Set up readline
-from .rl_utils import rl_force_redisplay, readline, rl_type, RlType
-from .argparse_completer import AutoCompleter, ACArgumentParser
+from .rl_utils import rl_type, RlType
+if rl_type == RlType.NONE:
+ rl_warning = "Readline features including tab completion have been disabled since no \n" \
+ "supported version of readline was found. To resolve this, install \n" \
+ "pyreadline on Windows or gnureadline on Mac.\n\n"
+ sys.stderr.write(Fore.LIGHTYELLOW_EX + rl_warning + Fore.RESET)
+else:
+ from .rl_utils import rl_force_redisplay, readline
-from cmd2.parsing import StatementParser, Statement
+ if rl_type == RlType.PYREADLINE:
-if rl_type == RlType.PYREADLINE:
+ # Save the original pyreadline display completion function since we need to override it and restore it
+ # noinspection PyProtectedMember
+ orig_pyreadline_display = readline.rl.mode._display_completions
- # Save the original pyreadline display completion function since we need to override it and restore it
- # noinspection PyProtectedMember
- orig_pyreadline_display = readline.rl.mode._display_completions
+ elif rl_type == RlType.GNU:
-elif rl_type == RlType.GNU:
+ # We need wcswidth to calculate display width of tab completions
+ from wcwidth import wcswidth
- # We need wcswidth to calculate display width of tab completions
- from wcwidth import wcswidth
+ # Get the readline lib so we can make changes to it
+ import ctypes
+ from .rl_utils import readline_lib
- # Get the readline lib so we can make changes to it
- import ctypes
- from .rl_utils import readline_lib
+from .argparse_completer import AutoCompleter, ACArgumentParser
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
try:
@@ -312,7 +321,6 @@ class Cmd(cmd.Cmd):
# Attributes used to configure the StatementParser, best not to change these at runtime
blankLinesAllowed = False
multiline_commands = []
- redirector = '>' # for sending output to file
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
aliases = dict()
terminators = [';']
@@ -377,7 +385,7 @@ class Cmd(cmd.Cmd):
pass
# If persistent readline history is enabled, then read history from file and register to write to file at exit
- if persistent_history_file:
+ if persistent_history_file and rl_type != RlType.NONE:
persistent_history_file = os.path.expanduser(persistent_history_file)
try:
readline.read_history_file(persistent_history_file)
@@ -1127,29 +1135,26 @@ class Cmd(cmd.Cmd):
if len(raw_tokens) > 1:
- # Build a list of all redirection tokens
- all_redirects = constants.REDIRECTION_CHARS + ['>>']
-
# Check if there are redirection strings prior to the token being completed
seen_pipe = False
has_redirection = False
for cur_token in raw_tokens[:-1]:
- if cur_token in all_redirects:
+ if cur_token in constants.REDIRECTION_TOKENS:
has_redirection = True
- if cur_token == '|':
+ if cur_token == constants.REDIRECTION_PIPE:
seen_pipe = True
# Get token prior to the one being completed
prior_token = raw_tokens[-2]
# If a pipe is right before the token being completed, complete a shell command as the piped process
- if prior_token == '|':
+ if prior_token == constants.REDIRECTION_PIPE:
return self.shell_cmd_complete(text, line, begidx, endidx)
# Otherwise do path completion either as files to redirectors or arguments to the piped process
- elif prior_token in all_redirects or seen_pipe:
+ elif prior_token in constants.REDIRECTION_TOKENS or seen_pipe:
return self.path_complete(text, line, begidx, endidx)
# If there were redirection strings anywhere on the command line, then we
@@ -1272,6 +1277,7 @@ class Cmd(cmd.Cmd):
:param state: int - non-negative integer
"""
import functools
+ if state == 0 and rl_type != RlType.NONE:
if state == 0:
unclosed_quote = ''
@@ -1806,7 +1812,7 @@ class Cmd(cmd.Cmd):
# We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
try:
- self.pipe_proc = subprocess.Popen(shlex.split(statement.pipe_to), stdin=subproc_stdin)
+ self.pipe_proc = subprocess.Popen(statement.pipe_to, stdin=subproc_stdin)
except Exception as ex:
# Restore stdout to what it was and close the pipe
self.stdout.close()
@@ -1821,24 +1827,30 @@ class Cmd(cmd.Cmd):
elif statement.output:
import tempfile
if (not statement.output_to) and (not can_clip):
- raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable')
+ raise EnvironmentError("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable")
self.kept_state = Statekeeper(self, ('stdout',))
self.kept_sys = Statekeeper(sys, ('stdout',))
self.redirecting = True
if statement.output_to:
+ # going to a file
mode = 'w'
- if statement.output == 2 * self.redirector:
+ # statement.output can only contain
+ # REDIRECTION_APPEND or REDIRECTION_OUTPUT
+ if statement.output == constants.REDIRECTION_APPEND:
mode = 'a'
sys.stdout = self.stdout = open(os.path.expanduser(statement.output_to), mode)
else:
+ # going to a paste buffer
sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+")
- if statement.output == '>>':
+ if statement.output == constants.REDIRECTION_APPEND:
self.poutput(get_paste_buffer())
def _restore_output(self, statement):
- """Handles restoring state after output redirection as well as the actual pipe operation if present.
+ """Handles restoring state after output redirection as well as
+ the actual pipe operation if present.
- :param statement: Statement object which contains the parsed input from the user
+ :param statement: Statement object which contains the parsed
+ input from the user
"""
# If we have redirected output to a file or the clipboard or piped it to a shell command, then restore state
if self.kept_state is not None:
@@ -2116,23 +2128,26 @@ Usage: Usage: alias [name] | [<name> <value>]
name = arglist[0]
value = ' '.join(arglist[1:])
- # Check for a valid name
- for cur_char in name:
- if cur_char not in self.identchars:
- self.perror("Alias names can only contain the following characters: {}".format(self.identchars),
- traceback_war=False)
- return
-
- # Set the alias
- self.aliases[name] = value
- self.poutput("Alias {!r} created".format(name))
+ # Validate the alias to ensure it doesn't include weird characters
+ # like terminators, output redirection, or whitespace
+ valid, invalidchars = self.statement_parser.is_valid_command(name)
+ if valid:
+ # Set the alias
+ self.aliases[name] = value
+ self.poutput("Alias {!r} created".format(name))
+ else:
+ errmsg = "Aliases can not contain: {}".format(invalidchars)
+ self.perror(errmsg, traceback_war=False)
def complete_alias(self, text, line, begidx, endidx):
""" Tab completion for alias """
+ alias_names = set(self.aliases.keys())
+ visible_commands = set(self.get_visible_commands())
+
index_dict = \
{
- 1: self.aliases,
- 2: self.get_visible_commands()
+ 1: alias_names,
+ 2: list(alias_names | visible_commands)
}
return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete)
@@ -2354,9 +2369,12 @@ Usage: Usage: unalias [-a] name [name ...]
self.poutput(' %2d. %s\n' % (idx + 1, text))
while True:
response = input(prompt)
- hlen = readline.get_current_history_length()
- if hlen >= 1 and response != '':
- readline.remove_history_item(hlen - 1)
+
+ if rl_type != RlType.NONE:
+ hlen = readline.get_current_history_length()
+ if hlen >= 1 and response != '':
+ readline.remove_history_item(hlen - 1)
+
try:
response = int(response)
result = fulloptions[response - 1][0]
@@ -2613,9 +2631,21 @@ Paths or arguments that contain spaces must be enclosed in quotes
Run python code from external files with ``run filename.py``
End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
"""
- banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
- exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
- embed(banner1=banner, exit_msg=exit_msg)
+ from .pyscript_bridge import PyscriptBridge
+ bridge = PyscriptBridge(self)
+
+ if self.locals_in_py:
+ def load_ipy(self, app):
+ banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
+ exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
+ embed(banner1=banner, exit_msg=exit_msg)
+ load_ipy(self, bridge)
+ else:
+ def load_ipy(app):
+ banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
+ exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
+ embed(banner1=banner, exit_msg=exit_msg)
+ load_ipy(bridge)
history_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
history_parser_group = history_parser.add_mutually_exclusive_group()
diff --git a/cmd2/constants.py b/cmd2/constants.py
index 838650e5..b829000f 100644
--- a/cmd2/constants.py
+++ b/cmd2/constants.py
@@ -4,9 +4,14 @@
import re
-# Used for command parsing, tab completion and word breaks. Do not change.
+# Used for command parsing, output redirection, tab completion and word
+# breaks. Do not change.
QUOTES = ['"', "'"]
-REDIRECTION_CHARS = ['|', '>']
+REDIRECTION_PIPE = '|'
+REDIRECTION_OUTPUT = '>'
+REDIRECTION_APPEND = '>>'
+REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT]
+REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND]
# Regular expression to match ANSI escape codes
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index f2c86ea8..655e0c58 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -45,7 +45,8 @@ class Statement(str):
redirection, if any
:type suffix: str or None
:var pipe_to: if output was piped to a shell command, the shell command
- :type pipe_to: str or None
+ 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, usually a filename
@@ -141,15 +142,67 @@ class StatementParser:
re.DOTALL | re.MULTILINE
)
- # aliases have to be a word, so make a regular expression
- # that matches the first word in the line. This regex has two
- # parts, the first parenthesis enclosed group matches one
- # or more non-whitespace characters (which may be preceeded
- # by whitespace) and the second group matches either a whitespace
- # character or the end of the string. We use \A and \Z to ensure
- # we always match the beginning and end of a string that may have
- # multiple lines
- self.command_pattern = re.compile(r'\A\s*(\S+)(\s|\Z)+')
+ # commands have to be a word, so make a regular expression
+ # that matches the first word in the line. This regex has three
+ # parts:
+ # - the '\A\s*' matches the beginning of the string (even
+ # if contains multiple lines) and gobbles up any leading
+ # whitespace
+ # - the first parenthesis enclosed group matches one
+ # or more non-whitespace characters with a non-greedy match
+ # (that's what the '+?' part does). The non-greedy match
+ # ensures that this first group doesn't include anything
+ # matched by the second group
+ # - the second parenthesis group must be dynamically created
+ # because it needs to match either whitespace, something in
+ # REDIRECTION_CHARS, one of the terminators, or the end of
+ # the string (\Z matches the end of the string even if it
+ # contains multiple lines)
+ #
+ invalid_command_chars = []
+ invalid_command_chars.extend(constants.QUOTES)
+ invalid_command_chars.extend(constants.REDIRECTION_CHARS)
+ invalid_command_chars.extend(terminators)
+ # escape each item so it will for sure get treated as a literal
+ second_group_items = [re.escape(x) for x in invalid_command_chars]
+ # add the whitespace and end of string, not escaped because they
+ # are not literals
+ second_group_items.extend([r'\s', r'\Z'])
+ # join them up with a pipe
+ second_group = '|'.join(second_group_items)
+ # build the regular expression
+ expr = r'\A\s*(\S*?)({})'.format(second_group)
+ self._command_pattern = re.compile(expr)
+
+ def is_valid_command(self, word: str) -> Tuple[bool, str]:
+ """Determine whether a word is a valid alias.
+
+ Aliases can not include redirection characters, whitespace,
+ or termination characters.
+
+ If word is not a valid command, return False and a comma
+ separated string of characters that can not appear in a command.
+ This string is suitable for inclusion in an error message of your
+ choice:
+
+ valid, invalidchars = statement_parser.is_valid_command('>')
+ if not valid:
+ errmsg = "Aliases can not contain: {}".format(invalidchars)
+ """
+ valid = False
+
+ errmsg = 'whitespace, quotes, '
+ errchars = []
+ errchars.extend(constants.REDIRECTION_CHARS)
+ errchars.extend(self.terminators)
+ errmsg += ', '.join([shlex.quote(x) for x in errchars])
+
+ match = self._command_pattern.search(word)
+ if match:
+ if word == match.group(1):
+ valid = True
+ errmsg = None
+ return valid, errmsg
def tokenize(self, line: str) -> List[str]:
"""Lex a string into a list of tokens.
@@ -197,23 +250,27 @@ class StatementParser:
tokens = self.tokenize(rawinput)
# of the valid terminators, find the first one to occur in the input
- terminator_pos = len(tokens)+1
- for test_terminator in self.terminators:
- try:
- pos = tokens.index(test_terminator)
- if pos < terminator_pos:
+ terminator_pos = len(tokens) + 1
+ for pos, cur_token in enumerate(tokens):
+ for test_terminator in self.terminators:
+ if cur_token.startswith(test_terminator):
terminator_pos = pos
terminator = test_terminator
+ # break the inner loop, and we want to break the
+ # outer loop too
break
- except ValueError:
- # the terminator is not in the tokens
- pass
+ else:
+ # this else clause is only run if the inner loop
+ # didn't execute a break. If it didn't, then
+ # continue to the next iteration of the outer loop
+ continue
+ # inner loop was broken, break the outer
+ break
if terminator:
if terminator == LINE_FEED:
terminator_pos = len(tokens)+1
- else:
- terminator_pos = tokens.index(terminator)
+
# everything before the first terminator is the command and the args
argv = tokens[:terminator_pos]
(command, args) = self._command_and_args(argv)
@@ -231,12 +288,27 @@ class StatementParser:
argv = tokens
tokens = []
+ # check for a pipe to a shell process
+ # if there is a pipe, everything after the pipe needs to be passed
+ # to the shell, even redirected output
+ # this allows '(Cmd) say hello | wc > countit.txt'
+ try:
+ # find the first pipe if it exists
+ pipe_pos = tokens.index(constants.REDIRECTION_PIPE)
+ # save everything after the first pipe as tokens
+ pipe_to = tokens[pipe_pos+1:]
+ # remove all the tokens after the pipe
+ tokens = tokens[:pipe_pos]
+ except ValueError:
+ # no pipe in the tokens
+ pipe_to = None
+
# check for output redirect
output = None
output_to = None
try:
- output_pos = tokens.index('>')
- output = '>'
+ output_pos = tokens.index(constants.REDIRECTION_OUTPUT)
+ output = constants.REDIRECTION_OUTPUT
output_to = ' '.join(tokens[output_pos+1:])
# remove all the tokens after the output redirect
tokens = tokens[:output_pos]
@@ -244,26 +316,14 @@ class StatementParser:
pass
try:
- output_pos = tokens.index('>>')
- output = '>>'
+ output_pos = tokens.index(constants.REDIRECTION_APPEND)
+ output = constants.REDIRECTION_APPEND
output_to = ' '.join(tokens[output_pos+1:])
# remove all tokens after the output redirect
tokens = tokens[:output_pos]
except ValueError:
pass
- # check for pipes
- try:
- # find the first pipe if it exists
- pipe_pos = tokens.index('|')
- # save everything after the first pipe
- pipe_to = ' '.join(tokens[pipe_pos+1:])
- # remove all the tokens after the pipe
- tokens = tokens[:pipe_pos]
- except ValueError:
- # no pipe in the tokens
- pipe_to = None
-
if terminator:
# whatever is left is the suffix
suffix = ' '.join(tokens)
@@ -324,16 +384,24 @@ class StatementParser:
command = None
args = None
- match = self.command_pattern.search(line)
+ match = self._command_pattern.search(line)
if match:
# we got a match, extract the command
command = match.group(1)
- # the command_pattern regex is designed to match the spaces
+ # the match could be an empty string, if so, turn it into none
+ if not command:
+ command = None
+ # the _command_pattern regex is designed to match the spaces
# between command and args with a second match group. Using
# the end of the second match group ensures that args has
# no leading whitespace. The rstrip() makes sure there is
# no trailing whitespace
args = line[match.end(2):].rstrip()
+ # if the command is none that means the input was either empty
+ # or something wierd like '>'. args should be None if we couldn't
+ # parse a command
+ if not command or not args:
+ args = None
# build the statement
# string representation of args must be an empty string instead of
@@ -355,11 +423,11 @@ class StatementParser:
for cur_alias in tmp_aliases:
keep_expanding = False
# apply our regex to line
- match = self.command_pattern.search(line)
+ match = self._command_pattern.search(line)
if match:
# we got a match, extract the command
command = match.group(1)
- if command == cur_alias:
+ if command and command == cur_alias:
# rebuild line with the expanded alias
line = self.aliases[cur_alias] + match.group(2) + line[match.end(2):]
tmp_aliases.remove(cur_alias)
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 055ae4ae..196be82b 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -8,10 +8,9 @@ Released under MIT license, see LICENSE file
"""
import argparse
-from collections import namedtuple
import functools
import sys
-from typing import List, Tuple
+from typing import List, Tuple, Callable
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
@@ -41,13 +40,15 @@ class CommandResult(namedtuple_with_defaults('CmdResult', ['stdout', 'stderr', '
class CopyStream(object):
"""Copies all data written to a stream"""
- def __init__(self, inner_stream):
+ def __init__(self, inner_stream, echo: bool = False):
self.buffer = ''
self.inner_stream = inner_stream
+ self.echo = echo
def write(self, s):
self.buffer += s
- self.inner_stream.write(s)
+ if self.echo:
+ self.inner_stream.write(s)
def read(self):
raise NotImplementedError
@@ -55,22 +56,35 @@ class CopyStream(object):
def clear(self):
self.buffer = ''
+ def __getattr__(self, item: str):
+ if item in self.__dict__:
+ return self.__dict__[item]
+ else:
+ return getattr(self.inner_stream, item)
+
-def _exec_cmd(cmd2_app, func):
+def _exec_cmd(cmd2_app, func: Callable, echo: bool):
"""Helper to encapsulate executing a command and capturing the results"""
- copy_stdout = CopyStream(sys.stdout)
- copy_stderr = CopyStream(sys.stderr)
+ copy_stdout = CopyStream(sys.stdout, echo)
+ copy_stderr = CopyStream(sys.stderr, echo)
+
+ copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
cmd2_app._last_result = None
- with redirect_stdout(copy_stdout):
- with redirect_stderr(copy_stderr):
- func()
+ try:
+ cmd2_app.stdout = copy_cmd_stdout
+ with redirect_stdout(copy_stdout):
+ with redirect_stderr(copy_stderr):
+ func()
+ finally:
+ cmd2_app.stdout = copy_cmd_stdout.inner_stream
# if stderr is empty, set it to None
- stderr = copy_stderr if copy_stderr.buffer else None
+ stderr = copy_stderr.buffer if copy_stderr.buffer else None
- result = CommandResult(stdout=copy_stdout.buffer, stderr=stderr, data=cmd2_app._last_result)
+ outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
+ result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
return result
@@ -78,9 +92,10 @@ class ArgparseFunctor:
"""
Encapsulates translating python object traversal
"""
- def __init__(self, cmd2_app, item, parser):
+ def __init__(self, echo: bool, cmd2_app, command_name: str, parser: argparse.ArgumentParser):
+ self._echo = echo
self._cmd2_app = cmd2_app
- self._item = item
+ self._command_name = command_name
self._parser = parser
# Dictionary mapping command argument name to value
@@ -88,7 +103,15 @@ class ArgparseFunctor:
# argparse object for the current command layer
self.__current_subcommand_parser = parser
- def __getattr__(self, item):
+ def __dir__(self):
+ """Returns a custom list of attribute names to match the sub-commands"""
+ commands = []
+ for action in self.__current_subcommand_parser._actions:
+ if not action.option_strings and isinstance(action, argparse._SubParsersAction):
+ commands.extend(action.choices)
+ return commands
+
+ def __getattr__(self, item: str):
"""Search for a subcommand matching this item and update internal state to track the traversal"""
# look for sub-command under the current command/sub-command layer
for action in self.__current_subcommand_parser._actions:
@@ -101,7 +124,6 @@ class ArgparseFunctor:
return self
raise AttributeError(item)
- # return super().__getattr__(item)
def __call__(self, *args, **kwargs):
"""
@@ -182,7 +204,7 @@ class ArgparseFunctor:
def _run(self):
# look up command function
- func = getattr(self._cmd2_app, 'do_' + self._item)
+ func = getattr(self._cmd2_app, 'do_' + self._command_name)
# reconstruct the cmd2 command from the python call
cmd_str = ['']
@@ -208,7 +230,7 @@ class ArgparseFunctor:
if action.option_strings:
cmd_str[0] += '{} '.format(action.option_strings[0])
- if isinstance(value, List) or isinstance(value, Tuple):
+ if isinstance(value, List) or isinstance(value, tuple):
for item in value:
item = str(item).strip()
if ' ' in item:
@@ -228,7 +250,7 @@ class ArgparseFunctor:
cmd_str[0] += '{} '.format(self._args[action.dest])
traverse_parser(action.choices[self._args[action.dest]])
elif isinstance(action, argparse._AppendAction):
- if isinstance(self._args[action.dest], List) or isinstance(self._args[action.dest], Tuple):
+ if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple):
for values in self._args[action.dest]:
process_flag(action, values)
else:
@@ -238,9 +260,8 @@ class ArgparseFunctor:
traverse_parser(self._parser)
- # print('Command: {}'.format(cmd_str[0]))
+ return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]), self._echo)
- return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]))
class PyscriptBridge(object):
"""Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for
@@ -248,6 +269,7 @@ class PyscriptBridge(object):
def __init__(self, cmd2_app):
self._cmd2_app = cmd2_app
self._last_result = None
+ self.cmd_echo = False
def __getattr__(self, item: str):
"""Check if the attribute is a command. If so, return a callable."""
@@ -261,13 +283,19 @@ class PyscriptBridge(object):
except AttributeError:
# Command doesn't, we will accept parameters in the form of a command string
def wrap_func(args=''):
- return _exec_cmd(self._cmd2_app, functools.partial(func, args))
+ return _exec_cmd(self._cmd2_app, functools.partial(func, args), self.cmd_echo)
return wrap_func
else:
# Command does use argparse, return an object that can traverse the argparse subcommands and arguments
- return ArgparseFunctor(self._cmd2_app, item, parser)
+ return ArgparseFunctor(self.cmd_echo, self._cmd2_app, item, parser)
- raise AttributeError(item)
+ return super().__getattr__(item)
+
+ def __dir__(self):
+ """Return a custom set of attribute names to match the available commands"""
+ commands = list(self._cmd2_app.get_all_commands())
+ commands.insert(0, 'cmd_echo')
+ return commands
- def __call__(self, args):
- return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'))
+ def __call__(self, args: str):
+ return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'), self.cmd_echo)
diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py
index 465fcaea..8ef65d28 100644
--- a/cmd2/rl_utils.py
+++ b/cmd2/rl_utils.py
@@ -34,11 +34,13 @@ if 'pyreadline' in sys.modules:
rl_type = RlType.PYREADLINE
elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
- rl_type = RlType.GNU
+ # We don't support libedit
+ if 'libedit' not in readline.__doc__:
+ rl_type = RlType.GNU
- # Load the readline lib so we can access members of it
- import ctypes
- readline_lib = ctypes.CDLL(readline.__file__)
+ # Load the readline lib so we can access members of it
+ import ctypes
+ readline_lib = ctypes.CDLL(readline.__file__)
def rl_force_redisplay() -> None:
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index 95ae127c..a03a1d08 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -100,26 +100,8 @@ As in a Unix shell, output of a command can be redirected:
- appended to a file with ``>>``, as in ``mycommand args >> filename.txt``
- piped (``|``) as input to operating-system commands, as in
``mycommand args | wc``
- - sent to the paste buffer, ready for the next Copy operation, by
- ending with a bare ``>``, as in ``mycommand args >``.. Redirecting
- to paste buffer requires software to be installed on the operating
- system, pywin32_ on Windows or xclip_ on \*nix.
+ - sent to the operating system paste buffer, by ending with a bare ``>``, as in ``mycommand args >``. You can even append output to the current contents of the paste buffer by ending your command with ``>>``.
-If your application depends on mathematical syntax, ``>`` may be a bad
-choice for redirecting output - it will prevent you from using the
-greater-than sign in your actual user commands. You can override your
-app's value of ``self.redirector`` to use a different string for output redirection::
-
- class MyApp(cmd2.Cmd):
- redirector = '->'
-
-::
-
- (Cmd) say line1 -> out.txt
- (Cmd) say line2 ->-> out.txt
- (Cmd) !cat out.txt
- line1
- line2
.. note::
@@ -136,8 +118,8 @@ app's value of ``self.redirector`` to use a different string for output redirect
arguments after them from the command line arguments accordingly. But output from a command will not be redirected
to a file or piped to a shell command.
-.. _pywin32: http://sourceforge.net/projects/pywin32/
-.. _xclip: http://www.cyberciti.biz/faq/xclip-linux-insert-files-command-output-intoclipboard/
+If you need to include any of these redirection characters in your command,
+you can enclose them in quotation marks, ``mycommand 'with > in the argument'``.
Python
======
diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst
index a4776a53..41144c8f 100644
--- a/docs/unfreefeatures.rst
+++ b/docs/unfreefeatures.rst
@@ -10,13 +10,17 @@ commands whose names are listed in the
parameter ``app.multiline_commands``. These
commands will be executed only
after the user has entered a *terminator*.
-By default, the command terminators is
+By default, the command terminator is
``;``; replacing or appending to the list
``app.terminators`` allows different
terminators. A blank line
is *always* considered a command terminator
(cannot be overridden).
+In multiline commands, output redirection characters
+like ``>`` and ``|`` are part of the command
+arguments unless they appear after the terminator.
+
Parsed statements
=================
diff --git a/examples/subcommands.py b/examples/subcommands.py
index 3dd2c683..55be7711 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -19,6 +19,10 @@ base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcomma
parser_foo = base_subparsers.add_parser('foo', help='foo help')
parser_foo.add_argument('-x', type=int, default=1, help='integer')
parser_foo.add_argument('y', type=float, help='float')
+input_file = parser_foo.add_argument('input_file', type=str, help='Input File')
+if __name__ == '__main__':
+ from cmd2.argcomplete_bridge import bash_complete
+ bash_complete(input_file)
# create the parser for the "bar" subcommand
parser_bar = base_subparsers.add_parser('bar', help='bar help')
diff --git a/examples/submenus.py b/examples/submenus.py
deleted file mode 100755
index 27c8cb10..00000000
--- a/examples/submenus.py
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/usr/bin/env python
-# coding=utf-8
-"""
-Create a CLI with a nested command structure as follows. The commands 'second' and 'third' navigate the CLI to the scope
-of the submenu. Nesting of the submenus is done with the cmd2.AddSubmenu() decorator.
-
- (Top Level)----second----->(2nd Level)----third----->(3rd Level)
- | | |
- ---> say ---> say ---> say
-"""
-from __future__ import print_function
-import sys
-
-from cmd2 import cmd2
-from IPython import embed
-
-
-class ThirdLevel(cmd2.Cmd):
- """To be used as a third level command class. """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.prompt = '3rdLevel '
- self.top_level_attr = None
- self.second_level_attr = None
-
- def do_say(self, line):
- print("You called a command in ThirdLevel with '%s'. "
- "It has access to top_level_attr: %s "
- "and second_level_attr: %s" % (line, self.top_level_attr, self.second_level_attr))
-
- def help_say(self):
- print("This is a third level submenu (submenu_ab). Options are qwe, asd, zxc")
-
- def complete_say(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
-
-@cmd2.AddSubmenu(ThirdLevel(),
- command='third',
- aliases=('third_alias',),
- shared_attributes=dict(second_level_attr='second_level_attr', top_level_attr='top_level_attr'))
-class SecondLevel(cmd2.Cmd):
- """To be used as a second level command class. """
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = '2ndLevel '
- self.top_level_attr = None
- self.second_level_attr = 987654321
-
- def do_ipy(self, arg):
- """Enters an interactive IPython shell.
-
- Run python code from external files with ``run filename.py``
- End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
- """
- banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
- exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
- embed(banner1=banner, exit_msg=exit_msg)
-
- def do_say(self, line):
- print("You called a command in SecondLevel with '%s'. "
- "It has access to top_level_attr: %s" % (line, self.top_level_attr))
-
- def help_say(self):
- print("This is a SecondLevel menu. Options are qwe, asd, zxc")
-
- def complete_say(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
-
-@cmd2.AddSubmenu(SecondLevel(),
- command='second',
- aliases=('second_alias',),
- shared_attributes=dict(top_level_attr='top_level_attr'))
-class TopLevel(cmd2.Cmd):
- """To be used as the main / top level command class that will contain other submenus."""
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = 'TopLevel '
- self.top_level_attr = 123456789
-
- def do_ipy(self, arg):
- """Enters an interactive IPython shell.
-
- Run python code from external files with ``run filename.py``
- End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
- """
- banner = 'Entering an embedded IPython shell type quit() or <Ctrl>-d to exit ...'
- exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
- embed(banner1=banner, exit_msg=exit_msg)
-
- def do_say(self, line):
- print("You called a command in TopLevel with '%s'. "
- "TopLevel has attribute top_level_attr=%s" % (line, self.top_level_attr))
-
- def help_say(self):
- print("This is a top level submenu. Options are qwe, asd, zxc")
-
- def complete_say(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
-
-if __name__ == '__main__':
-
- root = TopLevel()
- root.cmdloop()
-
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index f3302533..adfe9702 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -96,6 +96,15 @@ class TabCompleteExample(cmd2.Cmd):
},
}
+ file_list = \
+ [
+ '/home/user/file.db',
+ '/home/user/file space.db',
+ '/home/user/another.db',
+ '/home/other user/maps.db',
+ '/home/other user/tests.db'
+ ]
+
def instance_query_actors(self) -> List[str]:
"""Simulating a function that queries and returns a completion values"""
return actors
@@ -225,9 +234,23 @@ class TabCompleteExample(cmd2.Cmd):
required=True)
actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*')
+ vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load')
+ vid_movie_file_action = vid_movies_load_parser.add_argument('movie_file', help='Movie database')
+
+ vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read')
+ vid_movie_fread_action = vid_movies_read_parser.add_argument('movie_file', help='Movie database')
+
# tag the action objects with completion providers. This can be a collection or a callable
setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors)
- setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, instance_query_actors)
+ setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors')
+
+ # tag the file property with a custom completion function 'delimeter_complete' provided by cmd2.
+ setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES,
+ ('delimiter_complete',
+ {'delimiter': '/',
+ 'match_against': file_list}))
+ setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES,
+ ('path_complete', [False, False]))
vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete')
@@ -306,6 +329,9 @@ class TabCompleteExample(cmd2.Cmd):
movies_delete_parser = movies_commands_subparsers.add_parser('delete')
+ movies_load_parser = movies_commands_subparsers.add_parser('load')
+ movie_file_action = movies_load_parser.add_argument('movie_file', help='Movie database')
+
shows_parser = media_types_subparsers.add_parser('shows')
shows_parser.set_defaults(func=_do_media_shows)
@@ -333,7 +359,8 @@ class TabCompleteExample(cmd2.Cmd):
def complete_media(self, text, line, begidx, endidx):
""" Adds tab completion to media"""
choices = {'actor': query_actors, # function
- 'director': TabCompleteExample.static_list_directors # static list
+ 'director': TabCompleteExample.static_list_directors, # static list
+ 'movie_file': (self.path_complete, [False, False])
}
completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices)
diff --git a/tests/pyscript/bar1.py b/tests/pyscript/bar1.py
index c6276a87..521e2c29 100644
--- a/tests/pyscript/bar1.py
+++ b/tests/pyscript/bar1.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.bar('11', '22')
diff --git a/tests/pyscript/custom_echo.py b/tests/pyscript/custom_echo.py
new file mode 100644
index 00000000..14040e4c
--- /dev/null
+++ b/tests/pyscript/custom_echo.py
@@ -0,0 +1,2 @@
+custom.cmd_echo = True
+custom.echo('blah!')
diff --git a/tests/pyscript/foo1.py b/tests/pyscript/foo1.py
index 6e345d95..d9345354 100644
--- a/tests/pyscript/foo1.py
+++ b/tests/pyscript/foo1.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.foo('aaa', 'bbb', counter=3, trueval=True, constval=True)
diff --git a/tests/pyscript/foo2.py b/tests/pyscript/foo2.py
index d4df7616..d3600a60 100644
--- a/tests/pyscript/foo2.py
+++ b/tests/pyscript/foo2.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.foo('11', '22', '33', '44', counter=3, trueval=True, constval=True)
diff --git a/tests/pyscript/foo3.py b/tests/pyscript/foo3.py
index db69edaf..fc0e084a 100644
--- a/tests/pyscript/foo3.py
+++ b/tests/pyscript/foo3.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.foo('11', '22', '33', '44', '55', '66', counter=3, trueval=False, constval=False)
diff --git a/tests/pyscript/foo4.py b/tests/pyscript/foo4.py
index 88fd3ce8..e4b7d01c 100644
--- a/tests/pyscript/foo4.py
+++ b/tests/pyscript/foo4.py
@@ -1,3 +1,4 @@
+app.cmd_echo = True
result = app.foo('aaa', 'bbb', counter=3)
out_text = 'Fail'
if result:
diff --git a/tests/pyscript/help.py b/tests/pyscript/help.py
index 3f67793c..664c0488 100644
--- a/tests/pyscript/help.py
+++ b/tests/pyscript/help.py
@@ -1 +1,2 @@
-app.help() \ No newline at end of file
+app.cmd_echo = True
+app.help()
diff --git a/tests/pyscript/help_media.py b/tests/pyscript/help_media.py
index 78025bdd..d8d97c42 100644
--- a/tests/pyscript/help_media.py
+++ b/tests/pyscript/help_media.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.help('media')
diff --git a/tests/pyscript/media_movies_add1.py b/tests/pyscript/media_movies_add1.py
index a9139cb1..7249c0ef 100644
--- a/tests/pyscript/media_movies_add1.py
+++ b/tests/pyscript/media_movies_add1.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.add('My Movie', 'PG-13', director=('George Lucas', 'J. J. Abrams'))
diff --git a/tests/pyscript/media_movies_add2.py b/tests/pyscript/media_movies_add2.py
index 5c4617ae..681095d7 100644
--- a/tests/pyscript/media_movies_add2.py
+++ b/tests/pyscript/media_movies_add2.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.add('My Movie', 'PG-13', actor=('Mark Hamill'), director=('George Lucas', 'J. J. Abrams'))
diff --git a/tests/pyscript/media_movies_list1.py b/tests/pyscript/media_movies_list1.py
index 0124bbcb..edbc2021 100644
--- a/tests/pyscript/media_movies_list1.py
+++ b/tests/pyscript/media_movies_list1.py
@@ -1 +1,2 @@
-app.media.movies.list() \ No newline at end of file
+app.cmd_echo = True
+app.media.movies.list()
diff --git a/tests/pyscript/media_movies_list2.py b/tests/pyscript/media_movies_list2.py
index 83f6c8ff..5ad01b7b 100644
--- a/tests/pyscript/media_movies_list2.py
+++ b/tests/pyscript/media_movies_list2.py
@@ -1 +1,2 @@
-app.media().movies().list() \ No newline at end of file
+app.cmd_echo = True
+app.media().movies().list()
diff --git a/tests/pyscript/media_movies_list3.py b/tests/pyscript/media_movies_list3.py
index 4fcf1288..bdbdfceb 100644
--- a/tests/pyscript/media_movies_list3.py
+++ b/tests/pyscript/media_movies_list3.py
@@ -1 +1,2 @@
-app('media movies list') \ No newline at end of file
+app.cmd_echo = True
+app('media movies list')
diff --git a/tests/pyscript/media_movies_list4.py b/tests/pyscript/media_movies_list4.py
index 1165b0c5..5f7bdaa9 100644
--- a/tests/pyscript/media_movies_list4.py
+++ b/tests/pyscript/media_movies_list4.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(actor='Mark Hamill')
diff --git a/tests/pyscript/media_movies_list5.py b/tests/pyscript/media_movies_list5.py
index 962b1516..fa4efa5b 100644
--- a/tests/pyscript/media_movies_list5.py
+++ b/tests/pyscript/media_movies_list5.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(actor=('Mark Hamill', 'Carrie Fisher'))
diff --git a/tests/pyscript/media_movies_list6.py b/tests/pyscript/media_movies_list6.py
index 5f8d3654..ef1851cd 100644
--- a/tests/pyscript/media_movies_list6.py
+++ b/tests/pyscript/media_movies_list6.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(rating='PG')
diff --git a/tests/pyscript/media_movies_list7.py b/tests/pyscript/media_movies_list7.py
index bb0e28bb..7c827b7f 100644
--- a/tests/pyscript/media_movies_list7.py
+++ b/tests/pyscript/media_movies_list7.py
@@ -1 +1,2 @@
+app.cmd_echo = True
app.media.movies.list(rating=('PG', 'PG-13'))
diff --git a/tests/pyscript/pyscript_dir1.py b/tests/pyscript/pyscript_dir1.py
new file mode 100644
index 00000000..14a70a31
--- /dev/null
+++ b/tests/pyscript/pyscript_dir1.py
@@ -0,0 +1,3 @@
+out = dir(app)
+out.sort()
+print(out)
diff --git a/tests/pyscript/pyscript_dir2.py b/tests/pyscript/pyscript_dir2.py
new file mode 100644
index 00000000..28c61c8e
--- /dev/null
+++ b/tests/pyscript/pyscript_dir2.py
@@ -0,0 +1,3 @@
+out = dir(app.media)
+out.sort()
+print(out)
diff --git a/tests/scripts/recursive.py b/tests/scripts/recursive.py
index 32c981b6..4c29d317 100644
--- a/tests/scripts/recursive.py
+++ b/tests/scripts/recursive.py
@@ -3,4 +3,5 @@
"""
Example demonstrating that running a Python script recursively inside another Python script isn't allowed
"""
+app.cmd_echo = True
app('pyscript ../script.py')
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index 1d0c9678..e0a71831 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -168,7 +168,7 @@ def test_autocomp_subcmd_nested(cmd2_app):
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
assert first_match is not None and \
- cmd2_app.completion_matches == ['add', 'delete', 'list']
+ cmd2_app.completion_matches == ['add', 'delete', 'list', 'load']
def test_autocomp_subcmd_flag_choices_append(cmd2_app):
@@ -246,7 +246,7 @@ def test_autcomp_pos_consumed(cmd2_app):
def test_autcomp_pos_after_flag(cmd2_app):
text = 'Joh'
- line = 'media movies add -d "George Lucas" -- "Han Solo" PG "Emilia Clarke" "{}'.format(text)
+ line = 'video movies add -d "George Lucas" -- "Han Solo" PG "Emilia Clarke" "{}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
diff --git a/tests/test_bashcompletion.py b/tests/test_bashcompletion.py
index ceae2aa9..298bdf1e 100644
--- a/tests/test_bashcompletion.py
+++ b/tests/test_bashcompletion.py
@@ -139,15 +139,13 @@ def test_invalid_ifs(parser1, mock):
@pytest.mark.parametrize('comp_line, exp_out, exp_err', [
('media ', 'movies\013shows', ''),
('media mo', 'movies', ''),
+ ('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
+ ('media movies list ', '', ''),
('media movies add ', '\013\013 ', '''
Hint:
TITLE Movie Title'''),
- ('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
- ('media movies list ', '', '')
])
def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
- completer = CompletionFinder()
-
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
'_ARGCOMPLETE_IFS': '\013',
'COMP_TYPE': '63',
@@ -157,6 +155,8 @@ def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
mock.patch.object(os, 'fdopen', my_fdopen)
with pytest.raises(SystemExit):
+ completer = CompletionFinder()
+
choices = {'actor': query_actors, # function
}
autocompleter = AutoCompleter(parser1, arg_choices=choices)
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 11c2cad8..c66b1264 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1427,7 +1427,7 @@ def test_clipboard_failure(capsys):
# Make sure we got the error output
out, err = capsys.readouterr()
assert out == ''
- assert 'Cannot redirect to paste buffer; install ``xclip`` and re-run to enable' in err
+ assert "Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable" in err
class CmdResultApp(cmd2.Cmd):
@@ -1685,12 +1685,6 @@ def test_alias_lookup_invalid_alias(base_app, capsys):
out, err = capsys.readouterr()
assert "not found" in err
-def test_alias_with_invalid_name(base_app, capsys):
- run_cmd(base_app, 'alias @ help')
- out, err = capsys.readouterr()
- assert "can only contain the following characters" in err
-
-
def test_unalias(base_app):
# Create an alias
run_cmd(base_app, 'alias fake pyscript')
@@ -1708,6 +1702,18 @@ def test_unalias_non_existing(base_app, capsys):
out, err = capsys.readouterr()
assert "does not exist" in err
+@pytest.mark.parametrize('alias_name', [
+ '">"',
+ '"no>pe"',
+ '"no spaces"',
+ '"nopipe|"',
+ '"noterm;"',
+ 'noembedded"quotes',
+])
+def test_create_invalid_alias(base_app, alias_name, capsys):
+ run_cmd(base_app, 'alias {} help'.format(alias_name))
+ out, err = capsys.readouterr()
+ assert "can not contain" in err
def test_ppaged(base_app):
msg = 'testing...'
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 2d1ee2b7..2faa4a08 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -13,8 +13,7 @@ import os
import sys
import pytest
-from cmd2 import cmd2
-import cmd2.submenu
+import cmd2
from .conftest import complete_tester, StdOut
from examples.subcommands import SubcommandsExample
@@ -950,124 +949,3 @@ def test_subcommand_tab_completion_space_in_text_scu(scu_app):
assert first_match is not None and \
scu_app.completion_matches == ['Ball" '] and \
scu_app.display_matches == ['Space Ball']
-
-####################################################
-
-
-class SecondLevel(cmd2.Cmd):
- """To be used as a second level command class. """
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = '2ndLevel '
-
- def do_foo(self, line):
- self.poutput("You called a command in SecondLevel with '%s'. " % line)
-
- def help_foo(self):
- self.poutput("This is a second level menu. Options are qwe, asd, zxc")
-
- def complete_foo(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
-
-second_level_cmd = SecondLevel()
-
-
-@cmd2.submenu.AddSubmenu(second_level_cmd,
- command='second',
- require_predefined_shares=False)
-class SubmenuApp(cmd2.Cmd):
- """To be used as the main / top level command class that will contain other submenus."""
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = 'TopLevel '
-
-
-@pytest.fixture
-def sb_app():
- app = SubmenuApp()
- return app
-
-
-def test_cmd2_submenu_completion_single_end(sb_app):
- text = 'f'
- line = 'second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
-
- # It is at end of line, so extra space is present
- assert first_match is not None and sb_app.completion_matches == ['foo ']
-
-
-def test_cmd2_submenu_completion_multiple(sb_app):
- text = 'e'
- line = 'second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- expected = ['edit', 'eof', 'eos']
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
-
- assert first_match is not None and sb_app.completion_matches == expected
-
-
-def test_cmd2_submenu_completion_nomatch(sb_app):
- text = 'z'
- line = 'second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
- assert first_match is None
-
-
-def test_cmd2_submenu_completion_after_submenu_match(sb_app):
- text = 'a'
- line = 'second foo {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
- assert first_match is not None and sb_app.completion_matches == ['asd ']
-
-
-def test_cmd2_submenu_completion_after_submenu_nomatch(sb_app):
- text = 'b'
- line = 'second foo {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
- assert first_match is None
-
-
-def test_cmd2_help_submenu_completion_multiple(sb_app):
- text = 'p'
- line = 'help second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- matches = sorted(sb_app.complete_help(text, line, begidx, endidx))
- assert matches == ['py', 'pyscript']
-
-
-def test_cmd2_help_submenu_completion_nomatch(sb_app):
- text = 'fake'
- line = 'help second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
- assert sb_app.complete_help(text, line, begidx, endidx) == []
-
-
-def test_cmd2_help_submenu_completion_subcommands(sb_app):
- text = 'p'
- line = 'help second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- matches = sorted(sb_app.complete_help(text, line, begidx, endidx))
- assert matches == ['py', 'pyscript']
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 19237f6e..7b361b7e 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -16,7 +16,7 @@ from cmd2 import utils
def parser():
parser = StatementParser(
allow_redirection=True,
- terminators=[';'],
+ terminators=[';', '&'],
multiline_commands=['multiline'],
aliases={'helpalias': 'help',
'42': 'theanswer',
@@ -38,7 +38,13 @@ def test_parse_empty_string(parser):
('command /* with some comment */ arg', ['command', 'arg']),
('command arg1 arg2 # comment at the end', ['command', 'arg1', 'arg2']),
('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']),
- ('l', ['shell', 'ls', '-al'])
+ ('l', ['shell', 'ls', '-al']),
+ ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']),
+ ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']),
+ ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']),
+ ('termbare& > /tmp/output', ['termbare', '&', '>', '/tmp/output']),
+ ('help|less', ['help', '|', 'less']),
+ ('l|less', ['shell', 'ls', '-al', '|', 'less']),
])
def test_tokenize(parser, line, tokens):
tokens_to_test = parser.tokenize(line)
@@ -46,7 +52,7 @@ def test_tokenize(parser, line, tokens):
def test_tokenize_unclosed_quotes(parser):
with pytest.raises(ValueError):
- tokens = parser.tokenize('command with "unclosed quotes')
+ _ = parser.tokenize('command with "unclosed quotes')
@pytest.mark.parametrize('tokens,command,args', [
([], None, None),
@@ -69,18 +75,28 @@ def test_parse_single_word(parser, line):
assert not statement.args
assert statement.argv == [utils.strip_quotes(line)]
-def test_parse_word_plus_terminator(parser):
- line = 'termbare;'
+@pytest.mark.parametrize('line,terminator', [
+ ('termbare;', ';'),
+ ('termbare ;', ';'),
+ ('termbare&', '&'),
+ ('termbare &', '&'),
+])
+def test_parse_word_plus_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
- assert statement.terminator == ';'
+ assert statement.terminator == terminator
assert statement.argv == ['termbare']
-def test_parse_suffix_after_terminator(parser):
- line = 'termbare; suffx'
+@pytest.mark.parametrize('line,terminator', [
+ ('termbare; suffx', ';'),
+ ('termbare ;suffx', ';'),
+ ('termbare& suffx', '&'),
+ ('termbare &suffx', '&'),
+])
+def test_parse_suffix_after_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
- assert statement.terminator == ';'
+ assert statement.terminator == terminator
assert statement.suffix == 'suffx'
assert statement.argv == ['termbare']
@@ -134,12 +150,16 @@ def test_parse_what_if_quoted_strings_seem_to_start_comments(parser):
assert not statement.pipe_to
assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?']
-def test_parse_simple_piped(parser):
- statement = parser.parse('simple | piped')
+@pytest.mark.parametrize('line',[
+ 'simple | piped',
+ 'simple|piped',
+])
+def test_parse_simple_pipe(parser, line):
+ statement = parser.parse(line)
assert statement.command == 'simple'
assert not statement.args
assert statement.argv == ['simple']
- assert statement.pipe_to == 'piped'
+ assert statement.pipe_to == ['piped']
def test_parse_double_pipe_is_not_a_pipe(parser):
line = 'double-pipe || is not a pipe'
@@ -150,16 +170,29 @@ def test_parse_double_pipe_is_not_a_pipe(parser):
assert not statement.pipe_to
def test_parse_complex_pipe(parser):
- line = 'command with args, terminator;sufx | piped'
+ line = 'command with args, terminator&sufx | piped'
statement = parser.parse(line)
assert statement.command == 'command'
assert statement.args == "with args, terminator"
assert statement.argv == ['command', 'with', 'args,', 'terminator']
- assert statement.terminator == ';'
+ assert statement.terminator == '&'
assert statement.suffix == 'sufx'
- assert statement.pipe_to == 'piped'
+ assert statement.pipe_to == ['piped']
+
+@pytest.mark.parametrize('line,output', [
+ ('help > out.txt', '>'),
+ ('help>out.txt', '>'),
+ ('help >> out.txt', '>>'),
+ ('help>>out.txt', '>>'),
+])
+def test_parse_redirect(parser,line, output):
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.output == output
+ assert statement.output_to == 'out.txt'
-def test_parse_output_redirect(parser):
+def test_parse_redirect_with_args(parser):
line = 'output into > afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
@@ -168,7 +201,7 @@ def test_parse_output_redirect(parser):
assert statement.output == '>'
assert statement.output_to == 'afile.txt'
-def test_parse_output_redirect_with_dash_in_path(parser):
+def test_parse_redirect_with_dash_in_path(parser):
line = 'output into > python-cmd2/afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
@@ -177,7 +210,7 @@ def test_parse_output_redirect_with_dash_in_path(parser):
assert statement.output == '>'
assert statement.output_to == 'python-cmd2/afile.txt'
-def test_parse_output_redirect_append(parser):
+def test_parse_redirect_append(parser):
line = 'output appended to >> /tmp/afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
@@ -194,9 +227,9 @@ def test_parse_pipe_and_redirect(parser):
assert statement.argv == ['output', 'into']
assert statement.terminator == ';'
assert statement.suffix == 'sufx'
- assert statement.pipe_to == 'pipethrume plz'
- assert statement.output == '>'
- assert statement.output_to == 'afile.txt'
+ assert statement.pipe_to == ['pipethrume', 'plz', '>', 'afile.txt']
+ assert not statement.output
+ assert not statement.output_to
def test_parse_output_to_paste_buffer(parser):
line = 'output to paste buffer >> '
@@ -207,8 +240,9 @@ def test_parse_output_to_paste_buffer(parser):
assert statement.output == '>>'
def test_parse_redirect_inside_terminator(parser):
- """The terminator designates the end of the commmand/arguments portion. If a redirector
- occurs before a terminator, then it will be treated as part of the arguments and not as a redirector."""
+ """The terminator designates the end of the commmand/arguments portion.
+ If a redirector occurs before a terminator, then it will be treated as
+ part of the arguments and not as a redirector."""
line = 'has > inside;'
statement = parser.parse(line)
assert statement.command == 'has'
@@ -216,6 +250,23 @@ def test_parse_redirect_inside_terminator(parser):
assert statement.argv == ['has', '>', 'inside']
assert statement.terminator == ';'
+@pytest.mark.parametrize('line,terminator',[
+ ('multiline with | inside;', ';'),
+ ('multiline with | inside ;', ';'),
+ ('multiline with | inside;;;', ';'),
+ ('multiline with | inside;; ;;', ';'),
+ ('multiline with | inside&', '&'),
+ ('multiline with | inside &;', '&'),
+ ('multiline with | inside&&;', '&'),
+ ('multiline with | inside &; &;', '&'),
+])
+def test_parse_multiple_terminators(parser, line, terminator):
+ statement = parser.parse(line)
+ assert statement.multiline_command == 'multiline'
+ assert statement.args == 'with | inside'
+ assert statement.argv == ['multiline', 'with', '|', 'inside']
+ assert statement.terminator == terminator
+
def test_parse_unfinished_multiliine_command(parser):
line = 'multiline has > inside an unfinished command'
statement = parser.parse(line)
@@ -225,13 +276,19 @@ def test_parse_unfinished_multiliine_command(parser):
assert statement.argv == ['multiline', 'has', '>', 'inside', 'an', 'unfinished', 'command']
assert not statement.terminator
-def test_parse_multiline_command_ignores_redirectors_within_it(parser):
- line = 'multiline has > inside;'
+@pytest.mark.parametrize('line,terminator',[
+ ('multiline has > inside;', ';'),
+ ('multiline has > inside;;;', ';'),
+ ('multiline has > inside;; ;;', ';'),
+ ('multiline has > inside &', '&'),
+ ('multiline has > inside & &', '&'),
+])
+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.argv == ['multiline', 'has', '>', 'inside']
- assert statement.terminator == ';'
+ assert statement.terminator == terminator
def test_parse_multiline_with_incomplete_comment(parser):
"""A terminator within a comment will be ignored and won't terminate a multiline command.
@@ -296,7 +353,7 @@ def test_parse_redirect_to_unicode_filename(parser):
def test_parse_unclosed_quotes(parser):
with pytest.raises(ValueError):
- tokens = parser.tokenize("command with 'unclosed quotes")
+ _ = parser.tokenize("command with 'unclosed quotes")
def test_empty_statement_raises_exception():
app = cmd2.Cmd()
@@ -315,12 +372,12 @@ def test_empty_statement_raises_exception():
('!ls -al /tmp', 'shell', 'ls -al /tmp'),
('l', 'shell', 'ls -al')
])
-def test_alias_and_shortcut_expansion(parser, line, command, args):
+def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
statement = parser.parse(line)
assert statement.command == command
assert statement.args == args
-def test_alias_on_multiline_command(parser):
+def test_parse_alias_on_multiline_command(parser):
line = 'anothermultiline has > inside an unfinished command'
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
@@ -328,6 +385,43 @@ def test_alias_on_multiline_command(parser):
assert statement.args == 'has > inside an unfinished command'
assert not statement.terminator
+@pytest.mark.parametrize('line,output', [
+ ('helpalias > out.txt', '>'),
+ ('helpalias>out.txt', '>'),
+ ('helpalias >> out.txt', '>>'),
+ ('helpalias>>out.txt', '>>'),
+])
+def test_parse_alias_redirection(parser, line, output):
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.output == output
+ assert statement.output_to == 'out.txt'
+
+@pytest.mark.parametrize('line', [
+ 'helpalias | less',
+ 'helpalias|less',
+])
+def test_parse_alias_pipe(parser, line):
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.pipe_to == ['less']
+
+@pytest.mark.parametrize('line', [
+ 'helpalias;',
+ 'helpalias;;',
+ 'helpalias;; ;',
+ 'helpalias ;',
+ 'helpalias ; ;',
+ 'helpalias ;; ;',
+])
+def test_parse_alias_terminator_no_whitespace(parser, line):
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.terminator == ';'
+
def test_parse_command_only_command_and_args(parser):
line = 'help history'
statement = parser.parse_command_only(line)
@@ -373,3 +467,34 @@ def test_parse_command_only_quoted_args(parser):
assert statement.command == 'shell'
assert statement.args == 'ls -al "/tmp/directory with spaces/doit.sh"'
assert statement.command_and_args == line.replace('l', 'shell ls -al')
+
+@pytest.mark.parametrize('line', [
+ 'helpalias > out.txt',
+ 'helpalias>out.txt',
+ 'helpalias >> out.txt',
+ 'helpalias>>out.txt',
+ 'help|less',
+ 'helpalias;',
+ 'help ;;',
+ 'help; ;;',
+])
+def test_parse_command_only_specialchars(parser, line):
+ statement = parser.parse_command_only(line)
+ assert statement.command == 'help'
+
+@pytest.mark.parametrize('line', [
+ ';',
+ ';;',
+ ';; ;',
+ '&',
+ '& &',
+ ' && &',
+ '>',
+ "'",
+ '"',
+ '|',
+])
+def test_parse_command_only_none(parser, line):
+ statement = parser.parse_command_only(line)
+ assert statement.command == None
+ assert statement.args == None
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 8d0cefd8..73c1a62a 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -101,7 +101,14 @@ class PyscriptExample(Cmd):
@with_argparser(bar_parser)
def do_bar(self, args):
- print('bar ' + str(args.__dict__))
+ out = 'bar '
+ arg_dict = args.__dict__
+ keys = list(arg_dict.keys())
+ keys.sort()
+ out += '{'
+ for key in keys:
+ out += "'{}':'{}'".format(key, arg_dict[key])
+ print(out)
@pytest.fixture
@@ -160,7 +167,7 @@ def test_pyscript_help(ps_app, capsys, request, command, pyscript_file):
('foo aaa bbb -ccc -t -n', 'foo1.py'),
('foo 11 22 33 44 -ccc -t -n', 'foo2.py'),
('foo 11 22 33 44 55 66 -ccc', 'foo3.py'),
- ('bar 11 22', 'bar1.py')
+ ('bar 11 22', 'bar1.py'),
])
def test_pyscript_out(ps_app, capsys, request, command, pyscript_file):
test_dir = os.path.dirname(request.module.__file__)
@@ -204,11 +211,30 @@ def test_pyscript_results(ps_app, capsys, request, pyscript_file, exp_out):
assert exp_out in expected
-def test_pyscript_custom_name(ps_echo, capsys):
+@pytest.mark.parametrize('expected, pyscript_file', [
+ ("['_relative_load', 'alias', 'bar', 'cmd_echo', 'edit', 'eof', 'eos', 'foo', 'help', 'history', 'load', 'media', 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts', 'unalias']",
+ 'pyscript_dir1.py'),
+ ("['movies', 'shows']", 'pyscript_dir2.py')
+])
+def test_pyscript_dir(ps_app, capsys, request, expected, pyscript_file):
+ test_dir = os.path.dirname(request.module.__file__)
+ python_script = os.path.join(test_dir, 'pyscript', pyscript_file)
+
+ run_cmd(ps_app, 'pyscript {}'.format(python_script))
+ out, _ = capsys.readouterr()
+ out = out.strip()
+ assert len(out) > 0
+ assert out == expected
+
+
+def test_pyscript_custom_name(ps_echo, capsys, request):
message = 'blah!'
- run_cmd(ps_echo, 'py custom.echo("{}")'.format(message))
+
+ test_dir = os.path.dirname(request.module.__file__)
+ python_script = os.path.join(test_dir, 'pyscript', 'custom_echo.py')
+
+ run_cmd(ps_echo, 'pyscript {}'.format(python_script))
expected, _ = capsys.readouterr()
assert len(expected) > 0
expected = expected.splitlines()
assert message == expected[0]
-
diff --git a/tests/test_submenu.py b/tests/test_submenu.py
deleted file mode 100644
index 2603c536..00000000
--- a/tests/test_submenu.py
+++ /dev/null
@@ -1,182 +0,0 @@
-# coding=utf-8
-"""
-Cmd2 testing for argument parsing
-"""
-import pytest
-
-from cmd2 import cmd2
-import cmd2.submenu
-from .conftest import run_cmd, StdOut, normalize
-
-
-class SecondLevelB(cmd2.Cmd):
- """To be used as a second level command class. """
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = '2ndLevel B '
-
- def do_get_top_level_attr(self, line):
- self.poutput(str(self.top_level_attr))
-
- def do_set_top_level_attr(self, line):
- self.top_level_attr = 987654321
-
-
-class SecondLevel(cmd2.Cmd):
- """To be used as a second level command class. """
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = '2ndLevel '
- self.top_level_attr = None
-
- def do_say(self, line):
- self.poutput("You called a command in SecondLevel with '%s'. " % line)
-
- def help_say(self):
- self.poutput("This is a second level menu. Options are qwe, asd, zxc")
-
- def complete_say(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
- def do_get_top_level_attr(self, line):
- self.poutput(str(self.top_level_attr))
-
- def do_get_prompt(self, line):
- self.poutput(self.prompt)
-
-
-second_level_cmd = SecondLevel()
-second_level_b_cmd = SecondLevelB()
-
-
-@cmd2.submenu.AddSubmenu(SecondLevelB(),
- command='should_work_with_default_kwargs')
-@cmd2.submenu.AddSubmenu(second_level_b_cmd,
- command='secondb',
- shared_attributes=dict(top_level_attr='top_level_attr'),
- require_predefined_shares=False,
- preserve_shares=True
- )
-@cmd2.submenu.AddSubmenu(second_level_cmd,
- command='second',
- aliases=('second_alias',),
- shared_attributes=dict(top_level_attr='top_level_attr'))
-class SubmenuApp(cmd2.Cmd):
- """To be used as the main / top level command class that will contain other submenus."""
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = 'TopLevel '
- self.top_level_attr = 123456789
-
- def do_say(self, line):
- self.poutput("You called a command in TopLevel with '%s'. " % line)
-
- def help_say(self):
- self.poutput("This is a top level submenu. Options are qwe, asd, zxc")
-
- def complete_say(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
-
-@pytest.fixture
-def submenu_app():
- app = SubmenuApp()
- app.stdout = StdOut()
- second_level_cmd.stdout = StdOut()
- second_level_b_cmd.stdout = StdOut()
- return app
-
-
-@pytest.fixture
-def secondlevel_app():
- app = SecondLevel()
- app.stdout = StdOut()
- return app
-
-
-@pytest.fixture
-def secondlevel_app_b():
- app = SecondLevelB()
- app.stdout = StdOut()
- return app
-
-
-def run_submenu_cmd(app, second_level_app, cmd):
- """ Clear StdOut buffers, run the command, extract the buffer contents."""
- app.stdout.clear()
- second_level_app.stdout.clear()
- app.onecmd_plus_hooks(cmd)
- out1 = app.stdout.buffer
- out2 = second_level_app.stdout.buffer
- app.stdout.clear()
- second_level_app.stdout.clear()
- return normalize(out1), normalize(out2)
-
-
-def test_submenu_say_from_top_level(submenu_app):
- line = 'testing'
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'say ' + line)
- assert len(out1) == 1
- assert len(out2) == 0
- assert out1[0] == "You called a command in TopLevel with {!r}.".format(line)
-
-
-def test_submenu_second_say_from_top_level(submenu_app):
- line = 'testing'
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second say ' + line)
-
- # No output expected from the top level
- assert out1 == []
-
- # Output expected from the second level
- assert len(out2) == 1
- assert out2[0] == "You called a command in SecondLevel with {!r}.".format(line)
-
-
-def test_submenu_say_from_second_level(secondlevel_app):
- line = 'testing'
- out = run_cmd(secondlevel_app, 'say ' + line)
- assert out == ["You called a command in SecondLevel with '%s'." % line]
-
-
-def test_submenu_help_second_say_from_top_level(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say')
- # No output expected from the top level
- assert out1 == []
-
- # Output expected from the second level
- assert out2 == ["This is a second level menu. Options are qwe, asd, zxc"]
-
-
-def test_submenu_help_say_from_second_level(secondlevel_app):
- out = run_cmd(secondlevel_app, 'help say')
- assert out == ["This is a second level menu. Options are qwe, asd, zxc"]
-
-
-def test_submenu_help_second(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second')
- out3 = run_cmd(second_level_cmd, 'help')
- assert out2 == out3
-
-
-def test_submenu_from_top_help_second_say(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say')
- out3 = run_cmd(second_level_cmd, 'help say')
- assert out2 == out3
-
-
-def test_submenu_shared_attribute(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second get_top_level_attr')
- assert out2 == [str(submenu_app.top_level_attr)]
-
-
-def test_submenu_shared_attribute_preserve(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr')
- assert out2 == [str(submenu_app.top_level_attr)]
- out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb set_top_level_attr')
- assert submenu_app.top_level_attr == 987654321
- out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr')
- assert out2 == [str(987654321)]