I'm trying to figure out how to get help for global options shown in the help messages of sub-commands.
I created a simplistic cli:
#!/usr/bin/env python
import click
#click.group()
#click.option("-l", "--log-level", help="Set log level.")
def cli(log_level):
"CLI toolbox"
print("root")
#cli.group()
def admin():
print("admin")
#admin.command()
def invite():
print("invite")
if __name__ == "__main__":
cli()
Unfortunately the global options are not shown on the help screens of sub commands:
./cli.py --help
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
CLI toolbox
Options:
-l, --log-level TEXT Set log level. <-- Option listed on global command
--help Show this message and exit.
Commands:
admin
./cli.py admin --help
root
Usage: cli.py admin [OPTIONS] COMMAND [ARGS]...
Options:
<- Option missing on command.
--help Show this message and exit.
Commands:
invite
This is by no means pretty, but it gets the options by defining a custom group so we can override the help message. It then also iterates through all subcommands, but that has been hardcoded to only reference that particular group, rather than programatically infer it.
import click
#click.group()
#click.option("-l", "--log-level", help="Set log level.")
def cli(log_level):
"CLI toolbox"
print("root")
class CustomHelpGroup(click.Group):
def format_help(self, ctx, formatter):
parent = ctx.parent
help_text = ['Greetings! Options:']
for param in parent.command.get_params(ctx):
help_text.append(' '.join(param.get_help_record(parent)))
help_text.append("\n" + ctx.get_usage() + "\n")
help_text.append('Commands:\n')
help_text.extend([f'{command_name}' for command_name, command in admin.commands.items()])
formatter.write('\n'.join(help_text))
#cli.group(cls=CustomHelpGroup)
def admin():
print("admin")
#admin.command()
def invite():
print("invite")
if __name__ == "__main__":
cli()
Related
I'm starting a CLI pipe-type application project which will eventually have a rather large collection of commands (which will be further extensible with plug-in). As a result, I would like to categorise them in the --help text:
Here is how it looks now:
Usage: my_pipe [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--help Show this message and exit.
Commands:
another_filter help about that filter
another_generator help about that generator
another_sink help about that sink
some_filter help about this filter
some_generator help about this generator
some_sink help about this sink
This is more or less how I would like it to look:
Usage: my_pipe [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--help Show this message and exit.
Commands:
Generators:
some_generator help about this generator
another_generator help about that generator
Filters:
some_filter help about this filter
another_filter help about that filter
Sinks:
some_sink help about this sink
another_sink help about that sink
How can this be achieved? Note that apart from the look of --help, I'm happy with the flat logical command organisation. Also, sub-groups are not an option as they are not allowed inside a chain=True group.
If you inherit from click.Group you can add a bit of code to group the commands and then show those groups in the help.
Custom Class
class GroupedGroup(click.Group):
def command(self, *args, **kwargs):
"""Gather the command help groups"""
help_group = kwargs.pop('group', None)
decorator = super(GroupedGroup, self).command(*args, **kwargs)
def wrapper(f):
cmd = decorator(f)
cmd.help_group = help_group
return cmd
return wrapper
def format_commands(self, ctx, formatter):
# Modified fom the base class method
commands = []
for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand)
if not (cmd is None or cmd.hidden):
commands.append((subcommand, cmd))
if commands:
longest = max(len(cmd[0]) for cmd in commands)
# allow for 3 times the default spacing
limit = formatter.width - 6 - longest
groups = {}
for subcommand, cmd in commands:
help_str = cmd.get_short_help_str(limit)
subcommand += ' ' * (longest - len(subcommand))
groups.setdefault(
cmd.help_group, []).append((subcommand, help_str))
with formatter.section('Commands'):
for group_name, rows in groups.items():
with formatter.section(group_name):
formatter.write_dl(rows)
Using the Custom Class
To make use of the custom class, use the cls parameter to pass the class to the click.group() decorator.
#click.group(cls=GroupedGroup)
def cli():
"""My awesome cli"""
Then for each command mark the help group for the command to be included in like:
#cli.command(group='A Help Group')
def command():
"""This is a command"""
How does this work?
This works because click is a well designed OO framework. The #click.group() decorator usually instantiates a click.Group object but allows this behavior to be overridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and override desired methods.
In this case we override the click.Group.command() decorator to gather up the desired help group for each command. Then we override the click.Group.format_commands() method to use those groups when constructing the help.
Test Code
import click
#click.group(cls=GroupedGroup)
def cli():
"""My awesome cli"""
#cli.command(group='Generators')
def some_generator():
"""This is Some Generator"""
#cli.command(group='Generators')
def another_generator():
"""This is Another Generator"""
#cli.command(group='Filters')
def some_filter():
"""This is Some Filter"""
#cli.command(group='Filters')
def another_filter():
"""This is Another Filter"""
cli()
Results
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My awesome cli
Options:
--help Show this message and exit.
Commands:
Filters:
another-filter This is Another Filter
some-filter This is Some Filter
Generators:
another-generator This is Another Generator
some-generator This is Some Generator
My Click 7.0 application has one group, having multiple commands, called by the main cli function like so:
import click
#click.group()
#click.pass_context
def cli(ctx):
"This is cli helptext"
click.echo('cli called')
click.echo('cli args: {0}'.format(ctx.args))
#cli.group(chain=True)
#click.option('-r', '--repeat', default=1, type=click.INT, help='repeat helptext')
#click.pass_context
def chainedgroup(ctx, repeat):
"This is chainedgroup helptext"
for _ in range(repeat):
click.echo('chainedgroup called')
click.echo('chainedgroup args: {0}'.format(ctx.args))
#chainedgroup.command()
#click.pass_context
def command1(ctx):
"This is command1 helptext"
print('command1 called')
print('command1 args: {0}'.format(ctx.args))
#chainedgroup.command()
#click.pass_context
def command2(ctx):
"This is command2 helptext"
print('command2 called')
print('command2 args: {0}'.format(ctx.args))
Run:
$ testcli --help
$ testcli chainedgroup --help
$ testcli chainedgroup command1 --help
The help-text displays as expected--except that the parent functions are inadvertently run in the process. A single conditional checking to see if '--help' is contained in ctx.args should be enough to solve this problem, but does anyone know how/when '--help' is passed? Because with this code, ctx.args is empty every time.
If argparse is not an option, how about:
if '--help' in sys.argv:
...
click stores the arguments passed to a command in a list. The method get_os_args() returns such list. You can check if --help is in that list to determine if the help flag was invoked. Something like the following:
if '--help' in click.get_os_args():
pass
It is prebuilt - Click looks like a decorator for argparse (Hurrah for common sense).
import click
#click.command()
#click.option('--count', default=1, help='Number of greetings.')
#click.option('--name', prompt='Your name',
help='The person to greet.')
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for x in range(count):
click.echo('Hello %s!' % name)
if __name__ == '__main__':
hello()
So you can write
python cl.py --name bob
And see
Hello bob!
Help is already done (as it is argparse)
python cl.py --help
Usage: cl.py [OPTIONS]
Simple program that greets NAME for a total of COUNT times.
Options:
--count INTEGER Number of greetings.
--name TEXT The person to greet.
--help Show this message and exit.
Been busy only just had time to read into this.
Sorry for the delay
Why not use argparse ? It has excellent for CLI parsing.
I am going to write something very basic so as to explain what I'm looking to make happen. I have written some code to do some interesting WordPress administration. The program will create instances but also create https settings for apache.
What I would like it to do and where I'm having a problem:
(If you run a help on the wp cli you will see exactly what I want to have happen...but I'm not a developer so I'd like some help)
python3 testcommands.py --help
Usage: testcommands.py [OPTIONS] COMMAND [ARGS]...
This is the help
Options:
--help Show this message and exit.
Commands:
https Commands for HTTPS
wp Commands for WP
python3 testcommands.py https --help
Usage: testcommands.py [OPTIONS] COMMAND [ARGS]...
This is the help
Options:
--help Show this message and exit.
Commands:
create Creates config for HTTPS
sync Syncs config for Apache
My basic code:
import click
#click.group()
def cli1():
pass
"""First Command"""
#cli1.command('wp')
def cmd1():
"""Commands for WP"""
pass
#cli1.command('install')
#click.option("--test", help="This is the test option")
def install():
"""Test Install for WP"""
print('Installing...')
#click.group()
def cli2():
pass
"""Second Command"""
#cli2.command('https')
def cmd2():
click.echo('Test 2 called')
wp = click.CommandCollection(sources=[cli1, cli2], help="This is the help")
if __name__ == '__main__':
wp()
Returns:
python3 testcommands.py --help
Usage: testcommands.py [OPTIONS] COMMAND [ARGS]...
This is the help
Options:
--help Show this message and exit.
Commands:
https
install Test Install for WP
wp Commands for WP
I can't figure this out. I don't want install to show here, as it should be underneath wp so as not to show.
Thank you if you can help me out...I'm sure it is simple...or maybe not possible...but thanks anyways.
I was able to figure it out once I found a site that was trying to do the same thing.
https://github.com/chancez/igor-cli
I couldn't figure out the name...but I was looking for a way to do HIERARCHICAL commands.
Here is the base code:
import click
#click.group()
#click.pass_context
def main(ctx):
"""Demo WP Control Help"""
#main.group()
#click.pass_context
def wp(ctx):
"""Commands for WP"""
#wp.command('install')
#click.pass_context
def wp_install(ctx):
"""Install WP instance"""
#wp.command('duplicate')
#click.pass_context
def wp_dup(ctx):
"""Duplicate WP instance"""
#main.group()
#click.pass_context
def https(ctx):
"""Commands for HTTPS"""
#https.command('create')
#click.pass_context
def https_create(ctx):
"""Create HTTPS configuration"""
#https.command('sync')
#click.pass_context
def https_sync(ctx):
"""Sync HTTPS configuration with Apache"""
if __name__ == '__main__':
main(obj={})
How to set the default option as -h for Python click?
By default, my script, shows nothing when no arguments is given to the duh.py:
import click
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.command(context_settings=CONTEXT_SETTINGS)
#click.option('--toduhornot', is_flag=True, help='prints "duh..."')
def duh(toduhornot):
if toduhornot:
click.echo('duh...')
if __name__ == '__main__':
duh()
[out]:
$ python3 test_click.py -h
Usage: test_click.py [OPTIONS]
Options:
--toduhornot prints "duh..."
-h, --help Show this message and exit.
$ python3 test_click.py --toduhornot
duh...
$ python3 test_click.py
Question:
As shown above, the default prints no information python3 test_click.py.
Is there a way such that, the default option is set to -h if no arguments is given, e.g.
$ python3 test_click.py
Usage: test_click.py [OPTIONS]
Options:
--toduhornot prints "duh..."
-h, --help Show this message and exit.
As of version 7.1, one can simply specify #click.command(no_args_is_help=True).
Your structure is not the recommended one, you should use:
import click
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.group(context_settings=CONTEXT_SETTINGS)
def cli():
pass
#cli.command(help='prints "duh..."')
def duh():
click.echo('duh...')
if __name__ == '__main__':
cli()
And then python test_click.py will print help message:
Usage: test_click.py [OPTIONS] COMMAND [ARGS]...
Options:
-h, --help Show this message and exit.
Commands:
duh prints "duh..."
So you can use python test_click.py duh to call duh.
Update
import click
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.command(context_settings=CONTEXT_SETTINGS)
#click.option('--toduhornot', is_flag=True, help='prints "duh..."')
def duh(toduhornot):
if toduhornot:
click.echo('duh...')
else:
with click.Context(duh) as ctx:
click.echo(ctx.get_help())
if __name__ == '__main__':
duh()
If you inherit from click.Command and override the parse_args() method, you can create a custom class to default to help like:
Custom Class
import click
class DefaultHelp(click.Command):
def __init__(self, *args, **kwargs):
context_settings = kwargs.setdefault('context_settings', {})
if 'help_option_names' not in context_settings:
context_settings['help_option_names'] = ['-h', '--help']
self.help_flag = context_settings['help_option_names'][0]
super(DefaultHelp, self).__init__(*args, **kwargs)
def parse_args(self, ctx, args):
if not args:
args = [self.help_flag]
return super(DefaultHelp, self).parse_args(ctx, args)
Using Custom Class:
To use the custom class, pass the cls parameter to #click.command() decorator like:
#click.command(cls=DefaultHelp)
How does this work?
This works because click is a well designed OO framework. The #click.command() decorator usually instantiates a
click.Command object but allows this behavior to be over ridden with the cls parameter. So it is a relatively
easy matter to inherit from click.Command in our own class and over ride the desired methods.
In this case we over-ride click.Command.parse_args() and check for an empty argument list. If it is empty then we invoke the help. In addition this class will default the help to ['-h', '--help'] if it is not otherwise set.
Test Code:
#click.command(cls=DefaultHelp)
#click.option('--toduhornot', is_flag=True, help='prints "duh..."')
def duh(toduhornot):
if toduhornot:
click.echo('duh...')
if __name__ == "__main__":
commands = (
'--toduhornot',
'',
'--help',
'-h',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + cmd)
time.sleep(0.1)
duh(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Results:
Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --toduhornot
duh...
-----------
>
Usage: test.py [OPTIONS]
Options:
--toduhornot prints "duh..."
-h, --help Show this message and exit.
-----------
> --help
Usage: test.py [OPTIONS]
Options:
--toduhornot prints "duh..."
-h, --help Show this message and exit.
-----------
> -h
Usage: test.py [OPTIONS]
Options:
--toduhornot prints "duh..."
-h, --help Show this message and exit.
Easiest approach I've found
import click
#click.command()
#click.option('--option')
#click.pass_context
def run(ctx, option):
if not option:
click.echo(ctx.get_help())
ctx.exit()
I'm trying to add help to the command line application using click library. As mentioned in official documentation,
For commands, a short help snippet is generated. By default, it’s the
first sentence of the help message of the command, unless it’s too
long. This can also be overridden
With simple #click.command everything works as expected:
import click
#click.command()
def cli():
"""This is sample description of script."""
if __name__ == '__main__':
cli()
Running this would display description for the script from the method's doscstring:
Usage: example.py [OPTIONS]
This is sample description of script.
Options:
--help Show this message and exit.
But I need to use CommandCollection, as I'm creating a script consisting from multiple commands. Here is an example from official help:
import click
#click.group()
def cli1():
pass
#cli1.command()
def cmd1():
"""Command on cli1"""
#click.group()
def cli2():
pass
#cli2.command()
def cmd2():
"""Command on cli2"""
cli = click.CommandCollection(sources=[cli1, cli2])
if __name__ == '__main__':
cli()
And I don't know how to add description to whole command collection. What I've tried so far:
provide help with additional short_help parameter
set __doc__ argument for cli parameter, after creating CommandCollection
add docstring to cli1 method, decorated with #click.group
Any help is much appreciated.
Just use help parameter:
cli = click.CommandCollection(sources=[cli1, cli2], help="This would be your description, dude!")