My program uses Click for command line processing. It has a main command that takes a required argument. This command has subcommands that take optional arguments. Different subcommands take different options, but they all require the same argument from their parent. I'd like to have the command line look like this:
python myprogram.py argument-value subcommand1 --option-1=value
I can write this using Click like so
import click
@click.group()
@click.argument("argument")
@click.pass_context
def main(context, argument):
"""ARGUMENT is required for both subcommands"""
context.obj = {"argument": argument}
@click.command()
@click.option("--option-1", help="option for subcommand 1")
@click.pass_context
def subcommand1(context, option_1):
print("subcommand 1: %s %s" % (context.obj["argument"], option_1))
@click.command()
@click.option("--option-2", help="option for subcommand 2")
@click.pass_context
def subcommand2(context, option_2):
print("subcommand 2: %s %s" % (context.obj["argument"], option_2))
main.add_command(subcommand1)
main.add_command(subcommand2)
if __name__ == "__main__":
main()
The top-level help message is what I want.
python myprogram.py --help
Usage: myprogram.py [OPTIONS] ARGUMENT COMMAND [ARGS]...
ARGUMENT is required for both subcommands
Options:
--help Show this message and exit.
Commands:
subcommand1
subcommand2
I can get help for a subcommand if I pass in the required argument.
python myprogram.py dummy-argument subcommand1 --help
Usage: myprogram.py subcommand1 [OPTIONS]
Options:
--option-1 TEXT option for subcommand 1
--help Show this message and exit.
However, I'd like to get the subcommand help without requiring the user to pass in a dummy argument. I want to be able to run python myprogram.py subcommand1 --help
and see the same output as above, but instead I just get the help text for the top level.
python myprogram.py subcommand1 --help
Usage: myprogram.py [OPTIONS] ARGUMENT COMMAND [ARGS]...
ARGUMENT is required for both subcommands
Options:
--help Show this message and exit.
Commands:
subcommand1
subcommand2
Is there a way to get the behavior I want? I realize that Click puts a premium on having each of its commands be self-contained, but this seems like a common scenario.
There is an inherent ambiguity in your requirements, in that the subcommand name could possibly be the same as a valid value for the common argument.
So, some way to disambiguate is required. I present one possible solution below.
When finding a value of the argument which matches a subcommand name, the proposed solution will search for the presence of --help
. If found it then assumes that help is being requested for the subcommand, and will populate the dummy-argument
automatically.
import click
class PerCommandArgWantSubCmdHelp(click.Argument):
def handle_parse_result(self, ctx, opts, args):
# check to see if there is a --help on the command line
if any(arg in ctx.help_option_names for arg in args):
# if asking for help see if we are a subcommand name
for arg in opts.values():
if arg in ctx.command.commands:
# this matches a sub command name, and --help is
# present, let's assume the user wants help for the
# subcommand
args = [arg] + args
return super(PerCommandArgWantSubCmdHelp, self).handle_parse_result(
ctx, opts, args)
To use the custom class, pass the cls
parameter to @click.argument()
decorator like:
@click.argument("argument", cls=PerCommandArgWantSubCmdHelp)
This works because click is a well designed OO framework. The @click.argument()
decorator usually instantiates a click.Argument
object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Argument
in our own class and over ride the desired methods.
In this case we over ride click.Argument.handle_parse_result()
and look for the pattern of a subcommand name followed by --help
. When found, we then doctor the arguments list to get the pattern click needs to parse this the way want to show the subcommand help.
@click.group()
@click.argument("argument", cls=PerCommandArgWantSubCmdHelp)
@click.pass_context
def main(context, argument):
"""ARGUMENT is required for both subcommands"""
context.obj = {"argument": argument}
@click.command()
@click.option("--option-1", help="option for subcommand 1")
@click.pass_context
def subcommand1(context, option_1):
print("subcommand 1: %s %s" % (context.obj["argument"], option_1))
@click.command()
@click.option("--option-2", help="option for subcommand 2")
@click.pass_context
def subcommand2(context, option_2):
print("subcommand 2: %s %s" % (context.obj["argument"], option_2))
main.add_command(subcommand1)
main.add_command(subcommand2)
if __name__ == "__main__":
commands = (
'subcommand1 --help',
'subcommand2 --help',
'dummy-argument subcommand1 --help',
)
for cmd in commands:
try:
print('-----------')
print('> ' + cmd)
main(cmd.split())
except:
pass
-----------
> subcommand1 --help
Backend TkAgg is interactive backend. Turning interactive mode on.
Usage: test.py subcommand1 [OPTIONS]
Options:
--option-1 TEXT option for subcommand 1
--help Show this message and exit.
-----------
> subcommand2 --help
Usage: test.py subcommand2 [OPTIONS]
Options:
--option-2 TEXT option for subcommand 2
--help Show this message and exit.
-----------
> dummy-argument subcommand1 --help
Usage: test.py subcommand1 [OPTIONS]
Options:
--option-1 TEXT option for subcommand 1
--help Show this message and exit.
Another possible approach is to define a custom decorator that adds the common argument/option and use this decorator on every subcommand. This way, click will not ask for the argument/option when issuing subcommand --help
.
A possible implementation is outlined in click's GitHub Issue 295. The example code shows how to add a general purpose decorator which can add different - yet shared - options. It does however solve OP's issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With