diff options
-rwxr-xr-x | cmd2/argparse_completer.py | 22 | ||||
-rw-r--r-- | cmd2/cmd2.py | 217 | ||||
-rw-r--r-- | cmd2/parsing.py | 25 |
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) |