summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py142
-rw-r--r--docs/freefeatures.rst10
-rw-r--r--tests/conftest.py16
-rw-r--r--tests/test_cmd2.py30
-rw-r--r--tests/test_pyscript.py2
5 files changed, 94 insertions, 106 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index a2987a65..e3f1b2b6 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2314,7 +2314,7 @@ class Cmd(cmd.Cmd):
@with_argparser(alias_parser)
def do_alias(self, args: argparse.Namespace):
- """ Manages aliases """
+ """Manage aliases"""
func = getattr(args, 'func', None)
if func is not None:
# Call whatever subcommand function was selected
@@ -2518,7 +2518,7 @@ class Cmd(cmd.Cmd):
@with_argparser(macro_parser)
def do_macro(self, args: argparse.Namespace):
- """ Manages macros """
+ """Manage macros"""
func = getattr(args, 'func', None)
if func is not None:
# Call whatever subcommand function was selected
@@ -2666,18 +2666,21 @@ class Cmd(cmd.Cmd):
command = ''
self.stdout.write("\n")
- def do_shortcuts(self, _: str) -> None:
- """Lists shortcuts available"""
+ @with_argparser(ACArgumentParser())
+ def do_shortcuts(self, _: argparse.Namespace) -> None:
+ """List 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:
+ @with_argparser(ACArgumentParser())
+ def do_eof(self, _: argparse.Namespace) -> bool:
"""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"""
+ @with_argparser(ACArgumentParser())
+ def do_quit(self, _: argparse.Namespace) -> bool:
+ """Exit this application"""
self._should_quit = True
return self._STOP_AND_EXIT
@@ -2763,7 +2766,7 @@ class Cmd(cmd.Cmd):
else:
raise LookupError("Parameter '{}' not supported (type 'set' for list of parameters).".format(param))
- set_description = "Sets a settable parameter or shows current settings of parameters.\n"
+ set_description = "Set a settable parameter or show current settings of parameters.\n"
set_description += "\n"
set_description += "Accepts abbreviated parameter names so long as there is no ambiguity.\n"
set_description += "Call without arguments for a list of settable parameters with their values."
@@ -2777,7 +2780,7 @@ class Cmd(cmd.Cmd):
@with_argparser(set_parser)
def do_set(self, args: argparse.Namespace) -> None:
- """Sets a settable parameter or shows current settings of parameters"""
+ """Set a settable parameter or shows current settings of parameters"""
# Check if param was passed in
if not args.param:
@@ -2874,16 +2877,14 @@ class Cmd(cmd.Cmd):
sys.displayhook = sys.__displayhook__
sys.excepthook = sys.__excepthook__
- def do_py(self, arg: str) -> bool:
- """
- Invoke python command, shell, or script
+ py_description = "Invoke python command or shell"
+ py_parser = ACArgumentParser(description=py_description)
+ py_parser.add_argument('command', help="command to run", nargs='?')
+ py_parser.add_argument('remainder', help="remainder of command", nargs=argparse.REMAINDER)
- py <command>: Executes a Python command.
- py: Enters interactive Python mode.
- End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
- Non-python commands can be issued with ``pyscript_name("your command")``.
- Run python code from external script files with ``run("script.py")``
- """
+ @with_argparser(py_parser)
+ def do_py(self, args: argparse.Namespace) -> bool:
+ """Invoke python command or shell"""
from .pyscript_bridge import PyscriptBridge, CommandResult
if self._in_py:
err = "Recursively entering interactive Python consoles is not allowed."
@@ -2894,8 +2895,6 @@ class Cmd(cmd.Cmd):
# noinspection PyBroadException
try:
- arg = arg.strip()
-
# Support the run command even if called prior to invoking an interactive interpreter
def run(filename: str):
"""Run a Python script file in the interactive console.
@@ -2921,8 +2920,12 @@ class Cmd(cmd.Cmd):
interp = InteractiveConsole(locals=localvars)
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
- if arg:
- interp.runcode(arg)
+ if args.command:
+ full_command = utils.quote_string_if_needed(args.command)
+ for cur_token in args.remainder:
+ full_command += ' ' + utils.quote_string_if_needed(cur_token)
+
+ interp.runcode(full_command)
# If there are no args, then we will open an interactive Python console
else:
@@ -2987,7 +2990,9 @@ class Cmd(cmd.Cmd):
sys.stdin = self.stdin
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
- docstr = self.do_py.__doc__.replace('pyscript_name', self.pyscript_name)
+ docstr = 'End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n'
+ docstr += 'Non-python commands can be issued with: {}("your command")\n'.format(self.pyscript_name)
+ docstr += 'Run python code from external script files with: run("script.py")\n'
try:
interp.interact(banner="Python {} on {}\n{}\n({})\n{}".
@@ -3034,7 +3039,7 @@ class Cmd(cmd.Cmd):
@with_argument_list
def do_pyscript(self, arglist: List[str]) -> None:
- """\nRuns a python script file inside the console
+ """\nRun a python script file inside the console
Usage: pyscript <script_path> [script_arguments]
@@ -3070,26 +3075,22 @@ Paths or arguments that contain spaces must be enclosed in quotes
# Only include the do_ipy() method if IPython is available on the system
if ipython_available:
- # noinspection PyMethodMayBeStatic,PyUnusedLocal
- def do_ipy(self, arg: str) -> None:
- """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()``.
- """
+ @with_argparser(ACArgumentParser())
+ def do_ipy(self, _: argparse.Namespace) -> None:
+ """Enter an interactive IPython shell"""
from .pyscript_bridge import PyscriptBridge
bridge = PyscriptBridge(self)
+ banner = 'Entering an embedded IPython shell. Type quit() or <Ctrl>-d to exit ...\n'
+ banner += 'Run python code from external files with: run filename.py\n'
+ exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
+
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)
@@ -3253,29 +3254,27 @@ a..b, a:b, a:, ..b items by indices (inclusive)
msg = '{} {} saved to transcript file {!r}'
self.pfeedback(msg.format(len(history), plural, transcript_file))
- @with_argument_list
- def do_edit(self, arglist: List[str]) -> None:
- """Edit a file in a text editor
+ edit_description = "Edit a file in a text editor\n"
+ edit_description += "\n"
+ edit_description += "The editor used is determined by the ``editor`` settable parameter.\n"
+ edit_description += "`set editor (program-name)` to change or set the EDITOR environment variable.\n"
-Usage: edit [file_path]
- Where:
- * file_path - path to a file to open in editor
+ edit_parser = ACArgumentParser(description=edit_description)
+ setattr(edit_parser.add_argument('file_path', help="path to a file to open in editor", nargs="?"),
+ ACTION_ARG_CHOICES, ('path_complete',))
-The editor used is determined by the ``editor`` settable parameter.
-"set editor (program-name)" to change or set the EDITOR environment variable.
-"""
+ @with_argparser(edit_parser)
+ def do_edit(self, args: argparse.Namespace) -> None:
+ """Edit a file in a text editor"""
if not self.editor:
raise EnvironmentError("Please use 'set editor' to specify your text editing program of choice.")
- filename = arglist[0] if arglist else ''
- if filename:
- os.system('"{}" "{}"'.format(self.editor, filename))
- else:
- os.system('"{}"'.format(self.editor))
- def complete_edit(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
- """Enable tab-completion for edit command."""
- index_dict = {1: self.path_complete}
- return self.index_based_complete(text, line, begidx, endidx, index_dict)
+ editor = utils.quote_string_if_needed(self.editor)
+ if args.file_path:
+ expanded_path = utils.quote_string_if_needed(os.path.expanduser(args.file_path))
+ os.system('{} {}'.format(editor, expanded_path))
+ else:
+ os.system('{}'.format(editor))
@property
def _current_script_dir(self) -> Optional[str]:
@@ -3309,30 +3308,26 @@ NOTE: This command is intended to only be used within text file scripts.
file_path = arglist[0].strip()
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
relative_path = os.path.join(self._current_script_dir or '', file_path)
- self.do_load([relative_path])
+ self.do_load(relative_path)
- def do_eos(self, _: str) -> None:
- """Handles cleanup when a script has finished executing"""
+ @with_argparser(ACArgumentParser())
+ def do_eos(self, _: argparse.Namespace) -> None:
+ """Handle 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
-
- Usage: load <file_path>
+ load_description = "Run commands in script file that is encoded as either ASCII or UTF-8 text\n"
+ load_description += "\n"
+ load_description += "Script should contain one command per line, just like command would be typed in console"
- * file_path - a file path pointing to a script
-
-Script should contain one command per line, just like command would be typed in console.
- """
- # If arg is None or arg is an empty string this is an error
- if not arglist:
- self.perror('load command requires a file path', traceback_war=False)
- return
+ load_parser = ACArgumentParser(description=load_description)
+ setattr(load_parser.add_argument('script_path', help="path to the script file"),
+ ACTION_ARG_CHOICES, ('path_complete',))
- file_path = arglist[0].strip()
- expanded_path = os.path.abspath(os.path.expanduser(file_path))
+ @with_argparser(load_parser)
+ def do_load(self, args: argparse.Namespace) -> None:
+ """Run commands in script file that is encoded as either ASCII or UTF-8 text"""
+ expanded_path = os.path.abspath(os.path.expanduser(args.script_path))
# Make sure the path exists and we can access it
if not os.path.exists(expanded_path):
@@ -3366,11 +3361,6 @@ Script should contain one command per line, just like command would be typed in
self._script_dir.append(os.path.dirname(expanded_path))
- def complete_load(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
- """Enable tab-completion for load command."""
- index_dict = {1: self.path_complete}
- return self.index_based_complete(text, line, begidx, endidx, index_dict)
-
def run_transcript_tests(self, callargs: List[str]) -> None:
"""Runs transcript tests for provided file(s).
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index a03a1d08..0a95a829 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -174,13 +174,9 @@ More Python examples:
Type "help", "copyright", "credits" or "license" for more information.
(CmdLineApp)
- Invoke python command, shell, or script
-
- py <command>: Executes a Python command.
- py: Enters interactive Python mode.
- End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
- Non-python commands can be issued with ``app("your command")``.
- Run python code from external script files with ``run("script.py")``
+ End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.
+ Non-python commands can be issued with: app("your command")
+ Run python code from external script files with: run("script.py")
>>> import os
>>> os.uname()
diff --git a/tests/conftest.py b/tests/conftest.py
index faf1847d..1295a633 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -36,18 +36,18 @@ edit history macro pyscript set shortcuts
BASE_HELP_VERBOSE = """
Documented commands (type help <topic>):
================================================================================
-alias Manages aliases
+alias Manage 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
-macro Manages macros
-py Invoke python command, shell, or script
-pyscript Runs a python script file inside the console
-quit Exits this application
-set Sets a settable parameter or shows current settings of parameters
+load Run commands in script file that is encoded as either ASCII or UTF-8 text
+macro Manage macros
+py Invoke python command or shell
+pyscript Run a python script file inside the console
+quit Exit this application
+set Set a settable parameter or shows current settings of parameters
shell Execute a command as if at the OS prompt
-shortcuts Lists shortcuts available
+shortcuts List shortcuts available
"""
# Help text for the history command
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index e07e6af3..d250af26 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -69,7 +69,7 @@ def test_base_argparse_help(base_app, capsys):
assert out1 == out2
assert out1[0].startswith('Usage: set')
assert out1[1] == ''
- assert out1[2].startswith('Sets a settable parameter')
+ assert out1[2].startswith('Set a settable parameter')
def test_base_invalid_option(base_app, capsys):
run_cmd(base_app, 'set -z')
@@ -476,7 +476,7 @@ def test_load_with_empty_args(base_app, capsys):
out, err = capsys.readouterr()
# The load command requires a file path argument, so we should get an error message
- assert "load command requires a file path" in str(err)
+ assert "the following arguments are required" in str(err)
assert base_app.cmdqueue == []
@@ -862,7 +862,8 @@ def test_edit_file(base_app, request, monkeypatch):
run_cmd(base_app, 'edit {}'.format(filename))
# We think we have an editor, so should expect a system call
- m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename))
+ m.assert_called_once_with('{} {}'.format(utils.quote_string_if_needed(base_app.editor),
+ utils.quote_string_if_needed(filename)))
def test_edit_file_with_spaces(base_app, request, monkeypatch):
# Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
@@ -878,7 +879,8 @@ def test_edit_file_with_spaces(base_app, request, monkeypatch):
run_cmd(base_app, 'edit "{}"'.format(filename))
# We think we have an editor, so should expect a system call
- m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename))
+ m.assert_called_once_with('{} {}'.format(utils.quote_string_if_needed(base_app.editor),
+ utils.quote_string_if_needed(filename)))
def test_edit_blank(base_app, monkeypatch):
# Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
@@ -1253,17 +1255,17 @@ diddly This command does diddly
Other
================================================================================
-alias Manages aliases
+alias Manage 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
-macro Manages macros
-py Invoke python command, shell, or script
-pyscript Runs a python script file inside the console
-quit Exits this application
-set Sets a settable parameter or shows current settings of parameters
+load Run commands in script file that is encoded as either ASCII or UTF-8 text
+macro Manage macros
+py Invoke python command or shell
+pyscript Run a python script file inside the console
+quit Exit this application
+set Set a settable parameter or shows current settings of parameters
shell Execute a command as if at the OS prompt
-shortcuts Lists shortcuts available
+shortcuts List shortcuts available
Undocumented commands:
======================
@@ -1560,7 +1562,7 @@ def test_is_text_file_bad_input(base_app):
def test_eof(base_app):
# Only thing to verify is that it returns True
- assert base_app.do_eof('dont care')
+ assert base_app.do_eof('')
def test_eos(base_app):
sdir = 'dummy_dir'
@@ -1568,7 +1570,7 @@ def test_eos(base_app):
assert len(base_app._script_dir) == 1
# Assert that it does NOT return true
- assert not base_app.do_eos('dont care')
+ assert not base_app.do_eos('')
# And make sure it reduced the length of the script dir list
assert len(base_app._script_dir) == 0
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 84abc965..c7769c7a 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -84,7 +84,7 @@ class PyscriptExample(Cmd):
@with_argparser(foo_parser)
def do_foo(self, args):
- self.poutput('foo ' + str(args.__dict__))
+ self.poutput('foo ' + str(sorted(args.__dict__)))
if self._in_py:
FooResult = namedtuple_with_defaults('FooResult',
['counter', 'trueval', 'constval',