pythonpython-click

Optional argument in command with click


I am trying to accomplish something not very standard for CLI parsing with Click and it only works partially:

Sample:

import click

@click.group()
@click.argument('hostname', required=False)
@click.pass_context
def cli(ctx, hostname=None):
    """"""
    ctx.obj = hostname
    click.echo("cli: hostname={}".format(hostname))

@cli.command()
@click.pass_obj
def check(hostname):
    click.echo("check: hostname={}".format(hostname))

@cli.command()
@click.pass_obj
def show(hostname):
    click.echo("check: hostname={}".format(hostname))

if __name__ == '__main__':
    cli()

The part WITH the hostname works:

> pipenv run python cli.py  localhost check
cli: hostname=localhost
check: hostname=localhost
> pipenv run python cli.py  localhost show
cli: hostname=localhost
check: hostname=localhost

But the part WITHOUT the hostname DOES NOT:

> pipenv run python cli.py show
Usage: cli.py [OPTIONS] [HOSTNAME] COMMAND [ARGS]...

Error: Missing command.

Anybody has an idea about the direction I should start looking into?


Solution

  • This can be done by over riding the click.Group argument parser like:

    Custom Class:

    class MyGroup(click.Group):
        def parse_args(self, ctx, args):
            if args[0] in self.commands:
                if len(args) == 1 or args[1] not in self.commands:
                    args.insert(0, '')
            super(MyGroup, self).parse_args(ctx, args)
    

    Using Custom Class:

    Then to use the custom group, pass it as the cls argument to the group decorator like:

    @click.group(cls=MyGroup)
    @click.argument('hostname', required=False)
    @click.pass_context
    def cli(ctx, hostname=None):
        ....
    

    How?

    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 over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and over ride desired methods.

    In this case we over ride click.Group.parse_args() and if the first parameter matches a command and the second parameter does not, then we insert an empty string as the first parameter. This puts everything back where the parser expects it to be.