Search code examples
pythonpython-click

Command line interface with multiple commands using click: add unspecified options for command as dictionary


I have a command line interface build with click which implements multiple commands.

Now I want to pass unspecified named options into one command which is here named command1 e.g. the number of options and their names should be able to vary flexibly.

import click


@click.group(chain=True)
@click.pass_context
def cli(ctx, **kwargs):
    return True


@cli.command()
@click.option('--command1-option1', type=str)
@click.option('--command1-option2', type=str)
@click.pass_context
def command1(ctx, **kwargs):
    """Add command1."""
    ctx.obj['command1_args'] = {}
    for k, v in kwargs.items():
        ctx.obj['command1_args'][k] = v
    return True


@cli.command()
@click.argument('command2-argument1', type=str)
@click.pass_context
def command2(ctx, **kwargs):
    """Add command2."""
    print(ctx.obj)
    print(kwargs)
    return True


if __name__ == '__main__':
    cli(obj={})

I have already looked into forwarding unknown options like in this question but the problem is that I have to call (chanin) other commands after the first one which have to be asserted e.g. this call has to work but with arbitrary options for command1:

$python cli.py command1 --command1-option1 foo --command1-option2 bar command2 'hello'

So how can I add unspecified named options to a single command and call (chain) another one at the same time (after it)?


Solution

  • The custom class found here, can be adapted to your case.

    Using the Custom Class:

    To use the custom class, just use the cls parameter to the click.command() decorator like:

    @cli.command(cls=AcceptAllCommand)
    @click.pass_context
    def command1(ctx, **kwargs):
        """Add command1."""
        ...
    

    Test Code:

    import click
    
    class AcceptAllCommand(click.Command):
    
        def make_parser(self, ctx):
            """Hook 'make_parser' and allow the opt dict to find any option"""
            parser = super(AcceptAllCommand, self).make_parser(ctx)
            command = self
    
            class AcceptAllDict(dict):
    
                def __contains__(self, item):
                    """If the parser does no know this option, add it"""
    
                    if not super(AcceptAllDict, self).__contains__(item):
                        # create an option name
                        name = item.lstrip('-')
    
                        # add the option to our command
                        click.option(item)(command)
    
                        # get the option instance from the command
                        option = command.params[-1]
    
                        # add the option instance to the parser
                        parser.add_option(
                            [item], name.replace('-', '_'), obj=option)
                    return True
    
            # set the parser options to our dict
            parser._short_opt = AcceptAllDict(parser._short_opt)
            parser._long_opt = AcceptAllDict(parser._long_opt)
    
            return parser
    
    
    @click.group(chain=True)
    @click.pass_context
    def cli(ctx, **kwargs):
        """"""
    
    
    @cli.command(cls=AcceptAllCommand)
    @click.pass_context
    def command1(ctx, **kwargs):
        """Add command1."""
        ctx.obj['command1_args'] = {}
        for k, v in kwargs.items():
            ctx.obj['command1_args'][k] = v
    
    
    @cli.command()
    @click.argument('command2-argument1', type=str)
    @click.pass_context
    def command2(ctx, **kwargs):
        """Add command2."""
        print(ctx.obj)
        print(kwargs)
    
    
    if __name__ == "__main__":
        commands = (
            "command1 --cmd1-opt1 foo --cmd1-opt2 bar command2 hello",
            '--help',
        )
    
        import sys, time
    
        time.sleep(1)
        print('Click Version: {}'.format(click.__version__))
        print('Python Version: {}'.format(sys.version))
        for cmd in commands:
            try:
                time.sleep(0.1)
                print('-----------')
                print('> ' + cmd)
                time.sleep(0.1)
                cli(cmd.split(), obj={})
    
            except BaseException as exc:
                if str(exc) != '0' and \
                        not isinstance(exc, (click.ClickException, SystemExit)):
                    raise
    

    Results:

    Click Version: 6.7
    Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
    -----------
    > command1 --cmd1-opt1 foo --cmd1-opt2 bar command2 hello
    {'command1_args': {'cmd1_opt1': 'foo', 'cmd1_opt2': 'bar'}}
    {'command2_argument1': 'hello'}
    -----------
    > --help
    Usage: test.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      command1  Add command1.
      command2  Add command2.