Optional argument in command with click
Asked Answered
P

1

12

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?

Perspiratory answered 18/5, 2017 at 19:17 Comment(2)
Interesting approach, but this kind of violates Click's approach. An optional positional argument followed by a command cannot be unambiguously parsed (e.g. imagine someone named their host show or check). It would be better to use an option insteadAx
now click support required=False without Using Custom ClassObsolescent
M
19

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.

Mistake answered 18/5, 2017 at 19:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.