Big updates to betterhelp.
Instead of handwriting the help text, which was time consuming and prone to errors, I'm finally using the help parameter in parser.add_argument. betterhelp will render and colorize this for some good-looking automatic help text. The make_helptext function is still extremely long and would be nice to refactor, but I've been sitting on this commit for a few weeks now and I want to get my git repositories back in sync.
This commit is contained in:
parent
c1cba4f287
commit
232398eff0
4 changed files with 683 additions and 271 deletions
|
@ -1,7 +1,16 @@
|
||||||
import argparse
|
import argparse
|
||||||
import functools
|
import colorama
|
||||||
|
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 pipeable
|
||||||
|
from voussoirkit import subproctools
|
||||||
from voussoirkit import vlogging
|
from voussoirkit import vlogging
|
||||||
|
|
||||||
log = vlogging.get_logger(__name__)
|
log = vlogging.get_logger(__name__)
|
||||||
|
@ -33,18 +42,6 @@ HELPTEXT_EPILOGUES = set()
|
||||||
|
|
||||||
# INTERNALS
|
# INTERNALS
|
||||||
################################################################################
|
################################################################################
|
||||||
def add_previews(docstring, sub_docstrings) -> str:
|
|
||||||
'''
|
|
||||||
Given a primary docstring which contains {command_name} formatting elements,
|
|
||||||
and a dict of sub_docstrings of {command_name: docstring}, insert previews
|
|
||||||
of each command into the primary docstring.
|
|
||||||
'''
|
|
||||||
previews = {
|
|
||||||
sub_name: docstring_preview(sub_text)
|
|
||||||
for (sub_name, sub_text) in sub_docstrings.items()
|
|
||||||
}
|
|
||||||
docstring = docstring.format(**previews)
|
|
||||||
return docstring
|
|
||||||
|
|
||||||
def can_use_bare(parser) -> bool:
|
def can_use_bare(parser) -> bool:
|
||||||
'''
|
'''
|
||||||
|
@ -52,15 +49,6 @@ def can_use_bare(parser) -> bool:
|
||||||
This is used to decide whether running `> myprogram.py` should show the
|
This is used to decide whether running `> myprogram.py` should show the
|
||||||
helptext or just run normally.
|
helptext or just run normally.
|
||||||
'''
|
'''
|
||||||
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
|
|
||||||
|
|
||||||
has_func = bool(parser.get_default('func'))
|
has_func = bool(parser.get_default('func'))
|
||||||
has_required_args = any(is_required(action) for action in parser._actions)
|
has_required_args = any(is_required(action) for action in parser._actions)
|
||||||
return has_func and not has_required_args
|
return has_func and not has_required_args
|
||||||
|
@ -75,32 +63,39 @@ def can_use_bare_subparsers(subparser_action) -> set:
|
||||||
)
|
)
|
||||||
return can_bares
|
return can_bares
|
||||||
|
|
||||||
def docstring_preview(text) -> str:
|
def get_program_name():
|
||||||
'''
|
program_name = os.path.basename(sys.argv[0])
|
||||||
This function assumes that your docstring is formatted with a single blank
|
program_name = re.sub(r'\.pyw?$', '', program_name)
|
||||||
line separating the command's primary summary and the rest of the text,
|
return program_name
|
||||||
and will return the summary above the blank.
|
|
||||||
|
|
||||||
>>> cookbacon = """
|
|
||||||
... cookbacon: Cooks all nearby bacon to a specified temperature.
|
|
||||||
...
|
|
||||||
... Usage:
|
|
||||||
... > cookbacon 350F
|
|
||||||
... > cookbacon 175C
|
|
||||||
... """
|
|
||||||
>>>
|
|
||||||
>>>
|
|
||||||
>>> docstring_preview(cookbacon)
|
|
||||||
'cookbacon: Cooks all nearby bacon to a specified temperature.'
|
|
||||||
'''
|
|
||||||
text = text.split('\n\n')[0].strip()
|
|
||||||
return text
|
|
||||||
|
|
||||||
def get_subparser_action(parser):
|
def get_subparser_action(parser):
|
||||||
for action in parser._actions:
|
for action in parser._actions:
|
||||||
if isinstance(action, argparse._SubParsersAction):
|
if isinstance(action, argparse._SubParsersAction):
|
||||||
return action
|
return action
|
||||||
raise TypeError('Couldn\'t locate the SubParsersAction.')
|
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):
|
def listget(li, index, fallback=None):
|
||||||
try:
|
try:
|
||||||
|
@ -108,6 +103,432 @@ def listget(li, index, fallback=None):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return fallback
|
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 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=colorama.Style.BRIGHT + colorama.Fore.RED + '(*)' + colorama.Style.RESET_ALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
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) is argparse._StoreAction:
|
||||||
|
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:
|
def print_helptext(text) -> None:
|
||||||
'''
|
'''
|
||||||
Print the given text to stderr, along with any epilogues added by
|
Print the given text to stderr, along with any epilogues added by
|
||||||
|
@ -115,147 +536,81 @@ def print_helptext(text) -> None:
|
||||||
'''
|
'''
|
||||||
fulltext = []
|
fulltext = []
|
||||||
fulltext.append(text.strip())
|
fulltext.append(text.strip())
|
||||||
fulltext.extend(sorted(epi.strip() for epi in HELPTEXT_EPILOGUES))
|
epilogues = {textwrap.dedent(epi).strip() for epi in HELPTEXT_EPILOGUES}
|
||||||
separator = '\n' + ('-'*80) + '\n'
|
fulltext.extend(sorted(epi.strip() for epi in epilogues))
|
||||||
|
separator = '\n' + ('-' * 80) + '\n'
|
||||||
fulltext = separator.join(fulltext)
|
fulltext = separator.join(fulltext)
|
||||||
# Ensure one blank line above helptext.
|
# Ensure one blank line above helptext.
|
||||||
pipeable.stderr()
|
pipeable.stderr()
|
||||||
pipeable.stderr(fulltext)
|
pipeable.stderr(fulltext)
|
||||||
|
|
||||||
def set_alias_docstrings(sub_docstrings, subparser_action) -> dict:
|
# MAINS
|
||||||
'''
|
|
||||||
When using subparser aliases like the following:
|
|
||||||
|
|
||||||
subp = parser.add_subparser('do_this', aliases=['do-this'])
|
|
||||||
|
|
||||||
The _SubParsersAction will contain a dictionary `choices` of
|
|
||||||
{'do_this': ArgumentParser, 'do-this': ArgumentParser}.
|
|
||||||
This choices dict does not indicate which one was the original name;
|
|
||||||
all aliases are equal. So, we'll identify which names are aliases because
|
|
||||||
their ArgumentParsers will have the same ID in memory. And, as long as one
|
|
||||||
of those aliases is in the provided docstrings, all the other aliases will
|
|
||||||
get that docstring too.
|
|
||||||
|
|
||||||
This function modifies the given sub_docstrings so that all aliases are
|
|
||||||
present as keys.
|
|
||||||
'''
|
|
||||||
sub_docstrings = {name.lower(): docstring for (name, docstring) in sub_docstrings.items()}
|
|
||||||
# aliases is a map of {action object's id(): [list of alias name strings]}.
|
|
||||||
aliases = {}
|
|
||||||
# primary_aliases is {action object's id(): 'name string'}
|
|
||||||
primary_aliases = {}
|
|
||||||
|
|
||||||
for (sp_name, sp) in subparser_action.choices.items():
|
|
||||||
sp_id = id(sp)
|
|
||||||
sp_name = sp_name.lower()
|
|
||||||
aliases.setdefault(sp_id, []).append(sp_name)
|
|
||||||
if sp_name in sub_docstrings:
|
|
||||||
primary_aliases[sp_id] = sp_name
|
|
||||||
|
|
||||||
for (sp_id, sp_aliases) in aliases.items():
|
|
||||||
try:
|
|
||||||
primary_alias = primary_aliases[sp_id]
|
|
||||||
except KeyError:
|
|
||||||
log.warning('There is no docstring for any of %s.', sp_aliases)
|
|
||||||
docstring = ''
|
|
||||||
else:
|
|
||||||
docstring = sub_docstrings[primary_alias]
|
|
||||||
|
|
||||||
for sp_alias in sp_aliases:
|
|
||||||
sub_docstrings[sp_alias] = docstring
|
|
||||||
|
|
||||||
return sub_docstrings
|
|
||||||
|
|
||||||
# DECORATORS
|
|
||||||
################################################################################
|
################################################################################
|
||||||
def single_betterhelp(parser, docstring):
|
|
||||||
'''
|
def _go_single(parser, argv, *, args_postprocessor=None):
|
||||||
This decorator actually doesn't need the `parser`, but the
|
|
||||||
subparser_betterhelp decorator does, so in the interest of having similar
|
|
||||||
function signatures I'm making it required here too. I figure it's the
|
|
||||||
lesser of two evils. Plus, maybe someday I'll find a need for it and won't
|
|
||||||
have to make any changes to do it.
|
|
||||||
'''
|
|
||||||
can_bare = can_use_bare(parser)
|
can_bare = can_use_bare(parser)
|
||||||
def wrapper(main):
|
|
||||||
@functools.wraps(main)
|
|
||||||
def wrapped(argv):
|
|
||||||
if len(argv) == 0 and can_bare:
|
|
||||||
return main(argv)
|
|
||||||
|
|
||||||
if len(argv) == 0:
|
needs_help = (
|
||||||
print_helptext(docstring)
|
any(arg.lower() in HELP_ARGS for arg in argv) or
|
||||||
return 1
|
len(argv) == 0 and not can_bare
|
||||||
|
)
|
||||||
|
if needs_help:
|
||||||
|
print_helptext(make_helptext(parser))
|
||||||
|
return 1
|
||||||
|
|
||||||
if any(arg.lower() in HELP_ARGS for arg in argv):
|
args = parser.parse_args(argv)
|
||||||
print_helptext(docstring)
|
if args_postprocessor is not None:
|
||||||
return 1
|
args = args_postprocessor(args)
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
return main(argv)
|
def _go_multi(parser, argv, *, args_postprocessor=None):
|
||||||
return wrapped
|
subparsers = get_subparser_action(parser).choices
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def subparser_betterhelp(parser, main_docstring, sub_docstrings):
|
|
||||||
subparser_action = get_subparser_action(parser)
|
|
||||||
sub_docstrings = set_alias_docstrings(sub_docstrings, subparser_action)
|
|
||||||
can_bare = can_use_bare(parser)
|
can_bare = can_use_bare(parser)
|
||||||
can_bares = can_use_bare_subparsers(subparser_action)
|
|
||||||
|
|
||||||
def wrapper(main):
|
|
||||||
@functools.wraps(main)
|
|
||||||
def wrapped(argv):
|
|
||||||
command = listget(argv, 0, '').lower()
|
|
||||||
|
|
||||||
if command == '' and can_bare:
|
|
||||||
return main(argv)
|
|
||||||
|
|
||||||
if command == '':
|
|
||||||
print_helptext(main_docstring)
|
|
||||||
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(main_docstring)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if command not in sub_docstrings:
|
|
||||||
print_helptext(main_docstring)
|
|
||||||
because = f'"{command}" was not recognized'
|
|
||||||
pipeable.stderr(f'\nYou are seeing the default help text because {because}.')
|
|
||||||
return 1
|
|
||||||
|
|
||||||
arguments = argv[1:]
|
|
||||||
|
|
||||||
if len(arguments) == 0 and command in can_bares:
|
|
||||||
return main(argv)
|
|
||||||
|
|
||||||
if len(arguments) == 0:
|
|
||||||
print_helptext(sub_docstrings[command])
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if any(arg.lower() in HELP_ARGS for arg in arguments):
|
|
||||||
print_helptext(sub_docstrings[command])
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return main(argv)
|
|
||||||
return wrapped
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
# EASY MAINS
|
|
||||||
################################################################################
|
|
||||||
def single_main(argv, parser, docstring, args_postprocessor=None):
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
if args_postprocessor is not None:
|
if args_postprocessor is not None:
|
||||||
args = args_postprocessor(args)
|
args = args_postprocessor(args)
|
||||||
return args.func(args)
|
return args.func(args)
|
||||||
return single_betterhelp(parser, docstring)(main)(argv)
|
|
||||||
|
|
||||||
def subparser_main(argv, parser, main_docstring, sub_docstrings, args_postprocessor=None):
|
all_command_names = set(subparsers.keys())
|
||||||
def main(argv):
|
command = listget(argv, 0, '').lower()
|
||||||
args = parser.parse_args(argv)
|
|
||||||
if args_postprocessor is not None:
|
if command == '' and can_bare:
|
||||||
args = args_postprocessor(args)
|
return main(argv)
|
||||||
return args.func(args)
|
|
||||||
return subparser_betterhelp(parser, main_docstring, sub_docstrings)(main)(argv)
|
if command == '':
|
||||||
|
print_helptext(make_helptext(parser, all_command_names=all_command_names))
|
||||||
|
because = 'you did not choose a command'
|
||||||
|
pipeable.stderr(f'\nYou are seeing the default help text because {because}.')
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if command == 'helpall':
|
||||||
|
print_helptext(make_helptext(parser, full_subparsers=True, all_command_names=all_command_names))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if command in HELP_COMMANDS:
|
||||||
|
print_helptext(make_helptext(parser, all_command_names=all_command_names))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if command not in subparsers:
|
||||||
|
print_helptext(make_helptext(parser, all_command_names=all_command_names))
|
||||||
|
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))
|
||||||
|
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)
|
||||||
|
|
|
@ -1,23 +1,8 @@
|
||||||
'''
|
'''
|
||||||
bytestring
|
|
||||||
==========
|
|
||||||
|
|
||||||
This module provides integer constants for power-of-two byte size units, and
|
This module provides integer constants for power-of-two byte size units, and
|
||||||
functions for converting between ints and human-readable strings. E.g.:
|
functions for converting between ints and human-readable strings. E.g.:
|
||||||
bytestring.bytestring(5000000) -> '4.768 MiB'
|
bytestring.bytestring(5000000) -> '4.768 MiB'
|
||||||
bytestring.parsebytes('8.5gb') -> 9126805504
|
bytestring.parsebytes('8.5gb') -> 9126805504
|
||||||
|
|
||||||
Commandline usage:
|
|
||||||
|
|
||||||
> bytestring number1 [number2 number3...]
|
|
||||||
|
|
||||||
number:
|
|
||||||
An integer. Uses pipeable to support !c clipboard, !i stdin, which should
|
|
||||||
be one number per line.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
> bytestring 12345 89989
|
|
||||||
> some_process | bytestring !i
|
|
||||||
'''
|
'''
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
|
@ -186,22 +171,40 @@ def parsebytes(string):
|
||||||
return int(number * multiplier)
|
return int(number * multiplier)
|
||||||
|
|
||||||
def bytestring_argparse(args):
|
def bytestring_argparse(args):
|
||||||
numbers = pipeable.input_many(args.numbers)
|
numbers = pipeable.input_many(args.numbers, strip=True, skip_blank=True)
|
||||||
for number in numbers:
|
for number in numbers:
|
||||||
try:
|
try:
|
||||||
number = int(number)
|
number = int(number)
|
||||||
|
pipeable.stdout(bytestring(number))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pipeable.stderr(f'bytestring: Each number should be an integer, not {number}.')
|
pipeable.stdout(parsebytes(number))
|
||||||
return 1
|
return 0
|
||||||
pipeable.stdout(bytestring(number))
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(
|
||||||
|
description='''
|
||||||
|
Converts integers into byte strings and back again.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.examples = [
|
||||||
|
{'args': '10000', 'run': True},
|
||||||
|
{'args': '123456789', 'run': True},
|
||||||
|
{'args': '999999999999 888888888 890', 'run': True},
|
||||||
|
{'args': ['800 gb'], 'run': True},
|
||||||
|
{'args': ['9.2 kib', '100kb', '42b'], 'run': True},
|
||||||
|
]
|
||||||
|
|
||||||
parser.add_argument('numbers', nargs='+')
|
parser.add_argument(
|
||||||
|
'numbers',
|
||||||
|
nargs='+',
|
||||||
|
help='''
|
||||||
|
Uses pipeable to support !c clipboard, !i stdin, which should be one
|
||||||
|
number per line.
|
||||||
|
''',
|
||||||
|
)
|
||||||
parser.set_defaults(func=bytestring_argparse)
|
parser.set_defaults(func=bytestring_argparse)
|
||||||
|
|
||||||
return betterhelp.single_main(argv, parser, __doc__)
|
return betterhelp.go(parser, argv)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
raise SystemExit(main(sys.argv[1:]))
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
|
|
|
@ -32,20 +32,9 @@ If your application already uses the logging module, consider these options:
|
||||||
- wrap your function call in a operatornotify.LogHandlerContext, or
|
- wrap your function call in a operatornotify.LogHandlerContext, or
|
||||||
- add @operatornotify.main_decorator to your main function.
|
- add @operatornotify.main_decorator to your main function.
|
||||||
|
|
||||||
Commandline usage:
|
|
||||||
> operatornotify --subject XXX [--body XXX]
|
|
||||||
|
|
||||||
--subject xxx:
|
|
||||||
A string. Uses pipeable to support !c clipboard, !i stdin.
|
|
||||||
Required.
|
|
||||||
|
|
||||||
--body xxx:
|
|
||||||
A string. Uses pipeable to support !c clipboard, !i stdin.
|
|
||||||
Optional.
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
> some_process && operatornotify --subject success || operatornotify --subject fail
|
> some_process && operatornotify --subject success || operatornotify --subject fail
|
||||||
> some_process | operatornotify --subject "Results of some_process" --body !i 2>&1
|
> some_process 2>&1 | operatornotify --subject "Results of some_process" --body !i
|
||||||
'''
|
'''
|
||||||
import argparse
|
import argparse
|
||||||
import contextlib
|
import contextlib
|
||||||
|
@ -324,19 +313,29 @@ def parse_argv(argv):
|
||||||
def operatornotify_argparse(args):
|
def operatornotify_argparse(args):
|
||||||
notify(
|
notify(
|
||||||
subject=pipeable.input(args.subject, split_lines=False).strip(),
|
subject=pipeable.input(args.subject, split_lines=False).strip(),
|
||||||
body=pipeable.input(args.body, split_lines=False),
|
body=pipeable.input(args.body or '', split_lines=False),
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@vlogging.main_decorator
|
@vlogging.main_decorator
|
||||||
def main(argv):
|
def main(argv):
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
parser.add_argument('--subject', required=True)
|
'--subject',
|
||||||
parser.add_argument('--body', default='')
|
required=True,
|
||||||
|
help='''
|
||||||
|
A string. Uses pipeable to support !c clipboard, !i stdin.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--body',
|
||||||
|
help='''
|
||||||
|
A string. Uses pipeable to support !c clipboard, !i stdin.
|
||||||
|
''',
|
||||||
|
)
|
||||||
parser.set_defaults(func=operatornotify_argparse)
|
parser.set_defaults(func=operatornotify_argparse)
|
||||||
|
|
||||||
return betterhelp.single_main(argv, parser, __doc__)
|
return betterhelp.go(parser, argv)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
raise SystemExit(main(sys.argv[1:]))
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
|
|
|
@ -1,57 +1,9 @@
|
||||||
'''
|
'''
|
||||||
passwordy
|
|
||||||
=========
|
|
||||||
|
|
||||||
This module provides functions for generating random strings. All functions use
|
This module provides functions for generating random strings. All functions use
|
||||||
cryptographically strong randomness if the operating system supports it, and
|
cryptographically strong randomness if the operating system supports it, and
|
||||||
non-cs randomness if it does not.
|
non-cs randomness if it does not.
|
||||||
|
|
||||||
If os.urandom(1) gives you a byte, your system has cs randomness.
|
If os.urandom(1) gives you a byte, your system has cs randomness.
|
||||||
|
|
||||||
Command line usage:
|
|
||||||
|
|
||||||
> passwordy length <flags>
|
|
||||||
|
|
||||||
length:
|
|
||||||
Integer number of characters in normal mode.
|
|
||||||
Integer number of words in sentence mode.
|
|
||||||
|
|
||||||
# Sentence mode:
|
|
||||||
--sentence:
|
|
||||||
If this argument is passed, `length` random words are chosen.
|
|
||||||
|
|
||||||
--separator X:
|
|
||||||
When using sentence mode, the words will be joined with this string.
|
|
||||||
|
|
||||||
# Normal mode:
|
|
||||||
--letters:
|
|
||||||
Include ASCII letters in the password.
|
|
||||||
If none of the other following options are chosen, letters is the default.
|
|
||||||
|
|
||||||
--digits:
|
|
||||||
Include digits 0-9 in the password.
|
|
||||||
|
|
||||||
--hex
|
|
||||||
Include 0-9, a-f in the password.
|
|
||||||
|
|
||||||
--binary
|
|
||||||
Include 0, 1 in the password.
|
|
||||||
|
|
||||||
--punctuation
|
|
||||||
Include punctuation symbols in the password.
|
|
||||||
|
|
||||||
# Both modes:
|
|
||||||
--upper
|
|
||||||
Convert the entire password to uppercase.
|
|
||||||
|
|
||||||
--lower
|
|
||||||
Convert the entire password to lowercase.
|
|
||||||
|
|
||||||
--prefix X:
|
|
||||||
Add a static prefix to the password.
|
|
||||||
|
|
||||||
--suffix X:
|
|
||||||
Add a static suffix to the password.
|
|
||||||
'''
|
'''
|
||||||
import argparse
|
import argparse
|
||||||
import math
|
import math
|
||||||
|
@ -61,6 +13,7 @@ import string
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from voussoirkit import betterhelp
|
from voussoirkit import betterhelp
|
||||||
|
from voussoirkit import gentools
|
||||||
from voussoirkit import pipeable
|
from voussoirkit import pipeable
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -142,6 +95,11 @@ def passwordy_argparse(args):
|
||||||
elif args.upper:
|
elif args.upper:
|
||||||
password = password.upper()
|
password = password.upper()
|
||||||
|
|
||||||
|
if args.groups_of is not None:
|
||||||
|
chunks = gentools.chunk_generator(password, args.groups_of)
|
||||||
|
chunks = (''.join(chunk) for chunk in chunks)
|
||||||
|
password = args.separator.join(chunks)
|
||||||
|
|
||||||
prefix = args.prefix or ''
|
prefix = args.prefix or ''
|
||||||
suffix = args.suffix or ''
|
suffix = args.suffix or ''
|
||||||
password = f'{prefix}{password}{suffix}'
|
password = f'{prefix}{password}{suffix}'
|
||||||
|
@ -150,23 +108,120 @@ def passwordy_argparse(args):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(
|
||||||
|
description='''
|
||||||
parser.add_argument('length', type=int)
|
Generate random passwords using cryptographically strong randomness.
|
||||||
parser.add_argument('--sentence', action='store_true')
|
''',
|
||||||
parser.add_argument('--separator', default=' ')
|
)
|
||||||
parser.add_argument('--letters', action='store_true')
|
parser.examples = [
|
||||||
parser.add_argument('--digits', action='store_true')
|
{'args': '32 --letters --digits --punctuation', 'run': True},
|
||||||
parser.add_argument('--hex', action='store_true')
|
{'args': '48 --hex --upper', 'run': True},
|
||||||
parser.add_argument('--binary', action='store_true')
|
{'args': '8 --sentence --separator +', 'run': True},
|
||||||
parser.add_argument('--punctuation', action='store_true')
|
{'args': '16 --digits --groups-of 4 --separator -', 'run': True},
|
||||||
parser.add_argument('--lower', action='store_true')
|
{'args': '48 --prefix example.com_ --lower', 'run': True},
|
||||||
parser.add_argument('--upper', action='store_true')
|
]
|
||||||
parser.add_argument('--prefix', default=None)
|
parser.add_argument(
|
||||||
parser.add_argument('--suffix', default=None)
|
'length',
|
||||||
|
type=int,
|
||||||
|
help='''
|
||||||
|
Integer number of characters in normal mode.
|
||||||
|
Integer number of words in sentence mode.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--sentence',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
If this argument is passed, the password is made of length random words and
|
||||||
|
the other alphabet options are ignored.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--groups_of', '--groups-of',
|
||||||
|
type=int,
|
||||||
|
help='''
|
||||||
|
Split the password up into chunks of this many characters, and join them
|
||||||
|
back together with the --separator.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--separator',
|
||||||
|
type=str,
|
||||||
|
default=' ',
|
||||||
|
help='''
|
||||||
|
In sentence mode, the words will be joined with this string.
|
||||||
|
In normal mode, the --groups-of chunks will be joined with this string.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--letters',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Include ASCII letters in the password.
|
||||||
|
If none of the other following options are chosen, letters is the default.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--digits',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Include digits 0-9 in the password.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--hex',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Include 0-9, a-f in the password.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--binary',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Include 0, 1 in the password.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--punctuation',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Include punctuation symbols in the password.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--prefix',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help='''
|
||||||
|
Add a static prefix to the password.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--suffix',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help='''
|
||||||
|
Add a static suffix to the password.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--lower',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Convert the entire password to lowercase.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--upper',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Convert the entire password to uppercase.
|
||||||
|
''',
|
||||||
|
)
|
||||||
parser.set_defaults(func=passwordy_argparse)
|
parser.set_defaults(func=passwordy_argparse)
|
||||||
|
|
||||||
return betterhelp.single_main(argv, parser, __doc__)
|
return betterhelp.go(parser, argv)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
raise SystemExit(main(sys.argv[1:]))
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
|
|
Loading…
Reference in a new issue