Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • main CLI has multiple sub-commands (in sample below 'show' and 'check')
  • both those commands might have optional argument, but the argument is preceding them not following
  • I decided to handle that argument in the group "above" it and pass the value in the context

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?

like image 452
Sergey Avatar asked May 18 '17 19:05

Sergey


1 Answers

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.

like image 143
Stephen Rauch Avatar answered Oct 13 '22 02:10

Stephen Rauch