Optional Argument in Command with 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:

  • 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?

2

1 Answer

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.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Alexander Ross
Author

Alexander Ross

Alexander Ross has covered the video game industry for a decade, writing deep dives on game design, esports tournaments, VR developments, and gaming culture.