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
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? Commented Mar 23, 2020 at 16:48
  • 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='*'). Commented Mar 23, 2020 at 17:33

2 Answers 2

5
+250

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.

Sign up to request clarification or add additional context in comments.

3 Comments

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.
Glad I could help!
See Renaud's answer below for official Click support for this usecase.
5

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!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.