Closed kameroncarverWPN closed 1 year ago
Hi! click
does dynamic completions hence is quite slow, and is also not built-in (unlike argparse
) so I don't use it much.
Would be happy to accept a PR though!
However I suspect the easiest thing to do is convert click
objects to argparse.ArgumentParser
(same idea as argopt
, which converts docopt
to argparse
)... then tools like shtab
will work without modification.
Your suggestion ended up being significantly easier than I was expecting! A hacky solution:
import click
import argparse
import shtab
@main.command(hidden=True)
@click.pass_context
def completion(ctx):
"""Generate bash completion script"""
root = ctx.find_root()
rootcmd = root.command
main_parser = add_to_parser(rootcmd, argparse.ArgumentParser(prog=rootcmd.name))
print(shtab.complete(main_parser))
def add_to_parser(command, parser):
for param in command.params:
if param.param_type_name == "option":
spec = {}
if param.is_flag:
spec["action"] = "store_true"
else:
spec["nargs"] = param.nargs
spec["required"] = param.required
if isinstance(param.type, click.Choice):
spec["choices"] = param.type.choices
arg = parser.add_argument(*param.opts, **spec)
if isinstance(param.type, click.File):
arg.complete = shtab.FILE
elif isinstance(param.type, click.Path):
if param.type.file_okay and not param.type.dir_okay:
arg.complete = shtab.FILE
elif not param.type.file_okay and param.type.dir_okay:
arg.complete = shtab.DIRECTORY
elif param.param_type_name == "argument":
spec = {}
if param.nargs == -1:
spec["nargs"] = "+" if param.required else "*"
else:
spec["nargs"] = param.nargs
if isinstance(param.type, click.Choice):
spec["choices"] = param.type.choices
arg = parser.add_argument(param.name, **spec)
if isinstance(param.type, click.File):
arg.complete = shtab.FILE
elif isinstance(param.type, click.Path):
if param.type.file_okay and not param.type.dir_okay:
arg.complete = shtab.FILE
elif not param.type.file_okay and param.type.dir_okay:
arg.complete = shtab.DIRECTORY
if hasattr(command, "commands") and len(command.commands) > 0:
subparsers = parser.add_subparsers()
subparsers.required = True
subparsers.dest = "subcommand"
for name, subcmd in command.commands.items():
if subcmd.hidden:
continue
# non-empty help necessary or argparse doesn't consider it an action??
subparser = subparsers.add_parser(name, help=command.help)
add_to_parser(subcmd, subparser)
return parser
This is all I needed for my project, but this could expanded to take advantage of other shtab features.
Thanks for your great project!
Excellent! Were you thinking of releasing this as a small stand-alone package? Alternatively, if you'd prefer, I'd be happy to accept a PR creating shtab/contrib/click.py
or similar :)
I have a highly nested click-based cli project with many imports, so the built-in click shell completion is unbearably slow (several seconds per tab complete). I would like to modify shtab to generate a bash completion file for it.
Looking at the code, I don't see any obvious stoppers to modifying
get_bash_commands
to pull the same information out of a click.Command or click.Group that is pulled out of a parser/subparser, but I admit I do not understand how the actual bash completion script works.Have you looked at how click's Command/Group works before, and compared it to argparse-style parsers? Any reason why you think modifying shtab in this way wouldn't work? Any pointers for what I would need to do to make it work?
Thanks!