Shared options and flags between commands
Asked Answered
W

6

40

Say my CLI utility has three commands: cmd1, cmd2, cmd3

And I want cmd3 to have same options and flags as cmd1 and cmd2. Like some sort of inheritance.

@click.command()
@click.options("--verbose")
def cmd1():
    pass

@click.command()
@click.options("--directory")
def cmd2():
    pass

@click.command()
@click.inherit(cmd1, cmd2) # HYPOTHETICAL
def cmd3():
    pass

So cmd3 will have flag --verbose and option --directory. Is it possible to make this with Click? Maybe I just have overlooked something in the documentation...

EDIT: I know that I can do this with click.group(). But then all the group's options must be specified before group's command. I want to have all the options normally after command.

cli.py --verbose --directory /tmp cmd3 -> cli.py cmd3 --verbose --directory /tmp

Wertheimer answered 21/10, 2016 at 17:13 Comment(0)
W
45

I have found a simple solution! I slightly edited the snippet from https://github.com/pallets/click/issues/108 :

import click


_cmd1_options = [
    click.option('--cmd1-opt')
]

_cmd2_options = [
    click.option('--cmd2-opt')
]


def add_options(options):
    def _add_options(func):
        for option in reversed(options):
            func = option(func)
        return func
    return _add_options


@click.group()
def group(**kwargs):
    pass


@group.command()
@add_options(_cmd1_options)
def cmd1(**kwargs):
    print(kwargs)


@group.command()
@add_options(_cmd2_options)
def cmd2(**kwargs):
    print(kwargs)


@group.command()
@add_options(_cmd1_options)
@add_options(_cmd2_options)
@click.option("--cmd3-opt")
def cmd3(**kwargs):
    print(kwargs)


if __name__ == '__main__':
    group()
Wertheimer answered 22/10, 2016 at 18:49 Comment(1)
Kickass solution, thanks :)Ardoin
M
37

Define a class with common parameters

class StdCommand(click.core.Command):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.params.insert(0, click.core.Option(('--default-option',), help='Every command should have one'))

Then pass the class to decorator when defining the command function

@click.command(cls=StdCommand)
@click.option('--other')
def main(default_option, other):
  ...
Mats answered 20/12, 2018 at 20:24 Comment(2)
This the cleanest and most elegant solution. Works without a hitch. I suggests replacing click.core.Command with click.Command. Don't really need to go to the lower module. Also, it is cleaner to say self.params = [...] instead of inserting to an empty list.Nikolaos
@Nikolaos First comment is absolutely correct, but the second - isn't. The list is not necessarily empty, you overwrite what's in it with the proposed approach. When you add more options (the --other), they're gone.Densitometer
A
14

You could also have another decorator for shared options. I found this solution here

def common_params(func):
    @click.option('--foo')
    @click.option('--bar')
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


@click.command()
@common_params
@click.option('--baz')
def cli(foo, bar, baz):
    print(foo, bar, baz)
Appointment answered 7/9, 2021 at 4:48 Comment(0)
S
5

This code extracts all the options from it's arguments

def extract_params(*args):
    from click import Command
    if len(args) == 0:
        return ['']
    if any([ not isinstance(a, Command) for a in args ]):
        raise TypeError('Handles only Command instances')

    params = [ p.opts() for cmd_inst in args for p in cmd_inst.params ]
    return list(set(params))

now you can use it:

@click.command()
@click.option(extract_params(cmd1, cmd2))
def cmd3():
    pass

This code extracts only the parameters and none of their default values, you can improve it if needed.

Swaraj answered 21/10, 2016 at 18:22 Comment(0)
U
2

Options in click are decorator factories, and can be reused like so:

import sys
import click


verbose = click.option("--verbose")
directory = click.option("--directory")


@click.group()
@click.pass_context
def cli(ctx):
    pass


@cli.command()
@verbose
def cmd1(verbose):
    print(f'Running cmd1: {verbose = }')


@cli.command()
@directory
def cmd2(directory):
    print(f'Running cmd2: {directory = }')


@cli.command()
@verbose
@directory
@click.option('--baz')
def cmd3(verbose, directory, baz):
    print(f'Running cmd3: {verbose = }, {directory = }, {baz = }')


if __name__ == '__main__':
    sys.exit(cli())

Having saved this file as app.py, you can test with:

> python app.py cmd1 --verbose=1
# Running cmd1: verbose = '1'
> python app.py cmd2 --directory='.'
# Running cmd2: directory = '.'
> python app.py cmd3 --verbose=1 --directory='.'
# Running cmd3: verbose = '1', directory = '.', baz = None

However,

python app.py cmd2 --verbose=1
Usage: app.py cmd2 [OPTIONS]
Try 'app.py cmd2 --help' for help.

Error: No such option: --verbose

Don't forget that using @ for decorators in python is just a syntactic shortcut for an explicit call to the decorator, passing the function as the first parameter and returning another function. You can just assign that function to a variable, and click allows you to reuse them as shown above.

Unnerve answered 29/12, 2023 at 14:20 Comment(0)
E
1

A slight improvement on @jirinovo solution. this version support an unlimited number of click options. one thing that is worth mentioning, the order you pass the options is important

import click

_global_options = [click.option('--foo', '-f')]
_local_options = [click.option('--bar', '-b', required=True)]
_local_options2 = [click.option('--foofoo', required=True)]


def add_options(*args):
    def _add_options(func):
        options = [x for n in args for x in n]
        for option in reversed(options):
            func = option(func)
        return func

    return _add_options


@click.group()
def cli():
    pass


@cli.group()
def subcommand():
    pass


@subcommand.command()
@add_options(_global_options, _local_options)
def echo(foo, bar):
    print(foo, bar, sep='\n')


@subcommand.command()
@add_options(_global_options)
def echo2(foo):
    print(foo)


@subcommand.command()
@add_options(_global_options, _local_options2)
def echo3(foo, foofoo):
    print(foo, foofoo, sep='\n')


@subcommand.command()
@add_options(_global_options, _local_options, _local_options2)
def echo4(foo, bar, foofoo):
    print(foo, bar, foofoo, sep='\n')


if __name__ == '__main__':
    cli()


Erbes answered 18/2, 2023 at 23:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.