Skip to content Skip to sidebar Skip to footer

Python Multi-command Cli With Common Options

I am adding CLI for my Python application. The CLI should allow to run multiple commands in a time. The commands should have common options and personal options. Example: $ python

Solution 1:

Click absolutely supports this sort of syntax. A simple example looks something like:

import click


@click.group(chain=True)@click.option('--common-option1')@click.option('--common-option2')defmain(common_option1, common_option2):
    pass@main.command()@click.option('--cmd1-option', is_flag=True)defcmd1(cmd1_option):
    pass@main.command()@click.option('--cmd2-option')defcmd2(cmd2_option):
    pass@main.command()defcmd3():
    passif __name__ == '__main__':
    main()

Assuming the above is in mycliapp.py, we see the common help output:

$ python example.py --help
Usage: example.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...

Options:
  --common-option1 TEXT--common-option2 TEXT--help                 Show this message and exit.

Commands:
  cmd1
  cmd2
  cmd3

And for cmd1:

$ python mycliapp.py cmd1 --help
Usage: mycliapp.py cmd1 [OPTIONS]

Options:
  --cmd1-option--help         Show this message and exit.

And for cmd2:

$ python mycliapp.py cmd2 --help
Usage: mycliapp.py cmd2 [OPTIONS]

Options:
  --cmd2-option TEXT--help              Show this message and exit.

Etc.

With this we can run the command line from your question:

python mycliapp.py--common-option1 value1 --common-option2 value2 \
  cmd1 --cmd1-option \
  cmd2 --cmd2-option somevalue \
  cmd3

Update 1

Here's an example that implements pipelines using the callback model suggested in the documentation:

import click


@click.group(chain=True)@click.option('--common-option1')@click.option('--common-option2')@click.pass_contextdefmain(ctx, common_option1, common_option2):
    ctx.obj = {
        'common_option1': common_option1,
        'common_option2': common_option2,
    }


@main.resultcallback()defprocess_pipeline(processors, common_option1, common_option2):
    print('common_option1 is', common_option1)
    for func in processors:
        res = func()
        ifnot res:
            raise click.ClickException('Failed processing!')


@main.command()@click.option('--cmd1-option', is_flag=True)defcmd1(cmd1_option):
    defprocess():
        print('This is cmd1')
        return cmd1_option

    return process


@main.command()@click.option('--cmd2-option')defcmd2(cmd2_option):
    defprocess():
        print('This is cmd2')
        return cmd2_option != 'fail'return process


@main.command()@click.pass_contextdefcmd3(ctx):
    defprocess():
        print('This is cmd3 (common option 1 is: {common_option1}'.format(**ctx.obj))
        returnTruereturn process


if __name__ == '__main__':
    main()

Each command returns a boolean indicating whether or not it was successful. A failed command will abort pipeline processing. For example, here cmd1 fails so cmd2 never executes:

$ python mycliapp.py cmd1 cmd2
This is cmd1
Error: Failed processing!

But if we make cmd1 happy, it works:

$ python mycliapp.py cmd1 --cmd1-option cmd2
This is cmd1
This is cmd2

And similarly, compare this:

$ python mycliapp.py cmd1 --cmd1-option cmd2 --cmd2-option fail cmd3
This is cmd1
This is cmd2
Error: Failed processing!

With this:

$ python mycliapp.py cmd1 --cmd1-option cmd2  cmd3
This is cmd1
This is cmd2
This is cmd3

And of course you don't need to call things in order:

$ python mycliapp.py cmd2 cmd1 --cmd1-option
This is cmd2
This is cmd1

Solution 2:

You can do it without main command using argparse.

# maincmd just to tie between arguments and subparsers 
parser = argparse.ArgumentParser(prog='maincmd')
parser.add_argument('--common-option1', type=str, required=False)
parser.add_argument('--common-option2', type=str, required=False)

main_subparsers = parser.add_subparsers(title='sub_main',  dest='sub_cmd')
parser_cmd1 = main_subparsers.add_parser('cmd1', help='help cmd1')
parser_cmd1.add_argument('--cmd1-option', type=str, required=False)

cmd1_subparsers = parser_cmd1.add_subparsers(title='sub_cmd1', dest='sub_cmd1')
parser_cmd2 = cmd1_subparsers.add_parser('cmd2', help='help cmd2')

options = parser.parse_args(sys.argv[1:])
print(vars(options))

Let's check:

python test.py --common-option1 value1 --common-option2 value2
#{'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': None}

python test.py --common-option1 value1 --common-option2 value2 cmd1
# {'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': 'cmd1', 'cmd1_option': None, 'sub_cmd1': None}

python test.py --common-option1 value1 --common-option2 value2 cmd1 --cmd1-option cmd1-val
# {'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': 'cmd1', 'cmd1_option': 'cmd1-val', 'sub_cmd1': None}

python test.py --common-option1 value1 --common-option2 value2 cmd1 --cmd1-option cmd1-val cmd2
# {'common_option1': 'value1', 'common_option2': 'value2', 'sub_cmd': 'cmd1', 'cmd1_option': 'cmd1-val', 'sub_cmd1': 'cmd2'}

JFYI. I worked with Click and argparse. argparse seemed to me more extensible and functional.

Hope this helps.

Post a Comment for "Python Multi-command Cli With Common Options"