623 lines
24 KiB
Python
623 lines
24 KiB
Python
import argparse
|
|
try:
|
|
import colorama
|
|
except ImportError:
|
|
colorama = None
|
|
import io
|
|
import os
|
|
import re
|
|
import shlex
|
|
import sys
|
|
import textwrap
|
|
|
|
from voussoirkit import dotdict
|
|
from voussoirkit import niceprints
|
|
from voussoirkit import pipeable
|
|
from voussoirkit import subproctools
|
|
from voussoirkit import vlogging
|
|
|
|
log = vlogging.get_logger(__name__)
|
|
|
|
# When using a single parser or a command of a subparser, the presence of any
|
|
# of these strings in argv will trigger the helptext.
|
|
# > application.py --help
|
|
# > application.py command --help
|
|
HELP_ARGS = {'-h', '--help'}
|
|
|
|
# When using a subparser, the command name can be any of these to trigger
|
|
# the helptext.
|
|
# > application.py help
|
|
# > application.py --help
|
|
# This does not apply to single-parser applications because the user might try
|
|
# to pass the word "help" as the actual argument to the program, but in a
|
|
# subparser application it's very unlikely that there is an actual command
|
|
# called help.
|
|
HELP_COMMANDS = {'help', '-h', '--help'}
|
|
|
|
# Modules can add additional helptexts to this set, and they will appear
|
|
# after the program's main docstring is shown. This is used when the module
|
|
# intercepts sys.argv to change program behavior beyond the options provided
|
|
# by the program's argparse. For example, voussoirkit.vlogging.main_decorator
|
|
# adds command-line arguments like --debug which the application's argparse
|
|
# is not aware of. vlogging registers an epilogue here so that all vlogging-
|
|
# enabled applications gain the relevant helptext for free.
|
|
HELPTEXT_EPILOGUES = set()
|
|
|
|
# INTERNALS
|
|
################################################################################
|
|
|
|
def can_use_bare(parser) -> bool:
|
|
'''
|
|
Return true if the given parser has no required arguments, ie can run bare.
|
|
This is used to decide whether running `> myprogram.py` should show the
|
|
helptext or just run normally.
|
|
'''
|
|
has_func = bool(parser.get_default('func'))
|
|
has_required_args = any(is_required(action) for action in parser._actions)
|
|
return has_func and not has_required_args
|
|
|
|
def can_use_bare_subparsers(subparser_action) -> set:
|
|
'''
|
|
Return a set of subparser names which can be used bare.
|
|
'''
|
|
can_bares = set(
|
|
sp_name for (sp_name, sp) in subparser_action.choices.items()
|
|
if can_use_bare(sp)
|
|
)
|
|
return can_bares
|
|
|
|
def get_program_name():
|
|
program_name = os.path.basename(sys.argv[0])
|
|
program_name = re.sub(r'\.pyw?$', '', program_name)
|
|
return program_name
|
|
|
|
def get_subparser_action(parser):
|
|
for action in parser._actions:
|
|
if isinstance(action, argparse._SubParsersAction):
|
|
return action
|
|
return None
|
|
|
|
def get_subparsers(parser):
|
|
'''
|
|
Return a dictionary mapping subparser actions to a list of their aliases,
|
|
i.e. {parser: [names]}
|
|
'''
|
|
action = get_subparser_action(parser)
|
|
if action is None:
|
|
return {}
|
|
|
|
subparsers = {}
|
|
for (sp_name, sp) in action.choices.items():
|
|
subparsers.setdefault(sp, []).append(sp_name)
|
|
return subparsers
|
|
|
|
def is_required(action):
|
|
# I found that positional arguments marked with nargs=* were still being
|
|
# considered 'required', which is not what I want as far as can_use_bare
|
|
# goes. I believe option_strings==[] is what indicates this action is
|
|
# positional. If I'm wrong let's fix it.
|
|
if action.option_strings == [] and action.nargs == '*':
|
|
return False
|
|
return action.required
|
|
|
|
def listget(li, index, fallback=None):
|
|
try:
|
|
return li[index]
|
|
except IndexError:
|
|
return fallback
|
|
|
|
def listsplit(li, target, count=float('inf')):
|
|
'''
|
|
Split a list into multiple lists wherever the target element appears.
|
|
'''
|
|
newli = []
|
|
subli = []
|
|
for item in li:
|
|
if item == target and count > 0:
|
|
newli.append(subli)
|
|
subli = []
|
|
count -= 1
|
|
else:
|
|
subli.append(item)
|
|
newli.append(subli)
|
|
return newli
|
|
|
|
def make_helptext(
|
|
parser,
|
|
all_command_names=None,
|
|
command_name=None,
|
|
do_colors=True,
|
|
do_headline=True,
|
|
full_subparsers=False,
|
|
program_name=None,
|
|
):
|
|
# Even though this text is going out on stderr, we only colorize it if
|
|
# both stdout and stderr are tty because as soon as pipe buffers are
|
|
# involved, even on stdout, things start to get weird.
|
|
if do_colors and colorama and pipeable.stdout_tty() and pipeable.stderr_tty():
|
|
colorama.init()
|
|
color = dotdict.DotDict(
|
|
positional=colorama.Style.BRIGHT + colorama.Fore.CYAN,
|
|
named=colorama.Style.BRIGHT + colorama.Fore.GREEN,
|
|
flag=colorama.Style.BRIGHT + colorama.Fore.MAGENTA,
|
|
command=colorama.Style.BRIGHT + colorama.Fore.YELLOW,
|
|
reset=colorama.Style.RESET_ALL,
|
|
required_asterisk=colorama.Style.BRIGHT + colorama.Fore.RED + '(*)' + colorama.Style.RESET_ALL,
|
|
)
|
|
else:
|
|
color = dotdict.DotDict(
|
|
positional='',
|
|
named='',
|
|
flag='',
|
|
command='',
|
|
reset='',
|
|
required_asterisk='(*)',
|
|
)
|
|
|
|
if program_name is None:
|
|
program_name = get_program_name()
|
|
|
|
if command_name is None:
|
|
invoke_name = program_name
|
|
else:
|
|
invoke_name = f'{program_name} {color.command}{command_name}{color.reset}'
|
|
|
|
# If we are currently generating the helptext for a subparser, the caller
|
|
# can give us the names of all other command names, so that we can colorize
|
|
# them if this subparser references another one in its text. Otherwise, we
|
|
# will only gather up subparsers below the given parser object.
|
|
if all_command_names is None:
|
|
all_command_names = set()
|
|
|
|
# GATHER UP ARGUMENT TYPES
|
|
# Here we go through the list of actions in the parser and classify them
|
|
# into a few basic types, which will be rendered and colorized in
|
|
# different ways.
|
|
|
|
all_positional_names = set()
|
|
all_named_names = set()
|
|
all_flags_names = set(HELP_ARGS)
|
|
dest_to_action = {}
|
|
main_invocation = [invoke_name]
|
|
positional_actions = []
|
|
named_actions = []
|
|
required_named_actions = []
|
|
optional_named_actions = []
|
|
store_types = {argparse._StoreAction, argparse._AppendAction}
|
|
flag_types = {argparse._StoreTrueAction, argparse._StoreFalseAction, argparse._StoreConstAction}
|
|
flag_actions = []
|
|
|
|
for action in parser._actions:
|
|
if type(action) is argparse._HelpAction:
|
|
continue
|
|
|
|
if type(action) is argparse._SubParsersAction:
|
|
all_command_names.update(action.choices.keys())
|
|
continue
|
|
|
|
if type(action) in store_types:
|
|
if action.option_strings == []:
|
|
positional_actions.append(action)
|
|
all_positional_names.add(action.dest)
|
|
else:
|
|
named_actions.append(action)
|
|
if action.required:
|
|
required_named_actions.append(action)
|
|
else:
|
|
optional_named_actions.append(action)
|
|
all_named_names.update(action.option_strings)
|
|
elif type(action) in flag_types:
|
|
flag_actions.append(action)
|
|
all_flags_names.update(action.option_strings)
|
|
else:
|
|
raise TypeError(f'betterhelp doesn\'t know what to do with {action}.')
|
|
dest_to_action[action.dest] = action
|
|
|
|
# COLORIZE ARGUMENT INVOCATIONS
|
|
# Here we generate invocation strings for each of the arguments. That is,
|
|
# an argument called `--scales` with type=int and nargs=+ will be shown as
|
|
# `--scales int [int, ...]`. Each type of argument has different
|
|
# considerations involving the dest, type, metavar, nargs, etc.
|
|
# The required arguments will be added to the main_invocation, the optional
|
|
# arguments will simply wait in the action_invocations dictionary until we
|
|
# show their full help in an upcoming section.
|
|
|
|
def render_nargs(argname, nargs):
|
|
if nargs is None:
|
|
return argname
|
|
elif isinstance(nargs, int):
|
|
return ' '.join([argname] * nargs)
|
|
elif nargs == '?':
|
|
return f'[{argname}]'
|
|
elif nargs == '*':
|
|
return f'[{argname}, {argname}, ...]'
|
|
elif nargs == '+':
|
|
return f'{argname} [{argname}, ...]'
|
|
elif nargs == '...':
|
|
return f'[{argname}, ...]'
|
|
|
|
action_invocations = {}
|
|
for action in positional_actions:
|
|
if action.type is not None:
|
|
typename = f'({action.type.__name__})'
|
|
else:
|
|
typename = ''
|
|
|
|
if action.metavar is not None:
|
|
argname = action.metavar
|
|
else:
|
|
argname = action.dest
|
|
|
|
inv = render_nargs(argname, action.nargs)
|
|
inv = f'{color.positional}{inv}{typename}{color.reset}'
|
|
action_invocations[action] = [inv]
|
|
main_invocation.append(inv)
|
|
|
|
##
|
|
|
|
for action in named_actions:
|
|
action_invocations[action] = []
|
|
for alias in action.option_strings:
|
|
if action.metavar is not None:
|
|
argname = action.metavar
|
|
elif action.type is None:
|
|
argname = action.dest
|
|
else:
|
|
argname = action.type.__name__
|
|
|
|
inv = render_nargs(argname, action.nargs)
|
|
inv = f'{color.named}{alias} {inv}{color.reset}'
|
|
action_invocations[action].append(inv)
|
|
|
|
for action in required_named_actions:
|
|
main_invocation.append(action_invocations[action][0])
|
|
|
|
if optional_named_actions:
|
|
main_invocation.append(f'{color.named}[options]{color.reset}')
|
|
|
|
##
|
|
|
|
for action in flag_actions:
|
|
action_invocations[action] = []
|
|
for alias in action.option_strings:
|
|
inv = f'{color.flag}{alias}{color.reset}'
|
|
action_invocations[action].append(inv)
|
|
|
|
if flag_actions:
|
|
main_invocation.append(f'{color.flag}[flags]{color.reset}')
|
|
|
|
# COLORIZE ARGUMENT NAMES THAT APPEAR IN OTHER TEXTS
|
|
# Now that we know the names of all the different types of arguments, we
|
|
# can use them to colorize the program description and the help text of
|
|
# each individual argument. This makes it really easy to see when one
|
|
# argument has an influence on another argument.
|
|
# If you use a positional argument that is a common noun this can be
|
|
# a bit annoying.
|
|
|
|
def colorize_names(text):
|
|
for command in all_command_names:
|
|
text = re.sub(rf'((?:^|\s){command}(?:\b))', rf'{color.command}\1{color.reset}', text)
|
|
for positional in all_positional_names:
|
|
text = re.sub(rf'((?:^|\s){positional}(?:\b))', rf'{color.positional}\1{color.reset}', text)
|
|
for named in all_named_names:
|
|
text = re.sub(rf'((?:^|\s){named}(?:\b))', rf'{color.named}\1{color.reset}', text)
|
|
for flag in all_flags_names:
|
|
text = re.sub(rf'((?:^|\s){flag}(?:\b))', rf'{color.flag}\1{color.reset}', text)
|
|
return text
|
|
|
|
# PUTTING TOGETHER PROGRAM DESCRIPTION & ARGUMENT HELPS
|
|
# This is the portion that actually constructs the majority of the help
|
|
# text, by combining the program's own help description with the invocation
|
|
# tips and help texts of each of the arguments.
|
|
|
|
program_description = parser.description or ''
|
|
program_description = textwrap.dedent(program_description).strip()
|
|
program_description = colorize_names(program_description)
|
|
|
|
argument_helps = []
|
|
for action in (positional_actions + named_actions + flag_actions):
|
|
inv = '\n'.join(action_invocations[action])
|
|
arghelp = []
|
|
if action.help is not None:
|
|
arghelp.append(textwrap.dedent(action.help).strip())
|
|
if type(action) is argparse._StoreAction and action.default is not None:
|
|
arghelp.append(f'Default: {repr(action.default)}')
|
|
if action.option_strings and action.required:
|
|
arghelp.append(f'{color.required_asterisk} Required{color.reset}')
|
|
arghelp = '\n'.join(arghelp)
|
|
|
|
arghelp = colorize_names(arghelp)
|
|
|
|
arghelp = textwrap.indent(arghelp, ' ')
|
|
argument_helps.append(f'{inv}\n{arghelp}'.strip())
|
|
|
|
if len(main_invocation) > 1 or can_use_bare(parser):
|
|
main_invocation = ' '.join(main_invocation)
|
|
main_invocation = f'> {main_invocation}'
|
|
else:
|
|
main_invocation = ''
|
|
|
|
# SUBPARSER PREVIEWS
|
|
# If this program has subparsers, we will generate a preview of their name
|
|
# and description. If full_subparsers is True, we also show their full
|
|
# invocation and argument helps.
|
|
|
|
subparser_previews = []
|
|
for (sp, aliases) in get_subparsers(parser).items():
|
|
sp_help = []
|
|
for alias in aliases:
|
|
sp_help.append(f'{program_name} {color.command}{alias}{color.reset}')
|
|
if full_subparsers:
|
|
desc = make_helptext(
|
|
sp,
|
|
command_name=aliases[0],
|
|
do_headline=False,
|
|
all_command_names=all_command_names,
|
|
)
|
|
desc = textwrap.dedent(desc).strip()
|
|
desc = textwrap.indent(desc, ' ')
|
|
sp_help.append(f'{desc}')
|
|
elif sp.description is not None:
|
|
first_para = textwrap.dedent(sp.description).split('\n\n')[0].strip()
|
|
first_para = textwrap.indent(first_para, ' ')
|
|
sp_help.append(f'{first_para}')
|
|
sp_help = '\n'.join(sp_help)
|
|
subparser_previews.append(sp_help)
|
|
|
|
if subparser_previews:
|
|
subparser_previews = '\n\n'.join(subparser_previews)
|
|
subparser_previews = f'{color.command}Commands{color.reset}\n--------\n\n{subparser_previews}'
|
|
|
|
# COLORIZE EXAMPLE INVOCATIONS
|
|
# Here we take example invocation strings provided by the program itself,
|
|
# and run them through the argparser to colorize the positional, named,
|
|
# and flag arguments.
|
|
# argparse does not expose to us exactly which input strings led to which
|
|
# members of the outputted namespace, so we have to deduce it based on dest.
|
|
|
|
def dear_argparse_please_dont_call_sys_exit_im_trying_to_work_here(message):
|
|
raise TypeError()
|
|
|
|
example_invocations = []
|
|
for example in getattr(parser, 'examples', []):
|
|
args = example
|
|
if isinstance(args, dict):
|
|
args = args['args']
|
|
if isinstance(args, str):
|
|
args = shlex.split(args, posix=os.name != 'nt')
|
|
example_invocation = [invoke_name]
|
|
parser.error = dear_argparse_please_dont_call_sys_exit_im_trying_to_work_here
|
|
|
|
# more_positional is a list of positional arguments that we will put
|
|
# after -- in the colorized output. Since the argparse namespace will
|
|
# not tell us which positional arguments came from before or after
|
|
# the --, we will have to figure it out ourselves.
|
|
doubledash_parts = listsplit(args, '--', count=1)
|
|
if len(doubledash_parts) == 2:
|
|
more_positional = doubledash_parts[-1]
|
|
else:
|
|
more_positional = []
|
|
|
|
# more_positional_verify is a list of arguments we expect to be in
|
|
# more_positional but have not yet seen. This is needed because of the
|
|
# argparse type parameter which means a value in the namespace might
|
|
# not match the string that was inputted and we won't recognize it.
|
|
more_positional_verify = more_positional[:]
|
|
|
|
try:
|
|
parsed_example = parser.parse_args(args)
|
|
except TypeError:
|
|
example_invocation.extend(subproctools.quote(arg) for arg in args)
|
|
else:
|
|
keyvals = parsed_example.__dict__.copy()
|
|
keyvals.pop('func', None)
|
|
positional_args = []
|
|
for (dest, value) in list(keyvals.items()):
|
|
action = dest_to_action[dest]
|
|
if value == action.default:
|
|
keyvals.pop(dest)
|
|
continue
|
|
if action not in positional_actions:
|
|
continue
|
|
if action.nargs == '*' and value == []:
|
|
keyvals.pop(dest)
|
|
continue
|
|
if isinstance(value, list):
|
|
positional_args.extend(value)
|
|
else:
|
|
positional_args.append(value)
|
|
keyvals.pop(dest)
|
|
|
|
positional_args2 = []
|
|
for arg in reversed(positional_args):
|
|
try:
|
|
more_positional_verify.remove(arg)
|
|
except ValueError:
|
|
positional_args2.append(arg)
|
|
|
|
positional_args2.reverse()
|
|
positional_args = positional_args2
|
|
|
|
if positional_args:
|
|
positional_inv = ' '.join(subproctools.quote(str(arg)) for arg in positional_args)
|
|
positional_inv = f'{color.positional}{positional_inv}{color.reset}'
|
|
example_invocation.append(positional_inv)
|
|
for (dest, value) in list(keyvals.items()):
|
|
action = dest_to_action[dest]
|
|
if action in named_actions:
|
|
if isinstance(value, list):
|
|
value = ' '.join(subproctools.quote(str(arg)) for arg in value)
|
|
else:
|
|
value = subproctools.quote(str(value))
|
|
inv = f'{color.named}{action.option_strings[0]} {value}{color.reset}'
|
|
elif action in flag_actions:
|
|
inv = f'{color.flag}{action.option_strings[0]}{color.reset}'
|
|
example_invocation.append(inv)
|
|
|
|
# While we were looking for more_positional arguments, some of them
|
|
# may have slipped past us because of their type parameter. That is,
|
|
# the parsed namespace contains ints or other objects that were not
|
|
# matched to the strings in more_positional_verify. So, we remove
|
|
# the remaining more_positional_verify from more_positional, and
|
|
# those objects will appear in the regular positional area instead
|
|
# of after the --. This could be problematic if the input string
|
|
# has a leading hyphen and the type parameter turned it into
|
|
# something else (a negative number), but I think it's the best
|
|
# we can do without being able to inspect argparse's decisions.
|
|
|
|
# Not using set operations or [x if x not in verify] here because
|
|
# the positional arguments can be duplicates and the quantity of
|
|
# removals matters.
|
|
while more_positional_verify:
|
|
more_positional.remove(more_positional_verify.pop(0))
|
|
|
|
if more_positional:
|
|
example_invocation.append('--')
|
|
more_positional = ' '.join(subproctools.quote(str(arg)) for arg in more_positional)
|
|
more_positional = f'{color.positional}{more_positional}{color.reset}'
|
|
example_invocation.append(more_positional)
|
|
|
|
example_invocation = ' '.join(example_invocation)
|
|
example_invocation = f'> {example_invocation}'
|
|
if isinstance(example, dict) and example.get('comment'):
|
|
comment = example['comment']
|
|
example_invocation = f'# {comment}\n{example_invocation}'
|
|
|
|
if isinstance(example, dict) and example.get('run'):
|
|
_stdout = sys.stdout
|
|
_stderr = sys.stderr
|
|
buffer = io.StringIO()
|
|
sys.stdout = buffer
|
|
sys.stderr = buffer
|
|
try:
|
|
parsed_example.func(parsed_example)
|
|
buffer.seek(0)
|
|
output = buffer.read().strip()
|
|
buffer.close()
|
|
example_invocation = f'{example_invocation}\n{output}'
|
|
sys.stdout = _stdout
|
|
sys.stderr = _stderr
|
|
except Exception:
|
|
sys.stdout = _stdout
|
|
sys.stderr = _stderr
|
|
raise
|
|
|
|
example_invocations.append(example_invocation)
|
|
|
|
example_invocations = '\n\n'.join(example_invocations)
|
|
if example_invocations:
|
|
example_invocations = f'Examples:\n{example_invocations}'
|
|
|
|
if subparser_previews and not full_subparsers:
|
|
subparser_epilogue = textwrap.dedent(f'''
|
|
To see details on each command, run
|
|
> {program_name} {color.command}<command>{color.reset} {color.flag}--help{color.reset}
|
|
''').strip()
|
|
else:
|
|
subparser_epilogue = None
|
|
|
|
# PUT IT ALL TOGETHER
|
|
|
|
parts = [
|
|
niceprints.equals_header(program_name) if do_headline else None,
|
|
program_description,
|
|
main_invocation,
|
|
'\n\n'.join(argument_helps),
|
|
subparser_previews,
|
|
subparser_epilogue,
|
|
example_invocations,
|
|
]
|
|
parts = [part for part in parts if part]
|
|
parts = [part.strip() for part in parts]
|
|
parts = [part for part in parts if part]
|
|
helptext = '\n\n'.join(parts)
|
|
return helptext
|
|
|
|
def print_helptext(text) -> None:
|
|
'''
|
|
Print the given text to stderr, along with any epilogues added by
|
|
other modules.
|
|
'''
|
|
fulltext = []
|
|
fulltext.append(text.strip())
|
|
epilogues = {textwrap.dedent(epi).strip() for epi in HELPTEXT_EPILOGUES}
|
|
fulltext.extend(sorted(epi.strip() for epi in epilogues))
|
|
separator = '\n' + ('-' * 80) + '\n'
|
|
fulltext = separator.join(fulltext)
|
|
# Ensure one blank line above helptext.
|
|
pipeable.stderr()
|
|
pipeable.stderr(fulltext)
|
|
|
|
# MAINS
|
|
################################################################################
|
|
|
|
def _go_single(parser, argv, *, args_postprocessor=None):
|
|
can_bare = can_use_bare(parser)
|
|
|
|
needs_help = (
|
|
any(arg.lower() in HELP_ARGS for arg in argv) or
|
|
len(argv) == 0 and not can_bare
|
|
)
|
|
if needs_help:
|
|
do_colors = os.environ.get('NO_COLOR', None) is None
|
|
print_helptext(make_helptext(parser, do_colors=do_colors))
|
|
return 1
|
|
|
|
args = parser.parse_args(argv)
|
|
if args_postprocessor is not None:
|
|
args = args_postprocessor(args)
|
|
return args.func(args)
|
|
|
|
def _go_multi(parser, argv, *, args_postprocessor=None):
|
|
subparsers = get_subparser_action(parser).choices
|
|
can_bare = can_use_bare(parser)
|
|
|
|
def main(argv):
|
|
args = parser.parse_args(argv)
|
|
if args_postprocessor is not None:
|
|
args = args_postprocessor(args)
|
|
return args.func(args)
|
|
|
|
all_command_names = set(subparsers.keys())
|
|
command = listget(argv, 0, '').lower()
|
|
|
|
if command == '' and can_bare:
|
|
return main(argv)
|
|
|
|
do_colors = os.environ.get('NO_COLOR', None) is None
|
|
|
|
if command == 'helpall':
|
|
print_helptext(make_helptext(parser, full_subparsers=True, all_command_names=all_command_names, do_colors=do_colors))
|
|
return 1
|
|
|
|
if command == '':
|
|
print_helptext(make_helptext(parser, all_command_names=all_command_names, do_colors=do_colors))
|
|
because = 'you did not choose a command'
|
|
pipeable.stderr(f'\nYou are seeing the default help text because {because}.')
|
|
return 1
|
|
|
|
if command in HELP_COMMANDS:
|
|
print_helptext(make_helptext(parser, all_command_names=all_command_names, do_colors=do_colors))
|
|
return 1
|
|
|
|
if command not in subparsers:
|
|
print_helptext(make_helptext(parser, all_command_names=all_command_names, do_colors=do_colors))
|
|
because = f'"{command}" was not recognized'
|
|
pipeable.stderr(f'\nYou are seeing the default help text because {because}.')
|
|
return 1
|
|
|
|
subparser = subparsers[command]
|
|
arguments = argv[1:]
|
|
|
|
no_args = len(arguments) == 0 and not can_use_bare(subparser)
|
|
if no_args or any(arg.lower() in HELP_ARGS for arg in arguments):
|
|
print_helptext(make_helptext(subparser, command_name=command, all_command_names=all_command_names, do_colors=do_colors))
|
|
return 1
|
|
|
|
return main(argv)
|
|
|
|
def go(parser, argv, *, args_postprocessor=None):
|
|
if get_subparser_action(parser):
|
|
return _go_multi(parser, argv, args_postprocessor=args_postprocessor)
|
|
else:
|
|
return _go_single(parser, argv, args_postprocessor=args_postprocessor)
|