Optional CLI arguments with python Click library option
Asked Answered
V

2

10

I'm having a conundrum with the Python Click library when parsing some CLI options.

I would like an option to act as a flag by itself, but optionally accept string values. E.g.:

  1. $ myscript ⇒ option = False

  2. $ myscript -o ⇒ option = True

  3. $ myscript -o foobar ⇒ option = Foobar

Additionally, I would like the option to be "eager" (e.g. in "Click" terms abort execution after a callback), but this can be ignored for now.


When I define my arguments like this:

@click.command()
@click...
@click.option("-o", "option", is_flag=True, default=False)
def myscript(..., option):

I achieve example 1 and 2, but 3 is naturally impossible because the flag detects present/not present only.


When I define my arguments like this:

@click.command()
@click...
@click.option("-o", "--option", default="") # Let's assume I will cast empty string to False
def myscript(..., option):

I achieve 1 and 3, but 2 will fail with an Error: -c option requires an argument.


This does not seems like an out-of-this world scenario, but I can't seem to be able to achieve this or find examples that behave like this.

How can I define an @click.option that gets parsed like:

  • False when not set
  • True when set but without value
  • str when set with value
Veiled answered 18/3, 2020 at 14:50 Comment(2)
unless your CLI only accepts -o, I'm not sure you can achieve your request. For example if your CLI is myscript -o REQUIRED_PARAM, how would you know if your were providing -o OPTION_VALUE or the REQUIRED_PARAM value?Obidiah
Unfortunately not, as the docs state: For options, only a fixed number of arguments is supported. So you can't satisfy both (2) and (3) at the same time. Python's argparse on the other hand would support your requirements via nargs='*': parser.add_argument('-o', nargs='*').Kirk
E
5

One way that I have managed to achieve this behaviour was by actually using arguments as below. I'll post this as a workaround, while I try to see if it could be done with an option, and I'll update my post accordingly

@click.command(context_settings={"ignore_unknown_options": True})
@click.argument("options", nargs=-1)
def myscript(options):
    option = False
    if options is ():
        option = False
    if '-o' in options or '--option' in options:
        option = True
    if len(options) > 1:
        option = options[1]
    print(option)

Later Edit Using an option, I have managed to achieve this by adding an argument to the command definition.

@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', nargs=-1)
def myscript(option, value):
    if option and value != ():
        option = value[0]
    print(option)

The nargs can be removed if you only expect at most one argument to follow, and can be treated as not required.

@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', required=False)
def myscript(option, value=None):
    if option and value is not None:
        option = value
    print(option)

It might also be possible by putting together a context generator and storing some state, but that seems the least desirable solution, since you would be relying on the context storing your state.

Eyeleteer answered 23/3, 2020 at 23:34 Comment(3)
Super, thanks! I didn't realize you could add an argument last and after options; combined with the is_eager that is no problem, and I can simply check if the var is set or not in the decision tree.Veiled
Glad I could help!Eyeleteer
See Renaud's answer below for official Click support for this usecase.Subjoinder
H
3

FYI, if anybody bumps into this problem in 2024, it seems that click has made something specific for this use case, see: https://click.palletsprojects.com/en/8.1.x/options/#optional-value

Setting is_flag=False, flag_value=value tells Click that the option can still be passed a value, but if only the flag is given the flag_value is used.

@click.command()
@click.option("--name", is_flag=False, flag_value="Flag", default="Default")
def hello(name):
    click.echo(f"Hello, {name}!")

>> hello
Hello, Default!
>> hello --name Value
Hello, Value!
>> hello --name
Hello, Flag!
Hellbox answered 26/3 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.