summaryrefslogtreecommitdiff
path: root/cmd2/cmd2.py
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2018-09-27 21:52:38 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2018-09-27 21:52:38 -0400
commita67c694c153fd1d15c266d89bab29076d00919d5 (patch)
tree560dc2c7563e0cf4712e600c2f6c254e666c223d /cmd2/cmd2.py
parent848372592577f02e03c063c6ec29a349a0f40071 (diff)
parentb4e217239cf176b96aeb3b124eef3609e688d791 (diff)
downloadcmd2-git-a67c694c153fd1d15c266d89bab29076d00919d5.tar.gz
Merge branch 'macro' into argparse_conversion
Diffstat (limited to 'cmd2/cmd2.py')
-rw-r--r--cmd2/cmd2.py203
1 files changed, 102 insertions, 101 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 2c58d550..5cb38101 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -105,9 +105,9 @@ except ImportError:
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
if sys.version_info < (3, 5):
- from contextlib2 import redirect_stdout, redirect_stderr
+ from contextlib2 import redirect_stdout
else:
- from contextlib import redirect_stdout, redirect_stderr
+ from contextlib import redirect_stdout
# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
ipython_available = True
@@ -126,6 +126,9 @@ INTERNAL_COMMAND_EPILOG = ("Notes:\n"
" This command is for internal use and is not intended to be called from the\n"
" command line.")
+# All command functions start with this
+COMMAND_PREFIX = 'do_'
+
def categorize(func: Union[Callable, Iterable], category: str) -> None:
"""Categorize a function.
@@ -408,7 +411,7 @@ class Cmd(cmd.Cmd):
self.pystate = {}
self.py_history = []
self.pyscript_name = 'app'
- self.keywords = self.reserved_words + [fname[3:] for fname in dir(self) if fname.startswith('do_')]
+ self.keywords = self.reserved_words + self.get_all_commands()
self.statement_parser = StatementParser(
allow_redirection=self.allow_redirection,
terminators=self.terminators,
@@ -1479,16 +1482,15 @@ class Cmd(cmd.Cmd):
# Check if a valid command was entered
if command in self.get_all_commands():
# Get the completer function for this command
- try:
- compfunc = getattr(self, 'complete_' + command)
- except AttributeError:
+ compfunc = getattr(self, 'complete_' + command, None)
+
+ if compfunc is None:
# There's no completer function, next see if the command uses argparser
- try:
- cmd_func = getattr(self, 'do_' + command)
- argparser = getattr(cmd_func, 'argparser')
- # Command uses argparser, switch to the default argparse completer
- compfunc = functools.partial(self._autocomplete_default, argparser=argparser)
- except AttributeError:
+ cmd_func = self._cmd_func(command)
+ if cmd_func and hasattr(cmd_func, 'argparser'):
+ compfunc = functools.partial(self._autocomplete_default,
+ argparser=getattr(cmd_func, 'argparser'))
+ else:
compfunc = self.completedefault
# Check if a macro was entered
@@ -1604,8 +1606,8 @@ class Cmd(cmd.Cmd):
def get_all_commands(self) -> List[str]:
"""Returns a list of all commands."""
- return [name[3:] for name in self.get_names()
- if name.startswith('do_') and callable(getattr(self, name))]
+ return [name[len(COMMAND_PREFIX):] for name in self.get_names()
+ if name.startswith(COMMAND_PREFIX) and callable(getattr(self, name))]
def get_visible_commands(self) -> List[str]:
"""Returns a list of commands that have not been hidden."""
@@ -1968,17 +1970,24 @@ class Cmd(cmd.Cmd):
self.redirecting = False
- def _func_named(self, arg: str) -> str:
- """Gets the method name associated with a given command.
+ def _cmd_func(self, command: str) -> Optional[Callable]:
+ """
+ Get the function for a command
+ :param arg: the name of the command
+ """
+ func_name = self._cmd_func_name(command)
+ if func_name:
+ return getattr(self, func_name)
+ return None
+
+ def _cmd_func_name(self, command: str) -> str:
+ """Get the method name associated with a given command.
:param arg: command to look up method name which implements it
:return: method name which implements the given command
"""
- result = None
- target = 'do_' + arg
- if target in dir(self):
- result = target
- return result
+ target = COMMAND_PREFIX + command
+ return target if callable(getattr(self, target, None)) else ''
def onecmd(self, statement: Union[Statement, str]) -> bool:
""" This executes the actual do_* method for a command.
@@ -1997,40 +2006,35 @@ class Cmd(cmd.Cmd):
if statement.command in self.macros:
stop = self._run_macro(statement)
else:
- funcname = self._func_named(statement.command)
- if not funcname:
- self.default(statement)
- return False
+ cmd_func = self._cmd_func(statement.command)
+ if cmd_func:
+ stop = cmd_func(statement)
- # Since we have a valid command store it in the history
- if statement.command not in self.exclude_from_history:
- self.history.append(statement.raw)
+ # Since we have a valid command store it in the history
+ if statement.command not in self.exclude_from_history:
+ self.history.append(statement.raw)
- try:
- func = getattr(self, funcname)
- except AttributeError:
+ else:
self.default(statement)
- return False
-
- stop = func(statement)
+ stop = False
return stop
def _run_macro(self, statement: Statement) -> bool:
"""
- Resolves a macro and runs the resulting string
+ Resolve a macro and run the resulting string
:param statement: the parsed statement from the command line
:return: a flag indicating whether the interpretation of commands should stop
"""
- try:
- macro = self.macros[statement.command]
- except KeyError:
- raise KeyError("{} is not a macro".format(statement.command))
+ if statement.command not in self.macros.keys():
+ raise KeyError('{} is not a macro'.format(statement.command))
+
+ macro = self.macros[statement.command]
- # Confirm we have the correct number of arguments
+ # For macros, every argument must be provided and there can be no extra arguments.
if len(statement.arg_list) != macro.required_arg_count:
- self.perror('The macro {!r} expects {} argument(s)'.format(statement.command, macro.required_arg_count),
+ self.perror("The macro '{}' expects {} argument(s)".format(statement.command, macro.required_arg_count),
traceback_war=False)
return False
@@ -2210,15 +2214,15 @@ class Cmd(cmd.Cmd):
""" Creates or overwrites an alias """
# Validate the alias name
- alias_name = utils.strip_quotes(args.name)
- valid, errmsg = self.statement_parser.is_valid_command(alias_name)
+ args.name = utils.strip_quotes(args.name)
+ valid, errmsg = self.statement_parser.is_valid_command(args.name)
if not valid:
errmsg = "Invalid alias name: {}".format(errmsg)
self.perror(errmsg, traceback_war=False)
return
- if alias_name in self.macros:
- errmsg = "Aliases cannot have the same name as a macro"
+ if args.name in self.macros:
+ errmsg = "Alias cannot have the same name as a macro"
self.perror(errmsg, traceback_war=False)
return
@@ -2226,13 +2230,13 @@ class Cmd(cmd.Cmd):
# Build the alias value string
value = args.command
- for cur_arg in args.command_args:
- value += ' ' + cur_arg
+ if args.command_args:
+ value += ' ' + ' '.join(args.command_args)
# Set the alias
- result = "overwritten" if alias_name in self.aliases else "created"
- self.aliases[alias_name] = value
- self.poutput("Alias {!r} {}".format(alias_name, result))
+ result = "overwritten" if args.name in self.aliases else "created"
+ self.aliases[args.name] = value
+ self.poutput("Alias '{}' {}".format(args.name, result))
def alias_delete(self, args: argparse.Namespace):
""" Deletes aliases """
@@ -2242,27 +2246,27 @@ class Cmd(cmd.Cmd):
elif not args.name:
self.do_help('alias delete')
else:
- # Get rid of duplicates and strip quotes since the argparse decorator for alias command preserves them
+ # Get rid of duplicates and strip quotes since the argparse decorator for do_alias() preserves them
aliases_to_delete = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
for cur_name in aliases_to_delete:
if cur_name in self.aliases:
del self.aliases[cur_name]
- self.poutput("Alias {!r} deleted".format(cur_name))
+ self.poutput("Alias '{}' deleted".format(cur_name))
else:
- self.perror("Alias {!r} does not exist".format(cur_name), traceback_war=False)
+ self.perror("Alias '{}' does not exist".format(cur_name), traceback_war=False)
def alias_list(self, args: argparse.Namespace):
""" Lists some or all aliases """
if args.name:
- # Get rid of duplicates and strip quotes since the argparse decorator for alias command preserves them
+ # Get rid of duplicates and strip quotes since the argparse decorator for do_alias() preserves them
names_to_view = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
for cur_name in names_to_view:
if cur_name in self.aliases:
self.poutput("alias create {} {}".format(cur_name, self.aliases[cur_name]))
else:
- self.perror("Alias {!r} not found".format(cur_name), traceback_war=False)
+ self.perror("Alias '{}' not found".format(cur_name), traceback_war=False)
else:
sorted_aliases = utils.alphabetical_sort(self.aliases)
for cur_alias in sorted_aliases:
@@ -2298,8 +2302,7 @@ class Cmd(cmd.Cmd):
alias_create_parser = alias_subparsers.add_parser('create', help=alias_create_help,
description=alias_create_description,
epilog=alias_create_epilog)
- setattr(alias_create_parser.add_argument('name', help='name of this alias'),
- ACTION_ARG_CHOICES, get_commands_aliases_and_macros_for_completion)
+ alias_create_parser.add_argument('name', help='name of this alias')
setattr(alias_create_parser.add_argument('command', help='what the alias resolves to'),
ACTION_ARG_CHOICES, get_commands_aliases_and_macros_for_completion)
setattr(alias_create_parser.add_argument('command_args', nargs=argparse.REMAINDER,
@@ -2314,7 +2317,7 @@ class Cmd(cmd.Cmd):
description=alias_delete_description)
setattr(alias_delete_parser.add_argument('name', nargs='*', help='alias to delete'),
ACTION_ARG_CHOICES, get_alias_names)
- alias_delete_parser.add_argument('-a', '--all', action='store_true', help="all aliases will be deleted")
+ alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
alias_delete_parser.set_defaults(func=alias_delete)
# alias -> list
@@ -2322,7 +2325,7 @@ class Cmd(cmd.Cmd):
alias_list_description = ("List specified aliases in a reusable form that can be saved to a startup script\n"
"to preserve aliases across sessions\n"
"\n"
- "Without arguments, all aliases will be listed")
+ "Without arguments, all aliases will be listed.")
alias_list_parser = alias_subparsers.add_parser('list', help=alias_list_help,
description=alias_list_description)
@@ -2330,6 +2333,7 @@ class Cmd(cmd.Cmd):
ACTION_ARG_CHOICES, get_alias_names)
alias_list_parser.set_defaults(func=alias_list)
+ # Preserve quotes since we are passing strings to other commands
@with_argparser(alias_parser, preserve_quotes=True)
def do_alias(self, args: argparse.Namespace):
"""Manage aliases"""
@@ -2347,20 +2351,20 @@ class Cmd(cmd.Cmd):
""" Creates or overwrites a macro """
# Validate the macro name
- macro_name = utils.strip_quotes(args.name)
- valid, errmsg = self.statement_parser.is_valid_command(macro_name)
+ args.name = utils.strip_quotes(args.name)
+ valid, errmsg = self.statement_parser.is_valid_command(args.name)
if not valid:
errmsg = "Invalid macro name: {}".format(errmsg)
self.perror(errmsg, traceback_war=False)
return
- if macro_name in self.get_all_commands():
- errmsg = "Macros cannot have the same name as a command"
+ if args.name in self.get_all_commands():
+ errmsg = "Macro cannot have the same name as a command"
self.perror(errmsg, traceback_war=False)
return
- if macro_name in self.aliases:
- errmsg = "Macros cannot have the same name as an alias"
+ if args.name in self.aliases:
+ errmsg = "Macro cannot have the same name as an alias"
self.perror(errmsg, traceback_war=False)
return
@@ -2368,14 +2372,14 @@ class Cmd(cmd.Cmd):
# Build the macro value string
value = args.command
- for cur_arg in args.command_args:
- value += ' ' + cur_arg
+ if args.command_args:
+ value += ' ' + ' '.join(args.command_args)
# Find all normal arguments
arg_list = []
normal_matches = re.finditer(MacroArg.macro_normal_arg_pattern, value)
max_arg_num = 0
- num_set = set()
+ arg_nums = set()
while True:
try:
@@ -2388,7 +2392,7 @@ class Cmd(cmd.Cmd):
self.perror("Argument numbers must be greater than 0", traceback_war=False)
return
- num_set.add(cur_num)
+ arg_nums.add(cur_num)
if cur_num > max_arg_num:
max_arg_num = cur_num
@@ -2398,7 +2402,7 @@ class Cmd(cmd.Cmd):
break
# Make sure the argument numbers are continuous
- if len(num_set) != max_arg_num:
+ if len(arg_nums) != max_arg_num:
self.perror("Not all numbers between 1 and {} are present "
"in the argument placeholders".format(max_arg_num), traceback_war=False)
return
@@ -2418,9 +2422,9 @@ class Cmd(cmd.Cmd):
break
# Set the macro
- result = "overwritten" if macro_name in self.macros else "created"
- self.macros[macro_name] = Macro(name=macro_name, value=value, required_arg_count=max_arg_num, arg_list=arg_list)
- self.poutput("Macro {!r} {}".format(macro_name, result))
+ result = "overwritten" if args.name in self.macros else "created"
+ self.macros[args.name] = Macro(name=args.name, value=value, required_arg_count=max_arg_num, arg_list=arg_list)
+ self.poutput("Macro '{}' {}".format(args.name, result))
def macro_delete(self, args: argparse.Namespace):
""" Deletes macros """
@@ -2430,27 +2434,27 @@ class Cmd(cmd.Cmd):
elif not args.name:
self.do_help('macro delete')
else:
- # Get rid of duplicates and strip quotes since the argparse decorator for alias command preserves them
+ # Get rid of duplicates and strip quotes since the argparse decorator for do_macro() preserves them
macros_to_delete = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
for cur_name in macros_to_delete:
if cur_name in self.macros:
del self.macros[cur_name]
- self.poutput("Macro {!r} deleted".format(cur_name))
+ self.poutput("Macro '{}' deleted".format(cur_name))
else:
- self.perror("Macro {!r} does not exist".format(cur_name), traceback_war=False)
+ self.perror("Macro '{}' does not exist".format(cur_name), traceback_war=False)
def macro_list(self, args: argparse.Namespace):
""" Lists some or all macros """
if args.name:
- # Get rid of duplicates and strip quotes since the argparse decorator for alias command preserves them
+ # Get rid of duplicates and strip quotes since the argparse decorator for do_macro() preserves them
names_to_view = [utils.strip_quotes(cur_name) for cur_name in utils.remove_duplicates(args.name)]
for cur_name in names_to_view:
if cur_name in self.macros:
self.poutput("macro create {} {}".format(cur_name, self.macros[cur_name].value))
else:
- self.perror("Macro {!r} not found".format(cur_name), traceback_war=False)
+ self.perror("Macro '{}' not found".format(cur_name), traceback_war=False)
else:
sorted_macros = utils.alphabetical_sort(self.macros)
for cur_macro in sorted_macros:
@@ -2496,8 +2500,8 @@ class Cmd(cmd.Cmd):
"\n"
" macro create backup !cp \"{1}\" \"{1}.orig\"\n"
"\n"
- " Macros can resolve into commands, aliases, and macros. Thus it is possible\n"
- " to create a macro that results in infinite recursion. So be careful.\n"
+ " Be careful! Since macros can resolve into commands, aliases, and macros,\n"
+ " it is possible to create a macro that results in infinite recursion.\n"
"\n"
" If you want to use redirection or pipes in the macro, then quote them as in\n"
" this example to prevent the 'macro create' command from being redirected.\n"
@@ -2510,8 +2514,7 @@ class Cmd(cmd.Cmd):
macro_create_parser = macro_subparsers.add_parser('create', help=macro_create_help,
description=macro_create_description,
epilog=macro_create_epilog)
- setattr(macro_create_parser.add_argument('name', help='name of this macro'),
- ACTION_ARG_CHOICES, get_macro_names)
+ macro_create_parser.add_argument('name', help='name of this macro')
setattr(macro_create_parser.add_argument('command', help='what the macro resolves to'),
ACTION_ARG_CHOICES, get_commands_aliases_and_macros_for_completion)
setattr(macro_create_parser.add_argument('command_args', nargs=argparse.REMAINDER,
@@ -2526,7 +2529,7 @@ class Cmd(cmd.Cmd):
description=macro_delete_description)
setattr(macro_delete_parser.add_argument('name', nargs='*', help='macro to delete'),
ACTION_ARG_CHOICES, get_macro_names)
- macro_delete_parser.add_argument('-a', '--all', action='store_true', help="all macros will be deleted")
+ macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
macro_delete_parser.set_defaults(func=macro_delete)
# macro -> list
@@ -2541,6 +2544,7 @@ class Cmd(cmd.Cmd):
ACTION_ARG_CHOICES, get_macro_names)
macro_list_parser.set_defaults(func=macro_list)
+ # Preserve quotes since we are passing strings to other commands
@with_argparser(macro_parser, preserve_quotes=True)
def do_macro(self, args: argparse.Namespace):
"""Manage macros"""
@@ -2584,13 +2588,10 @@ class Cmd(cmd.Cmd):
matches = []
# Check if this is a command with an argparse function
- funcname = self._func_named(command)
- if funcname:
- func = getattr(self, funcname)
- if hasattr(func, 'argparser'):
- parser = getattr(func, 'argparser')
- completer = AutoCompleter(parser, cmd2_app=self)
- matches = completer.complete_command_help(tokens[cmd_index:], text, line, begidx, endidx)
+ cmd_func = self._cmd_func(command)
+ if cmd_func and hasattr(cmd_func, 'argparser'):
+ completer = AutoCompleter(getattr(cmd_func, 'argparser'), cmd2_app=self)
+ matches = completer.complete_command_help(tokens[cmd_index:], text, line, begidx, endidx)
return matches
@@ -2612,13 +2613,11 @@ class Cmd(cmd.Cmd):
elif args.command:
# Getting help for a specific command
- funcname = self._func_named(args.command)
- if funcname:
+ cmd_func = self._cmd_func(args.command)
+ if cmd_func:
# Check to see if this function was decorated with an argparse ArgumentParser
- func = getattr(self, funcname)
- if hasattr(func, 'argparser'):
- completer = AutoCompleter(getattr(func, 'argparser'), cmd2_app=self)
-
+ if hasattr(cmd_func, 'argparser'):
+ completer = AutoCompleter(getattr(cmd_func, 'argparser'), cmd2_app=self)
tokens = [args.command]
tokens.extend(args.subcommand)
self.poutput(completer.format_help(tokens))
@@ -2643,11 +2642,12 @@ class Cmd(cmd.Cmd):
cmds_cats = {}
for command in visible_commands:
- if command in help_topics or getattr(self, self._func_named(command)).__doc__:
+ cmd_func = self._cmd_func(command)
+ if command in help_topics or cmd_func.__doc__:
if command in help_topics:
help_topics.remove(command)
- if hasattr(getattr(self, self._func_named(command)), HELP_CATEGORY):
- category = getattr(getattr(self, self._func_named(command)), HELP_CATEGORY)
+ if hasattr(cmd_func, HELP_CATEGORY):
+ category = getattr(cmd_func, HELP_CATEGORY)
cmds_cats.setdefault(category, [])
cmds_cats[category].append(command)
else:
@@ -2700,12 +2700,13 @@ class Cmd(cmd.Cmd):
func = getattr(self, 'help_' + command)
except AttributeError:
# Couldn't find a help function
+ cmd_func = self._cmd_func(command)
try:
# Now see if help_summary has been set
- doc = getattr(self, self._func_named(command)).help_summary
+ doc = cmd_func.help_summary
except AttributeError:
# Last, try to directly access the function's doc-string
- doc = getattr(self, self._func_named(command)).__doc__
+ doc = cmd_func.__doc__
else:
# we found the help function
result = io.StringIO()