summaryrefslogtreecommitdiff
path: root/cmd2/cmd2.py
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2/cmd2.py')
-rw-r--r--cmd2/cmd2.py154
1 files changed, 80 insertions, 74 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 8f2cdca3..b314a683 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -47,13 +47,14 @@ from . import ansi
from . import constants
from . import plugin
from . import utils
-from .argparse_custom import CompletionError, CompletionItem, DEFAULT_ARGUMENT_PARSER
+from .argparse_custom import CompletionItem, DEFAULT_ARGUMENT_PARSER
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .decorators import with_argparser
+from .exceptions import EmbeddedConsoleExit, EmptyStatement
from .history import History, HistoryItem
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt, rl_warning
-from .utils import Settable
+from .utils import CompletionError, Settable
# Set up readline
if rl_type == RlType.NONE: # pragma: no cover
@@ -106,16 +107,6 @@ class _SavedCmd2Env:
self.sys_stdin = None
-class EmbeddedConsoleExit(SystemExit):
- """Custom exception class for use with the py command."""
- pass
-
-
-class EmptyStatement(Exception):
- """Custom exception class for handling behavior when the user just presses <Enter>."""
- pass
-
-
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function'])
@@ -1050,8 +1041,8 @@ class Cmd(cmd.Cmd):
in_pipe = False
in_file_redir = True
- # Not a redirection token
- else:
+ # Only tab complete after redirection tokens if redirection is allowed
+ elif self.allow_redirection:
do_shell_completion = False
do_path_completion = False
@@ -1263,7 +1254,7 @@ class Cmd(cmd.Cmd):
if func is not None and argparser is not None:
import functools
- compfunc = functools.partial(self._autocomplete_default,
+ compfunc = functools.partial(self._complete_argparse_command,
argparser=argparser,
preserve_quotes=getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES))
else:
@@ -1416,17 +1407,27 @@ class Cmd(cmd.Cmd):
except IndexError:
return None
+ except CompletionError as ex:
+ # Don't print error and redraw the prompt unless the error has length
+ err_str = str(ex)
+ if err_str:
+ if ex.apply_style:
+ err_str = ansi.style_error(err_str)
+ ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n')
+ rl_force_redisplay()
+ return None
except Exception as e:
# Insert a newline so the exception doesn't print in the middle of the command line being tab completed
self.perror()
self.pexcept(e)
+ rl_force_redisplay()
return None
- def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int, *,
- argparser: argparse.ArgumentParser, preserve_quotes: bool) -> List[str]:
- """Default completion function for argparse commands"""
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(argparser, self)
+ def _complete_argparse_command(self, text: str, line: str, begidx: int, endidx: int, *,
+ argparser: argparse.ArgumentParser, preserve_quotes: bool) -> List[str]:
+ """Completion function for argparse commands"""
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(argparser, self)
tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
# To have tab-completion parsing match command line parsing behavior,
@@ -2560,11 +2561,11 @@ class Cmd(cmd.Cmd):
if func is None or argparser is None:
return []
- # Combine the command and its subcommand tokens for the AutoCompleter
+ # Combine the command and its subcommand tokens for the ArgparseCompleter
tokens = [command] + arg_tokens['subcommands']
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(argparser, self)
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(argparser, self)
return completer.complete_subcommand_help(tokens, text, line, begidx, endidx)
help_parser = DEFAULT_ARGUMENT_PARSER(description="List available commands or provide "
@@ -2576,7 +2577,7 @@ class Cmd(cmd.Cmd):
help_parser.add_argument('-v', '--verbose', action='store_true',
help="print a list of all commands with descriptions of each")
- # Get rid of cmd's complete_help() functions so AutoCompleter will complete the help command
+ # Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command
if getattr(cmd.Cmd, 'complete_help', None) is not None:
delattr(cmd.Cmd, 'complete_help')
@@ -2594,8 +2595,8 @@ class Cmd(cmd.Cmd):
# If the command function uses argparse, then use argparse's help
if func is not None and argparser is not None:
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(argparser, self)
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(argparser, self)
tokens = [args.command] + args.subcommands
# Set end to blank so the help output matches how it looks when "command -h" is used
@@ -2838,8 +2839,8 @@ class Cmd(cmd.Cmd):
completer_function=settable.completer_function,
completer_method=settable.completer_method)
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(settable_parser, self)
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(settable_parser, self)
# Use raw_tokens since quotes have been preserved
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
@@ -2860,7 +2861,7 @@ class Cmd(cmd.Cmd):
set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
# Suppress tab-completion hints for this field. The completer method is going to create an
- # AutoCompleter based on the actual parameter being completed and we only want that hint printing.
+ # ArgparseCompleter based on the actual parameter being completed and we only want that hint printing.
set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable',
completer_method=complete_set_value, suppress_tab_hint=True)
@@ -3093,8 +3094,7 @@ class Cmd(cmd.Cmd):
# This is a hidden flag for telling do_py to run a pyscript. It is intended only to be used by run_pyscript
# after it sets up sys.argv for the script being run. When this flag is present, it takes precedence over all
- # other arguments. run_pyscript uses this method instead of "py run('file')" because file names with
- # 2 or more consecutive spaces cause issues with our parser, which isn't meant to parse Python statements.
+ # other arguments.
py_parser.add_argument('--pyscript', help=argparse.SUPPRESS)
# Preserve quotes since we are passing these strings to Python
@@ -3104,65 +3104,69 @@ class Cmd(cmd.Cmd):
Enter an interactive Python shell
:return: True if running of commands should stop
"""
+ def py_quit():
+ """Function callable from the interactive Python console to exit that environment"""
+ raise EmbeddedConsoleExit
+
from .py_bridge import PyBridge
+ py_bridge = PyBridge(self)
+ saved_sys_path = None
+
if self.in_pyscript():
err = "Recursively entering interactive Python consoles is not allowed."
self.perror(err)
return
- py_bridge = PyBridge(self)
- py_code_to_run = ''
-
- # Handle case where we were called by run_pyscript
- if args.pyscript:
- args.pyscript = utils.strip_quotes(args.pyscript)
-
- # Run the script - use repr formatting to escape things which
- # need to be escaped to prevent issues on Windows
- py_code_to_run = 'run({!r})'.format(args.pyscript)
-
- elif args.command:
- py_code_to_run = args.command
- if args.remainder:
- py_code_to_run += ' ' + ' '.join(args.remainder)
-
- # Set cmd_echo to True so PyBridge statements like: py app('help')
- # run at the command line will print their output.
- py_bridge.cmd_echo = True
-
try:
self._in_py = True
+ py_code_to_run = ''
- def py_run(filename: str):
- """Run a Python script file in the interactive console.
- :param filename: filename of script file to run
- """
- expanded_filename = os.path.expanduser(filename)
+ # Make a copy of self.py_locals for the locals dictionary in the Python environment we are creating.
+ # This is to prevent pyscripts from editing it. (e.g. locals().clear()). It also ensures a pyscript's
+ # environment won't be filled with data from a previously run pyscript. Only make a shallow copy since
+ # it's OK for py_locals to contain objects which are editable in a pyscript.
+ localvars = dict(self.py_locals)
+ localvars[self.py_bridge_name] = py_bridge
+ localvars['quit'] = py_quit
+ localvars['exit'] = py_quit
+
+ if self.self_in_py:
+ localvars['self'] = self
+
+ # Handle case where we were called by run_pyscript
+ if args.pyscript:
+ # Read the script file
+ expanded_filename = os.path.expanduser(utils.strip_quotes(args.pyscript))
try:
with open(expanded_filename) as f:
- interp.runcode(f.read())
+ py_code_to_run = f.read()
except OSError as ex:
self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex))
+ return
- def py_quit():
- """Function callable from the interactive Python console to exit that environment"""
- raise EmbeddedConsoleExit
+ localvars['__name__'] = '__main__'
+ localvars['__file__'] = expanded_filename
- # Set up Python environment
- self.py_locals[self.py_bridge_name] = py_bridge
- self.py_locals['run'] = py_run
- self.py_locals['quit'] = py_quit
- self.py_locals['exit'] = py_quit
+ # Place the script's directory at sys.path[0] just as Python does when executing a script
+ saved_sys_path = list(sys.path)
+ sys.path.insert(0, os.path.dirname(os.path.abspath(expanded_filename)))
- if self.self_in_py:
- self.py_locals['self'] = self
- elif 'self' in self.py_locals:
- del self.py_locals['self']
+ else:
+ # This is the default name chosen by InteractiveConsole when no locals are passed in
+ localvars['__name__'] = '__console__'
- localvars = self.py_locals
+ if args.command:
+ py_code_to_run = args.command
+ if args.remainder:
+ py_code_to_run += ' ' + ' '.join(args.remainder)
+
+ # Set cmd_echo to True so PyBridge statements like: py app('help')
+ # run at the command line will print their output.
+ py_bridge.cmd_echo = True
+
+ # Create the Python interpreter
interp = InteractiveConsole(locals=localvars)
- interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
# Check if we are running Python code
if py_code_to_run:
@@ -3177,8 +3181,7 @@ class Cmd(cmd.Cmd):
else:
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
instructions = ('End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n'
- 'Non-Python commands can be issued with: {}("your command")\n'
- 'Run Python code from external script files with: run("script.py")'
+ 'Non-Python commands can be issued with: {}("your command")'
.format(self.py_bridge_name))
saved_cmd2_env = None
@@ -3205,7 +3208,10 @@ class Cmd(cmd.Cmd):
pass
finally:
- self._in_py = False
+ with self.sigint_protection:
+ if saved_sys_path is not None:
+ sys.path = saved_sys_path
+ self._in_py = False
return py_bridge.stop