Shared options and flags between commands
Question:
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
Answers:
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.
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()
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):
...
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)
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()
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
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.
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()
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):
...
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)
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()