summaryrefslogtreecommitdiff
path: root/cmd2/argparse_custom.py
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-07-15 22:49:36 -0400
committerGitHub <noreply@github.com>2019-07-15 22:49:36 -0400
commit94b424e9c41f99c6eb268c6c97f09e99a8342de8 (patch)
treebcbf724e20fed985f7d05515a10d28ba32112a68 /cmd2/argparse_custom.py
parent8109e70b0442206103fa5fe1a3af79d1851d7ec1 (diff)
parent3ad59ceffb9810b774a93448328c7c590080cc98 (diff)
downloadcmd2-git-94b424e9c41f99c6eb268c6c97f09e99a8342de8.tar.gz
Merge pull request #718 from python-cmd2/auto_completer_refactor
Auto completer refactor
Diffstat (limited to 'cmd2/argparse_custom.py')
-rw-r--r--cmd2/argparse_custom.py725
1 files changed, 725 insertions, 0 deletions
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
new file mode 100644
index 00000000..1cdb7840
--- /dev/null
+++ b/cmd2/argparse_custom.py
@@ -0,0 +1,725 @@
+# coding=utf-8
+"""
+This module adds capabilities to argparse by patching a few of its functions. It also defines a parser
+class called ArgParser which improves error and help output over normal argparse. All cmd2 code uses
+this parser and it is recommended that developers of cmd2-based apps either use it or write their own parser
+that inherits from it. This will give a consistent look-and-feel between the help/error output of built-in
+cmd2 commands and the app-specific commands.
+
+Since the new capabilities are added by patching at the argparse API level, they are available whether or
+not ArgParser is used. However, the help and error output of ArgParser is customized to notate nargs ranges
+whereas any other parser class won't be as explicit in their output.
+
+############################################################################################################
+# Added capabilities
+############################################################################################################
+
+Extends argparse nargs functionality by allowing tuples which specify a range (min, max). To specify a max
+value with no upper bound, use a 1-item tuple (min,)
+
+ Example:
+ # -f argument expects at least 3 values
+ parser.add_argument('-f', nargs=(3,))
+
+ # -f argument expects 3 to 5 values
+ parser.add_argument('-f', nargs=(3, 5))
+
+Tab Completion:
+ cmd2 uses its AutoCompleter class to enable argparse-based tab completion on all commands that use the
+ @with_argparse wrappers. Out of the box you get tab completion of commands, sub-commands, and flag names,
+ as well as instructive hints about the current argument that print when tab is pressed. In addition,
+ you can add tab completion for each argument's values using parameters passed to add_argument().
+
+ Below are the 5 add_argument() parameters for enabling tab completion of an argument's value. Only one
+ can be used at a time.
+
+ choices
+ Pass a list of values to the choices parameter.
+ Example:
+ parser.add_argument('-o', '--options', choices=['An Option', 'SomeOtherOption'])
+ parser.add_argument('-o', '--options', choices=my_list)
+
+ choices_function
+ Pass a function that returns choices. This is good in cases where the choice list is dynamically
+ generated when the user hits tab.
+
+ Example:
+ def my_choices_function):
+ ...
+ return my_generated_list
+
+ parser.add_argument('-o', '--options', choices_function=my_choices_function)
+
+ choices_method
+ This is exactly like choices_function, but the function needs to be an instance method of a cmd2-based class.
+ When AutoCompleter calls the method, it will pass the app instance as the self argument. This is good in
+ cases where the list of choices being generated relies on state data of the cmd2-based app
+
+ Example:
+ def my_choices_method(self):
+ ...
+ return my_generated_list
+
+ completer_function
+ Pass a tab-completion function that does custom completion. Since custom tab completion operations commonly
+ need to modify cmd2's instance variables related to tab-completion, it will be rare to need a completer
+ function. completer_method should be used in those cases.
+
+ Example:
+ def my_completer_function(text, line, begidx, endidx):
+ ...
+ return completions
+ parser.add_argument('-o', '--options', completer_function=my_completer_function)
+
+ completer_method
+ This is exactly like completer_function, but the function needs to be an instance method of a cmd2-based class.
+ When AutoCompleter calls the method, it will pass the app instance as the self argument. cmd2 provides
+ a few completer methods for convenience (e.g. path_complete, delimiter_complete)
+
+ Example:
+ This adds file-path completion to an argument
+ parser.add_argument('-o', '--options', completer_method=cmd2.Cmd.path_complete)
+
+
+ In all cases in which function/methods are passed you can use functools.partial() to prepopulate
+ values of the underlying function.
+
+ Example:
+ This says to call path_complete with a preset value for its path_filter argument.
+ completer_method = functools.partial(path_complete,
+ path_filter=lambda path: os.path.isdir(path))
+ parser.add_argument('-o', '--options', choices_method=completer_method)
+
+CompletionItem Class:
+ This class was added to help in cases where uninformative data is being tab completed. For instance,
+ tab completing ID numbers isn't very helpful to a user without context. Returning a list of CompletionItems
+ instead of a regular string for completion results will signal the AutoCompleter to output the completion
+ results in a table of completion tokens with descriptions instead of just a table of tokens.
+
+ Instead of this:
+ 1 2 3
+
+ The user sees this:
+ ITEM_ID Item Name
+ 1 My item
+ 2 Another item
+ 3 Yet another item
+
+
+ The left-most column is the actual value being tab completed and its header is that value's name.
+ The right column header is defined using the descriptive_header parameter of add_argument(). The right
+ column values come from the CompletionItem.description value.
+
+ Example:
+ token = 1
+ token_description = "My Item"
+ completion_item = CompletionItem(token, token_description)
+
+ Since descriptive_header and CompletionItem.description are just strings, you can format them in
+ such a way to have multiple columns.
+
+ ITEM_ID Item Name Checked Out Due Date
+ 1 My item True 02/02/2022
+ 2 Another item False
+ 3 Yet another item False
+
+ To use CompletionItems, just return them from your choices or completer functions.
+
+ To avoid printing a ton of information to the screen at once when a user presses tab, there is
+ a maximum threshold for the number of CompletionItems that will be shown. It's value is defined
+ in cmd2.Cmd.max_completion_items. It defaults to 50, but can be changed. If the number of completion
+ suggestions exceeds this number, they will be displayed in the typical columnized format and will
+ not include the description value of the CompletionItems.
+
+############################################################################################################
+# Patched argparse functions:
+###########################################################################################################
+argparse._ActionsContainer.add_argument - adds arguments related to tab completion and enables nargs range parsing
+ See _add_argument_wrapper for more details on these argument
+
+argparse.ArgumentParser._get_nargs_pattern - adds support to for nargs ranges
+ See _get_nargs_pattern_wrapper for more details
+
+argparse.ArgumentParser._match_argument - adds support to for nargs ranges
+ See _match_argument_wrapper for more details
+"""
+
+import argparse
+import re
+import sys
+
+# noinspection PyUnresolvedReferences,PyProtectedMember
+from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _
+from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
+
+from .ansi import ansi_aware_write, style_error
+
+############################################################################################################
+# The following are names of custom argparse argument attributes added by cmd2
+############################################################################################################
+
+# Used in nargs ranges to signify there is no maximum
+INFINITY = float('inf')
+
+# A tuple specifying nargs as a range (min, max)
+ATTR_NARGS_RANGE = 'nargs_range'
+
+# ChoicesCallable object that specifies the function to be called which provides choices to the argument
+ATTR_CHOICES_CALLABLE = 'choices_callable'
+
+# Pressing tab normally displays the help text for the argument if no choices are available
+# Setting this attribute to True will suppress these hints
+ATTR_SUPPRESS_TAB_HINT = 'suppress_tab_hint'
+
+# Descriptive header that prints when using CompletionItems
+ATTR_DESCRIPTIVE_COMPLETION_HEADER = 'desc_completion_header'
+
+
+def generate_range_error(range_min: int, range_max: Union[int, float]) -> str:
+ """Generate an error message when the the number of arguments provided is not within the expected range"""
+ err_str = "expected "
+
+ if range_max == INFINITY:
+ err_str += "at least {} argument".format(range_min)
+
+ if range_min != 1:
+ err_str += "s"
+ else:
+ if range_min == range_max:
+ err_str += "{} argument".format(range_min)
+ else:
+ err_str += "{} to {} argument".format(range_min, range_max)
+
+ if range_max != 1:
+ err_str += "s"
+
+ return err_str
+
+
+class CompletionItem(str):
+ """
+ Completion item with descriptive text attached
+
+ See header of this file for more information
+ """
+ def __new__(cls, value: object, *args, **kwargs) -> str:
+ return super().__new__(cls, value)
+
+ # noinspection PyUnusedLocal
+ def __init__(self, value: object, desc: str = '', *args, **kwargs) -> None:
+ """
+ CompletionItem Initializer
+
+ :param value: the value being tab completed
+ :param desc: description text to display
+ :param args: args for str __init__
+ :param kwargs: kwargs for str __init__
+ """
+ super().__init__(*args, **kwargs)
+ self.description = desc
+
+
+class ChoicesCallable:
+ """
+ Enables using a callable as the choices provider for an argparse argument.
+ While argparse has the built-in choices attribute, it is limited to an iterable.
+ """
+ def __init__(self, is_method: bool, is_completer: bool, to_call: Callable):
+ """
+ Initializer
+ :param is_method: True if to_call is an instance method of a cmd2 app. False if it is a function.
+ :param is_completer: True if to_call is a tab completion routine which expects
+ the args: text, line, begidx, endidx
+ :param to_call: the callable object that will be called to provide choices for the argument
+ """
+ self.is_method = is_method
+ self.is_completer = is_completer
+ self.to_call = to_call
+
+
+############################################################################################################
+# Patch _ActionsContainer.add_argument with our wrapper to support more arguments
+############################################################################################################
+
+# Save original _ActionsContainer.add_argument so we can call it in our wrapper
+# noinspection PyProtectedMember
+orig_actions_container_add_argument = argparse._ActionsContainer.add_argument
+
+
+def _add_argument_wrapper(self, *args,
+ nargs: Union[int, str, Tuple[int], Tuple[int, int], None] = None,
+ choices_function: Optional[Callable[[], Iterable[Any]]] = None,
+ choices_method: Optional[Callable[[Any], Iterable[Any]]] = None,
+ completer_function: Optional[Callable[[str, str, int, int], List[str]]] = None,
+ completer_method: Optional[Callable[[Any, str, str, int, int], List[str]]] = None,
+ suppress_tab_hint: bool = False,
+ descriptive_header: Optional[str] = None,
+ **kwargs) -> argparse.Action:
+ """
+ Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2
+
+ # Args from original function
+ :param self: instance of the _ActionsContainer being added to
+ :param args: arguments expected by argparse._ActionsContainer.add_argument
+
+ # Customized arguments from original function
+ :param nargs: extends argparse nargs functionality by allowing tuples which specify a range (min, max)
+ to specify a max value with no upper bound, use a 1-item tuple (min,)
+
+ # Added args used by AutoCompleter
+ :param choices_function: function that provides choices for this argument
+ :param choices_method: cmd2-app method that provides choices for this argument
+ :param completer_function: tab-completion function that provides choices for this argument
+ :param completer_method: cmd2-app tab-completion method that provides choices for this argument
+ :param suppress_tab_hint: when AutoCompleter has no results to show during tab completion, it displays the current
+ argument's help text as a hint. Set this to True to suppress the hint. If this argument's
+ help text is set to argparse.SUPPRESS, then tab hints will not display regardless of the
+ value passed for suppress_tab_hint. Defaults to False.
+ :param descriptive_header: if the provided choices are CompletionItems, then this header will display
+ during tab completion. Defaults to None.
+
+ # Args from original function
+ :param kwargs: keyword-arguments recognized by argparse._ActionsContainer.add_argument
+
+ Note: You can only use 1 of the following in your argument:
+ choices, choices_function, choices_method, completer_function, completer_method
+
+ See the header of this file for more information
+
+ :return: the created argument action
+ """
+ # Pre-process special ranged nargs
+ nargs_range = None
+
+ if nargs is not None:
+ # Check if nargs was given as a range
+ if isinstance(nargs, tuple):
+
+ # Handle 1-item tuple by setting max to INFINITY
+ if len(nargs) == 1:
+ nargs = (nargs[0], INFINITY)
+
+ # Validate nargs tuple
+ if len(nargs) != 2 or not isinstance(nargs[0], int) or \
+ not (isinstance(nargs[1], int) or nargs[1] == INFINITY):
+ raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers')
+ if nargs[0] >= nargs[1]:
+ raise ValueError('Invalid nargs range. The first value must be less than the second')
+ if nargs[0] < 0:
+ raise ValueError('Negative numbers are invalid for nargs range')
+
+ # Save the nargs tuple as our range setting
+ nargs_range = nargs
+ range_min = nargs_range[0]
+ range_max = nargs_range[1]
+
+ # Convert nargs into a format argparse recognizes
+ if range_min == 0:
+ if range_max == 1:
+ nargs_adjusted = argparse.OPTIONAL
+
+ # No range needed since (0, 1) is just argparse.OPTIONAL
+ nargs_range = None
+ else:
+ nargs_adjusted = argparse.ZERO_OR_MORE
+ if range_max == INFINITY:
+ # No range needed since (0, INFINITY) is just argparse.ZERO_OR_MORE
+ nargs_range = None
+ elif range_min == 1 and range_max == INFINITY:
+ nargs_adjusted = argparse.ONE_OR_MORE
+
+ # No range needed since (1, INFINITY) is just argparse.ONE_OR_MORE
+ nargs_range = None
+ else:
+ nargs_adjusted = argparse.ONE_OR_MORE
+ else:
+ nargs_adjusted = nargs
+
+ # Add the argparse-recognized version of nargs to kwargs
+ kwargs['nargs'] = nargs_adjusted
+
+ # Create the argument using the original add_argument function
+ new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
+
+ # Verify consistent use of arguments
+ choice_params = [new_arg.choices, choices_function, choices_method, completer_function, completer_method]
+ num_set = len(choice_params) - choice_params.count(None)
+
+ if num_set > 1:
+ err_msg = ("Only one of the following may be used in an argparser argument at a time:\n"
+ "choices, choices_function, choices_method, completer_function, completer_method")
+ raise (ValueError(err_msg))
+
+ # Set the custom attributes
+ setattr(new_arg, ATTR_NARGS_RANGE, nargs_range)
+
+ if choices_function:
+ setattr(new_arg, ATTR_CHOICES_CALLABLE,
+ ChoicesCallable(is_method=False, is_completer=False, to_call=choices_function))
+ elif choices_method:
+ setattr(new_arg, ATTR_CHOICES_CALLABLE,
+ ChoicesCallable(is_method=True, is_completer=False, to_call=choices_method))
+ elif completer_function:
+ setattr(new_arg, ATTR_CHOICES_CALLABLE,
+ ChoicesCallable(is_method=False, is_completer=True, to_call=completer_function))
+ elif completer_method:
+ setattr(new_arg, ATTR_CHOICES_CALLABLE,
+ ChoicesCallable(is_method=True, is_completer=True, to_call=completer_method))
+
+ setattr(new_arg, ATTR_SUPPRESS_TAB_HINT, suppress_tab_hint)
+ setattr(new_arg, ATTR_DESCRIPTIVE_COMPLETION_HEADER, descriptive_header)
+
+ return new_arg
+
+
+# Overwrite _ActionsContainer.add_argument with our wrapper
+# noinspection PyProtectedMember
+argparse._ActionsContainer.add_argument = _add_argument_wrapper
+
+############################################################################################################
+# Patch ArgumentParser._get_nargs_pattern with our wrapper to nargs ranges
+############################################################################################################
+
+# Save original ArgumentParser._get_nargs_pattern so we can call it in our wrapper
+# noinspection PyProtectedMember
+orig_argument_parser_get_nargs_pattern = argparse.ArgumentParser._get_nargs_pattern
+
+
+# noinspection PyProtectedMember
+def _get_nargs_pattern_wrapper(self, action) -> str:
+ # Wrapper around ArgumentParser._get_nargs_pattern behavior to support nargs ranges
+ nargs_range = getattr(action, ATTR_NARGS_RANGE, None)
+ if nargs_range is not None:
+ if nargs_range[1] == INFINITY:
+ range_max = ''
+ else:
+ range_max = nargs_range[1]
+
+ nargs_pattern = '(-*A{{{},{}}}-*)'.format(nargs_range[0], range_max)
+
+ # if this is an optional action, -- is not allowed
+ if action.option_strings:
+ nargs_pattern = nargs_pattern.replace('-*', '')
+ nargs_pattern = nargs_pattern.replace('-', '')
+ return nargs_pattern
+
+ return orig_argument_parser_get_nargs_pattern(self, action)
+
+
+# Overwrite ArgumentParser._get_nargs_pattern with our wrapper
+# noinspection PyProtectedMember
+argparse.ArgumentParser._get_nargs_pattern = _get_nargs_pattern_wrapper
+
+
+############################################################################################################
+# Patch ArgumentParser._match_argument with our wrapper to nargs ranges
+############################################################################################################
+# noinspection PyProtectedMember
+orig_argument_parser_match_argument = argparse.ArgumentParser._match_argument
+
+
+# noinspection PyProtectedMember
+def _match_argument_wrapper(self, action, arg_strings_pattern) -> int:
+ # Wrapper around ArgumentParser._match_argument behavior to support nargs ranges
+ nargs_pattern = self._get_nargs_pattern(action)
+ match = re.match(nargs_pattern, arg_strings_pattern)
+
+ # raise an exception if we weren't able to find a match
+ if match is None:
+ nargs_range = getattr(action, ATTR_NARGS_RANGE, None)
+ if nargs_range is not None:
+ raise ArgumentError(action, generate_range_error(nargs_range[0], nargs_range[1]))
+
+ return orig_argument_parser_match_argument(self, action, arg_strings_pattern)
+
+
+# Overwrite ArgumentParser._match_argument with our wrapper
+# noinspection PyProtectedMember
+argparse.ArgumentParser._match_argument = _match_argument_wrapper
+
+############################################################################################################
+# Unless otherwise noted, everything below this point are copied from Python's
+# argparse implementation with minor tweaks to adjust output.
+# Changes are noted if it's buried in a block of copied code. Otherwise the
+# function will check for a special case and fall back to the parent function
+############################################################################################################
+
+
+# noinspection PyCompatibility,PyShadowingBuiltins,PyShadowingBuiltins
+class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
+ """Custom help formatter to configure ordering of help text"""
+
+ def _format_usage(self, usage, actions, groups, prefix) -> str:
+ if prefix is None:
+ prefix = _('Usage: ')
+
+ # if usage is specified, use that
+ if usage is not None:
+ usage %= dict(prog=self._prog)
+
+ # if no optionals or positionals are available, usage is just prog
+ elif usage is None and not actions:
+ usage = '%(prog)s' % dict(prog=self._prog)
+
+ # if optionals and positionals are available, calculate usage
+ elif usage is None:
+ prog = '%(prog)s' % dict(prog=self._prog)
+
+ # split optionals from positionals
+ optionals = []
+ positionals = []
+ # Begin cmd2 customization (separates required and optional, applies to all changes in this function)
+ required_options = []
+ for action in actions:
+ if action.option_strings:
+ if action.required:
+ required_options.append(action)
+ else:
+ optionals.append(action)
+ else:
+ positionals.append(action)
+ # End cmd2 customization
+
+ # build full usage string
+ format = self._format_actions_usage
+ 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
+ text_width = self._width - self._current_indent
+ if len(prefix) + len(usage) > text_width:
+
+ # Begin cmd2 customization
+
+ # 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_parts = re.findall(part_regexp, req_usage)
+ opt_parts = re.findall(part_regexp, opt_usage)
+ pos_parts = re.findall(part_regexp, pos_usage)
+ assert ' '.join(req_parts) == req_usage
+ assert ' '.join(opt_parts) == opt_usage
+ assert ' '.join(pos_parts) == pos_usage
+
+ # End cmd2 customization
+
+ # helper for wrapping lines
+ # noinspection PyMissingOrEmptyDocstring,PyShadowingNames
+ def get_lines(parts, indent, prefix=None):
+ lines = []
+ line = []
+ if prefix is not None:
+ line_len = len(prefix) - 1
+ else:
+ line_len = len(indent) - 1
+ for part in parts:
+ if line_len + 1 + len(part) > text_width and line:
+ lines.append(indent + ' '.join(line))
+ line = []
+ line_len = len(indent) - 1
+ line.append(part)
+ line_len += len(part) + 1
+ if line:
+ lines.append(indent + ' '.join(line))
+ if prefix is not None:
+ lines[0] = lines[0][len(indent):]
+ return lines
+
+ # if prog is short, follow it with optionals or positionals
+ if len(prefix) + len(prog) <= 0.75 * text_width:
+ indent = ' ' * (len(prefix) + len(prog) + 1)
+ # Begin cmd2 customization
+ 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)
+ else:
+ lines = [prog]
+ # End cmd2 customization
+
+ # if prog is long, put it on its own line
+ else:
+ indent = ' ' * len(prefix)
+ # Begin cmd2 customization
+ parts = req_parts + opt_parts + pos_parts
+ lines = get_lines(parts, indent)
+ if len(lines) > 1:
+ lines = []
+ 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
+
+ # join lines into usage
+ usage = '\n'.join(lines)
+
+ # prefix with 'Usage:'
+ return '%s%s\n\n' % (prefix, usage)
+
+ def _format_action_invocation(self, action) -> str:
+ if not action.option_strings:
+ default = self._get_default_metavar_for_positional(action)
+ metavar, = self._metavar_formatter(action, default)(1)
+ return metavar
+
+ else:
+ parts = []
+
+ # if the Optional doesn't take a value, format is:
+ # -s, --long
+ if action.nargs == 0:
+ parts.extend(action.option_strings)
+ return ', '.join(parts)
+
+ # Begin cmd2 customization (less verbose)
+ # if the Optional takes a value, format is:
+ # -s, --long ARGS
+ else:
+ default = self._get_default_metavar_for_optional(action)
+ args_string = self._format_args(action, default)
+
+ return ', '.join(action.option_strings) + ' ' + args_string
+ # End cmd2 customization
+
+ def _metavar_formatter(self, action, default_metavar) -> Callable:
+ if action.metavar is not None:
+ result = action.metavar
+ elif action.choices is not None:
+ choice_strs = [str(choice) for choice in action.choices]
+ # Begin cmd2 customization (added space after comma)
+ result = '{%s}' % ', '.join(choice_strs)
+ # End cmd2 customization
+ else:
+ result = default_metavar
+
+ # noinspection PyMissingOrEmptyDocstring
+ def format(tuple_size):
+ if isinstance(result, tuple):
+ return result
+ else:
+ return (result, ) * tuple_size
+ return format
+
+ # noinspection PyProtectedMember
+ def _format_args(self, action, default_metavar) -> str:
+ get_metavar = self._metavar_formatter(action, default_metavar)
+ # Begin cmd2 customization (less verbose)
+ nargs_range = getattr(action, ATTR_NARGS_RANGE, None)
+
+ if nargs_range is not None:
+ if nargs_range[1] == INFINITY:
+ range_str = '{}+'.format(nargs_range[0])
+ else:
+ range_str = '{}..{}'.format(nargs_range[0], nargs_range[1])
+
+ result = '{}{{{}}}'.format('%s' % get_metavar(1), range_str)
+ elif action.nargs == ZERO_OR_MORE:
+ result = '[%s [...]]' % get_metavar(1)
+ elif action.nargs == ONE_OR_MORE:
+ result = '%s [...]' % get_metavar(1)
+ elif isinstance(action.nargs, int) and action.nargs > 1:
+ result = '{}{{{}}}'.format('%s' % get_metavar(1), action.nargs)
+ # End cmd2 customization
+ else:
+ result = super()._format_args(action, default_metavar)
+ return result
+
+
+# noinspection PyCompatibility
+class ArgParser(argparse.ArgumentParser):
+ """Custom ArgumentParser class that improves error and help output"""
+
+ def __init__(self, *args, **kwargs) -> None:
+ if 'formatter_class' not in kwargs:
+ kwargs['formatter_class'] = Cmd2HelpFormatter
+
+ super().__init__(*args, **kwargs)
+
+ def add_subparsers(self, **kwargs):
+ """Custom override. Sets a default title if one was not given."""
+ if 'title' not in kwargs:
+ kwargs['title'] = 'sub-commands'
+
+ return super().add_subparsers(**kwargs)
+
+ def error(self, message: str) -> None:
+ """Custom override that applies custom formatting to the error message"""
+ lines = message.split('\n')
+ linum = 0
+ formatted_message = ''
+ for line in lines:
+ if linum == 0:
+ formatted_message = 'Error: ' + line
+ else:
+ formatted_message += '\n ' + line
+ linum += 1
+
+ self.print_usage(sys.stderr)
+ formatted_message = style_error(formatted_message)
+ self.exit(2, '{}\n\n'.format(formatted_message))
+
+ # noinspection PyProtectedMember
+ def format_help(self) -> str:
+ """Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters"""
+ formatter = self._get_formatter()
+
+ # usage
+ formatter.add_usage(self.usage, self._actions,
+ self._mutually_exclusive_groups)
+
+ # description
+ formatter.add_text(self.description)
+
+ # Begin cmd2 customization (separate required and optional arguments)
+
+ # positionals, optionals and user-defined groups
+ for action_group in self._action_groups:
+ if action_group.title == 'optional arguments':
+ # check if the arguments are required, group accordingly
+ req_args = []
+ opt_args = []
+ for action in action_group._group_actions:
+ if action.required:
+ req_args.append(action)
+ else:
+ opt_args.append(action)
+
+ # separately display required arguments
+ formatter.start_section('required arguments')
+ formatter.add_text(action_group.description)
+ formatter.add_arguments(req_args)
+ formatter.end_section()
+
+ # now display truly optional arguments
+ formatter.start_section(action_group.title)
+ formatter.add_text(action_group.description)
+ formatter.add_arguments(opt_args)
+ formatter.end_section()
+ else:
+ formatter.start_section(action_group.title)
+ formatter.add_text(action_group.description)
+ formatter.add_arguments(action_group._group_actions)
+ formatter.end_section()
+
+ # End cmd2 customization
+
+ # epilog
+ formatter.add_text(self.epilog)
+
+ # determine help from format above
+ return formatter.format_help() + '\n'
+
+ def _print_message(self, message, file=None):
+ # Override _print_message to use ansi_aware_write() since we use ANSI escape characters to support color
+ if message:
+ if file is None:
+ file = sys.stderr
+ ansi_aware_write(file, message)