summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2/argparse_completer.py22
-rw-r--r--cmd2/cmd2.py217
-rw-r--r--cmd2/parsing.py25
3 files changed, 145 insertions, 119 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 03ff4375..0c0bc6a1 100755
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -707,7 +707,7 @@ class ACHelpFormatter(argparse.RawTextHelpFormatter):
def _format_usage(self, usage, actions, groups, prefix) -> str:
if prefix is None:
- prefix = _('Usage: ')
+ prefix = _('usage: ')
# if usage is specified, use that
if usage is not None:
@@ -738,7 +738,7 @@ class ACHelpFormatter(argparse.RawTextHelpFormatter):
# build full usage string
format = self._format_actions_usage
- action_usage = format(positionals + required_options + optionals, groups)
+ action_usage = format(required_options + optionals + positionals, groups)
usage = ' '.join([s for s in [prog, action_usage] if s])
# wrap the usage parts if it's too long
@@ -749,15 +749,15 @@ class ACHelpFormatter(argparse.RawTextHelpFormatter):
# break usage into wrappable parts
part_regexp = r'\(.*?\)+|\[.*?\]+|\S+'
+ req_usage = format(required_options, groups)
opt_usage = format(optionals, groups)
pos_usage = format(positionals, groups)
- req_usage = format(required_options, groups)
+ req_parts = _re.findall(part_regexp, req_usage)
opt_parts = _re.findall(part_regexp, opt_usage)
pos_parts = _re.findall(part_regexp, pos_usage)
- req_parts = _re.findall(part_regexp, req_usage)
+ assert ' '.join(req_parts) == req_usage
assert ' '.join(opt_parts) == opt_usage
assert ' '.join(pos_parts) == pos_usage
- assert ' '.join(req_parts) == req_usage
# End cmd2 customization
@@ -787,13 +787,15 @@ class ACHelpFormatter(argparse.RawTextHelpFormatter):
if len(prefix) + len(prog) <= 0.75 * text_width:
indent = ' ' * (len(prefix) + len(prog) + 1)
# Begin cmd2 customization
- if opt_parts:
- lines = get_lines([prog] + pos_parts, indent, prefix)
- lines.extend(get_lines(req_parts, indent))
+ if req_parts:
+ lines = get_lines([prog] + req_parts, indent, prefix)
lines.extend(get_lines(opt_parts, indent))
+ lines.extend(get_lines(pos_parts, indent))
+ elif opt_parts:
+ lines = get_lines([prog] + opt_parts, indent, prefix)
+ lines.extend(get_lines(pos_parts, indent))
elif pos_parts:
lines = get_lines([prog] + pos_parts, indent, prefix)
- lines.extend(get_lines(req_parts, indent))
else:
lines = [prog]
# End cmd2 customization
@@ -806,9 +808,9 @@ class ACHelpFormatter(argparse.RawTextHelpFormatter):
lines = get_lines(parts, indent)
if len(lines) > 1:
lines = []
- lines.extend(get_lines(pos_parts, indent))
lines.extend(get_lines(req_parts, indent))
lines.extend(get_lines(opt_parts, indent))
+ lines.extend(get_lines(pos_parts, indent))
# End cmd2 customization
lines = [prog] + lines
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index c2d3eb1c..57660331 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2217,122 +2217,137 @@ class Cmd(cmd.Cmd):
return stop
- def do_alias(self, statement: Statement) -> None:
- """Define or display aliases
-
-Usage: Usage: alias [name] | [<name> <value>]
- Where:
- name - name of the alias being looked up, added, or replaced
- value - what the alias will be resolved to (if adding or replacing)
- this can contain spaces and does not need to be quoted
-
- Without arguments, 'alias' prints a list of all aliases in a reusable form which
- can be outputted to a startup_script to preserve aliases across sessions.
-
- With one argument, 'alias' shows the value of the specified alias.
- Example: alias ls (Prints the value of the alias called 'ls' if it exists)
-
- With two or more arguments, 'alias' creates or replaces an alias.
-
- Example: alias ls !ls -lF
-
- If you want to use redirection or pipes in the alias, then quote them to prevent
- the alias command itself from being redirected
-
- Examples:
- alias save_results print_results ">" out.txt
- alias save_results print_results '>' out.txt
-"""
- # Get alias arguments as a list with quotes preserved
- alias_arg_list = statement.arg_list
-
- # If no args were given, then print a list of current aliases
- if not alias_arg_list:
- for cur_alias in self.aliases:
- self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
+ # ----- Alias subcommand functions -----
+
+ def alias_create(self, args: argparse.Namespace):
+ """ Creates or overwrites an alias """
+ # Validate the alias name
+ valid, errmsg = self.statement_parser.is_valid_command(args.name)
+ if not valid:
+ errmsg = "Alias names {}".format(errmsg)
+ self.perror(errmsg, traceback_war=False)
return
- # Get the alias name
- name = alias_arg_list[0]
-
- # The user is looking up an alias
- if len(alias_arg_list) == 1:
- if name in self.aliases:
- self.poutput("alias {} {}".format(name, self.aliases[name]))
- else:
- self.perror("Alias {!r} not found".format(name), traceback_war=False)
-
- # The user is creating an alias
- else:
- # Unquote redirection and pipes
- index = 1
- while index < len(alias_arg_list):
- unquoted_arg = utils.strip_quotes(alias_arg_list[index])
- if unquoted_arg in constants.REDIRECTION_TOKENS:
- alias_arg_list[index] = unquoted_arg
- index += 1
-
- # Build the alias value string
- value = ' '.join(alias_arg_list[1:])
-
- # Validate the alias to ensure it doesn't include weird characters
- # like terminators, output redirection, or whitespace
- 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))
-
- # Keep aliases in alphabetically sorted order
- self.aliases = collections.OrderedDict(sorted(self.aliases.items()))
- else:
- errmsg = "Aliases can not contain: {}".format(invalidchars)
- self.perror(errmsg, traceback_war=False)
-
- def complete_alias(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
- """ Tab completion for alias """
- alias_names = set(self.aliases.keys())
- visible_commands = set(self.get_visible_commands())
+ stripped_command = args.command.strip()
+ if not stripped_command:
+ errmsg = "An alias cannot resolve to an empty string"
+ self.perror(errmsg, traceback_war=False)
+ return
- index_dict = \
- {
- 1: alias_names,
- 2: list(alias_names | visible_commands)
- }
- return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete)
+ # Build the alias command string
+ command = stripped_command + ' ' + ' '.join(utils.quote_string_if_needed(args.command_args))
- @with_argument_list
- def do_unalias(self, arglist: List[str]) -> None:
- """Unsets aliases
-
-Usage: Usage: unalias [-a] name [name ...]
- Where:
- name - name of the alias being unset
+ # Set the alias
+ self.aliases[args.name] = command
+ self.poutput("Alias {!r} created".format(args.name))
- Options:
- -a remove all alias definitions
-"""
- if not arglist:
- self.do_help(['unalias'])
-
- if '-a' in arglist:
+ def alias_delete(self, args: argparse.Namespace):
+ """ Deletes aliases """
+ if args.all:
self.aliases.clear()
- self.poutput("All aliases cleared")
-
+ self.poutput("All aliases deleted")
+ elif not args.name:
+ self.onecmd('help alias delete')
else:
# Get rid of duplicates
- arglist = utils.remove_duplicates(arglist)
+ aliases_to_delete = utils.remove_duplicates(args.name)
- for cur_arg in arglist:
+ for cur_arg in aliases_to_delete:
if cur_arg in self.aliases:
del self.aliases[cur_arg]
self.poutput("Alias {!r} cleared".format(cur_arg))
else:
self.perror("Alias {!r} does not exist".format(cur_arg), traceback_war=False)
- def complete_unalias(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
- """ Tab completion for unalias """
- return self.basic_complete(text, line, begidx, endidx, self.aliases)
+ def alias_list(self, args: argparse.Namespace):
+ """ Lists some or all aliases """
+ if args.name:
+ names_to_view = 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)
+ else:
+ # noinspection PyTypeChecker
+ sorted_aliases = utils.alphabetical_sort(self.aliases.keys())
+ for cur_alias in sorted_aliases:
+ self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
+
+ def get_aliases(self):
+ """ Used to complete alias names """
+ return self.aliases.keys()
+
+ def get_aliases_and_commands(self):
+ """ Used to complete alias and command names """
+ alias_names = set(self.aliases.keys())
+ visible_commands = set(self.get_visible_commands())
+ return list(alias_names | visible_commands)
+
+ # Top-level parser for alias
+ alias_parser = ACArgumentParser(description="Manage aliases", prog='alias')
+
+ # Add subcommands to alias
+ alias_subparsers = alias_parser.add_subparsers()
+
+ # alias -> create
+ alias_create_help = "create or overwrite an alias"
+ alias_create_description = "Create or overwrite an alias"
+
+ alias_create_epilog = "Notes:\n"
+ alias_create_epilog += " If you want to use redirection or pipes in the alias, then quote them to prevent\n"
+ alias_create_epilog += " the alias command itself from being redirected\n"
+ alias_create_epilog += "\n"
+ alias_create_epilog += "Examples:\n"
+ alias_create_epilog += " alias ls !ls -lF\n"
+ alias_create_epilog += " alias create show_log !cat \"log file.txt\"\n"
+ alias_create_epilog += " alias create save_results print_results \">\" out.txt\n"
+
+ 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', type=str, help='Name of this alias'),
+ ACTION_ARG_CHOICES, get_aliases_and_commands)
+ setattr(alias_create_parser.add_argument('command', type=str, help='command or alias the alias resolves to'),
+ ACTION_ARG_CHOICES, get_aliases_and_commands)
+ setattr(alias_create_parser.add_argument('command_args', type=str, nargs=argparse.REMAINDER,
+ help='arguments being passed to command'),
+ ACTION_ARG_CHOICES, ('path_complete',))
+ alias_create_parser.set_defaults(func=alias_create)
+
+ # alias -> delete
+ alias_delete_help = "delete aliases"
+ alias_delete_description = "Delete specified aliases or all aliases if --all is used"
+ alias_delete_parser = alias_subparsers.add_parser('delete', help=alias_delete_help,
+ description=alias_delete_description)
+ setattr(alias_delete_parser.add_argument('name', type=str, nargs='*', help='alias to delete'),
+ ACTION_ARG_CHOICES, get_aliases)
+ alias_delete_parser.add_argument('-a', '--all', action='store_true', help="all aliases will be deleted")
+ alias_delete_parser.set_defaults(func=alias_delete)
+
+ # alias -> list
+ alias_list_help = "list aliases"
+ alias_list_description = "List specified aliases in a reusable form that can be saved to\n"
+ alias_list_description += "a startup_script to preserve aliases across sessions\n"
+ alias_list_description += "\n"
+ alias_list_description += "Without arguments, all aliases will be listed"
+
+ alias_list_parser = alias_subparsers.add_parser('list', help=alias_list_help,
+ description=alias_list_description)
+ setattr(alias_list_parser.add_argument('name', type=str, nargs="*", help='alias to list'),
+ ACTION_ARG_CHOICES, get_aliases)
+ alias_list_parser.set_defaults(func=alias_list)
+
+ @with_argparser(alias_parser)
+ def do_alias(self, args: argparse.Namespace):
+ """ Manages aliases """
+ func = getattr(args, 'func', None)
+ if func is not None:
+ # Call whatever subcommand function was selected
+ func(self, args)
+ else:
+ # No subcommand was provided, so call help
+ self.do_help(['alias'])
@with_argument_list
def do_help(self, arglist: List[str]) -> None:
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 1d22ccb8..c21da920 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -247,23 +247,32 @@ class StatementParser:
self._command_pattern = re.compile(expr)
def is_valid_command(self, word: str) -> Tuple[bool, str]:
- """Determine whether a word is a valid alias.
+ """Determine whether a word is a valid name for a command.
- Aliases can not include redirection characters, whitespace,
- or termination characters.
+ Commands can not include redirection characters, whitespace,
+ or termination characters. They also cannot start with a
+ shortcut.
- If word is not a valid command, return False and a comma
- separated string of characters that can not appear in a command.
+ If word is not a valid command, return False and error text
This string is suitable for inclusion in an error message of your
choice:
- valid, invalidchars = statement_parser.is_valid_command('>')
+ valid, errmsg = statement_parser.is_valid_command('>')
if not valid:
- errmsg = "Aliases can not contain: {}".format(invalidchars)
+ errmsg = "Aliases {}".format(errmsg)
"""
valid = False
- errmsg = 'whitespace, quotes, '
+ if not word:
+ return False, 'cannot be an empty string'
+
+ errmsg = 'cannot start with a shortcut: '
+ errmsg += ', '.join(shortcut for (shortcut, expansion) in self.shortcuts)
+ for (shortcut, expansion) in self.shortcuts:
+ if word.startswith(shortcut):
+ return False, errmsg
+
+ errmsg = 'cannot contain: whitespace, quotes, '
errchars = []
errchars.extend(constants.REDIRECTION_CHARS)
errchars.extend(self.terminators)