Divide click commands into sections in cli documentation - python

This code:
#!/usr/bin env python3
import click
def f(*a, **kw):
print(a, kw)
commands = [click.Command("cmd1", callback=f), click.Command("cmd2", callback=f)]
cli = click.Group(commands={c.name: c for c in commands})
if __name__ == "__main__":
cli()
generates this help:
# Usage: cli.py [OPTIONS] COMMAND [ARGS]...
# Options:
# --help Show this message and exit.
# Commands:
# cmd1
# cmd2
I have a lot of subcommands, so I want to divide them into sections in the help like this:
# Usage: cli.py [OPTIONS] COMMAND [ARGS]...
# Options:
# --help Show this message and exit.
# Commands:
# cmd1
# cmd2
#
# Extra other commands:
# cmd3
# cmd4
How can I split the commands into sections in the help like that, without affecting the functionality?

If you define your own group class you can overide the help generation like:
Custom Class:
class SectionedHelpGroup(click.Group):
"""Sections commands into help groups"""
def __init__(self, *args, **kwargs):
self.grouped_commands = kwargs.pop('grouped_commands', {})
commands = {}
for group, command_list in self.grouped_commands.items():
for cmd in command_list:
cmd.help_group = group
commands[cmd.name] = cmd
super(SectionedHelpGroup, self).__init__(
*args, commands=commands, **kwargs)
def command(self, *args, **kwargs):
help_group = kwargs.pop('help_group')
decorator = super(SectionedHelpGroup, self).command(*args, **kwargs)
def new_decorator(f):
cmd = decorator(f)
cmd.help_group = help_group
self.grouped_commands.setdefault(help_group, []).append(cmd)
return cmd
return new_decorator
def format_commands(self, ctx, formatter):
for group, cmds in self.grouped_commands.items():
rows = []
for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand)
if cmd is None or cmd.help_group != group:
continue
rows.append((subcommand, cmd.short_help or ''))
if rows:
with formatter.section(group):
formatter.write_dl(rows)
Using the Custom Class:
Pass the Custom Class to click.group() using cls parameter like:
#click.group(cls=SectionedHelpGroup)
def cli():
""""""
when defining commands, pass the help group the command belongs to like:
#cli.command(help_group='my help group')
def a_command(*args, **kwargs):
....
How does this work?
This works because click is a well designed OO framework. The #click.group() decorator usually instantiates a click.Group object but allows this behavior to be over-ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and over ride the desired methods.
In this case, we hook the command() decorator to allow the help_group to be identified. We also override the format_commands() method to print the commands help into the groups.
Test Code:
import click
def f(*args, **kwargs):
click.echo(args, kwargs)
commands = {
'help group 1': [
click.Command("cmd1", callback=f),
click.Command("cmd2", callback=f)
],
'help group 2': [
click.Command("cmd3", callback=f),
click.Command("cmd4", callback=f)
]
}
cli = SectionedHelpGroup(grouped_commands=commands)
#cli.command(help_group='help group 3')
def a_command(*args, **kwargs):
"""My command"""
click.echo(args, kwargs)
if __name__ == "__main__":
cli(['--help'])
Results:
Usage: test.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
help group 1:
cmd1
cmd2
help group 2:
cmd3
cmd4
help group 3:
a_command My command

Stephen's answer above gives the general principle. If you only add new commands to a group using add_command, it can be simplified slightly:
import click
import collections
class SectionedHelpGroup(click.Group):
"""Organize commands as sections"""
def __init__(self, *args, **kwargs):
self.section_commands = collections.defaultdict(list)
super().__init__(*args, **kwargs)
def add_command(self, cmd, name=None, section=None):
self.section_commands[section].append(cmd)
super().add_command(cmd, name=name)
def format_commands(self, ctx, formatter):
for group, cmds in self.section_commands.items():
with formatter.section(group):
formatter.write_dl(
[(cmd.name, cmd.get_short_help_str() or "") for cmd in cmds]
)
Example
def f(*args, **kwargs):
click.echo(args, kwargs)
commands = {
'help group 1': [
click.Command("cmd1", callback=f),
click.Command("cmd2", callback=f)
],
'help group 2': [
click.Command("cmd3", callback=f),
click.Command("cmd4", callback=f)
]
}
#click.group(
help=f"Sectioned Commands CLI",
cls=SectionedHelpGroup
)
def cli():
pass
for (section, cmds) in commands.items():
for cmd in cmds:
cli.add_command(cmd, section=section)
if __name__ == "__main__":
cli()

Related

Python click different named arguments based on number of arguments

How can I achieve the following synopsis using the Python click library?
Usage: app CMD [OPTIONS] [FOO] [BAR]
app CMD [OPTIONS] [FOOBAR]
I can't figure out whether I am able to pass two different sets of named argument for the same command based on the number of given arguments. That is, if only one argument was passed it's foobar, but if two arguments were passed, they are foo and bar.
The code representation of such implementation would look something like this (provided you could use function overload, which you can't)
#click.command()
#click.argument('foo', required=False)
#click.argument('bar', required=False)
def cmd(foo, bar):
# ...
#click.command()
#click.argument('foobar', required=False)
def cmd(foobar):
# ...
You can add multiple command handlers with a different number of arguments for each by creating a custom click.Command class. There is some ambiguity around which of the command handlers would best be called if parameters are not strictly required, but that can be mostly dealt with by using the first signature that fits the command line passed.
Custom Class
class AlternateArgListCmd(click.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.alternate_arglist_handlers = [(self, super())]
self.alternate_self = self
def alternate_arglist(self, *args, **kwargs):
from click.decorators import command as cmd_decorator
def decorator(f):
command = cmd_decorator(*args, **kwargs)(f)
self.alternate_arglist_handlers.append((command, command))
# verify we have no options defined and then copy options from base command
options = [o for o in command.params if isinstance(o, click.Option)]
if options:
raise click.ClickException(
f'Options not allowed on {type(self).__name__}: {[o.name for o in options]}')
command.params.extend(o for o in self.params if isinstance(o, click.Option))
return command
return decorator
def make_context(self, info_name, args, parent=None, **extra):
"""Attempt to build a context for each variant, use the first that succeeds"""
orig_args = list(args)
for handler, handler_super in self.alternate_arglist_handlers:
args[:] = list(orig_args)
self.alternate_self = handler
try:
return handler_super.make_context(info_name, args, parent, **extra)
except click.UsageError:
pass
except:
raise
# if all alternates fail, return the error message for the first command defined
args[:] = orig_args
return super().make_context(info_name, args, parent, **extra)
def invoke(self, ctx):
"""Use the callback for the appropriate variant"""
if self.alternate_self.callback is not None:
return ctx.invoke(self.alternate_self.callback, **ctx.params)
return super().invoke(ctx)
def format_usage(self, ctx, formatter):
"""Build a Usage for each variant"""
prefix = "Usage: "
for _, handler_super in self.alternate_arglist_handlers:
pieces = handler_super.collect_usage_pieces(ctx)
formatter.write_usage(ctx.command_path, " ".join(pieces), prefix=prefix)
prefix = " " * len(prefix)
Using the Custom Class:
To use the custom class, pass it as the cls argument to the click.command decorator like:
#click.command(cls=AlternateArgListCmd)
#click.argument('foo')
#click.argument('bar')
def cli(foo, bar):
...
Then use the alternate_arglist() decorator on the command to add another
command handler with different arguments.
#cli.alternate_arglist()
#click.argument('foobar')
def cli_one_param(foobar):
...
How does this work?
This works because click is a well designed OO framework. The #click.command() decorator
usually instantiates a click.Command object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Command in our own class and over ride the desired methods.
In this case we add a new decorator method: alternate_arglist(), and override three methods: make_context(), invoke() & format_usage(). The overridden make_context() method checks to see which of the command handler variants matches the number of args passed, the overridden invoke() method is used to call the appropriate command handler variant and the overridden format_usage() is used to create the help message showing the various usages.
Test Code:
import click
#click.command(cls=AlternateArgListCmd)
#click.argument('foo')
#click.argument('bar')
#click.argument('baz')
#click.argument('bing', required=False)
#click.option('--an-option', default='empty')
def cli(foo, bar, baz, bing, an_option):
"""Best Command Ever!"""
if bing is not None:
click.echo(f'foo bar baz bing an-option: {foo} {bar} {baz} {bing} {an_option}')
else:
click.echo(f'foo bar baz an-option: {foo} {bar} {baz} {an_option}')
#cli.alternate_arglist()
#click.argument('foo')
#click.argument('bar')
def cli_two_param(foo, bar, an_option):
click.echo(f'foo bar an-option: {foo} {bar} {an_option}')
#cli.alternate_arglist()
#click.argument('foobar', required=False)
def cli_one_param(foobar, an_option):
click.echo(f'foobar an-option: {foobar} {an_option}')
if __name__ == "__main__":
commands = (
'',
'p1',
'p1 p2 --an-option=optional',
'p1 p2 p3',
'p1 p2 p3 p4 --an-option=optional',
'p1 p2 p3 p4 p5',
'--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())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Test Results:
Click Version: 7.1.2
Python Version: 3.8.5 (tags/v3.8.5:580fbb0, Jul 20 2020, 15:57:54) [MSC v.1924 64 bit (AMD64)]
-----------
>
foobar an-option: None empty
-----------
> p1
foobar an-option: p1 empty
-----------
> p1 p2 --an-option=optional
foo bar an-option: p1 p2 optional
-----------
> p1 p2 p3
foo bar baz an-option: p1 p2 p3 empty
-----------
> p1 p2 p3 p4 --an-option=optional
foo bar baz bing an-option: p1 p2 p3 p4 optional
-----------
> p1 p2 p3 p4 p5
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
test_code.py [OPTIONS] FOO BAR
test_code.py [OPTIONS] [FOOBAR]
Try 'test_code.py --help' for help.
Error: Got unexpected extra argument (p5)
-----------
> --help
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
test_code.py [OPTIONS] FOO BAR
test_code.py [OPTIONS] [FOOBAR]
Best Command Ever!
Options:
--an-option TEXT
--help Show this message and exit.

Is there a way to control the order the arguments are evaluated in Python argparse

Let's take the example below. The parser contains two arguments --inputfile and verbosity. The Set_verbosity_level() function is used to controls the value of a module-level/global variable (in my real life a package-level variable) to 0-4. The CheckFile() function performs tests inside input file (in the real life depending on type).
I would like to print messages in CheckFile() depending on verbosity. The problem is that argparse calls CheckFile() before Set_verbosity_level() so the verbosity level is always 0/default in CheckFile...
So my question is whether there is any solution to force argparse to evaluate some arguments before others...
import argparse
VERBOSITY = 0
def Set_verbosity_level():
"""Set the verbosity level.
"""
def type_func(value):
a_value = int(value)
globals()['VERBOSITY'] = value
print("Verbosity inside Set_verbosity_level(): " + str(globals()['VERBOSITY']))
return value
return type_func
class CheckFile(argparse.FileType):
"""
Check whatever in the file
"""
def __init__(self, mode='r', **kwargs):
super(CheckFile, self).__init__(mode, **kwargs)
def __call__(self, string):
# Do whatever processing/checking/transformation
# e.g print some message according to verbosity
print("Verbosity inside CheckFile(): " + str(globals()['VERBOSITY']))
return super(CheckFile, self).__call__(string)
def make_parser():
"""The main argument parser."""
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument("-V",
"--verbosity",
default=0,
type=Set_verbosity_level(),
help="Increase output verbosity.",
required=False)
parser.add_argument('-i', '--inputfile',
help="Input file",
type=CheckFile(mode='r'),
required=True)
return parser
if __name__ == '__main__':
myparser = make_parser()
args = myparser.parse_args()
print("Verbosity in Main: " + str(VERBOSITY))
Calling this script gives:
$python test.py -i test.bed -V 2
Verbosity inside CheckFile(): 0
Verbosity inside Set_verbosity_level(): 2
Verbosity in Main: 2
argparse processes the command-line arguments in the order that they are listed, so if you simply swap the order of the given options, it would output in the verbosity you want:
python test.py -V 2 -i test.bed
This outputs:
Verbosity inside Set_verbosity_level(): 2
Verbosity inside CheckFile(): 2
There's no way otherwise to tell argparse to process the command-line arguments in a different order than how they're listed.
I don't know that you can force an argparse variable to be read first, but you can use pythons built in command line parser in your main function:
import sys
# Your classes here #
if __name__ == '__main__':
verbosity = 0
for i, sysarg in enumerate(sys.argv):
if str(sysarg).strip().lower().replace('-','') in ['v', 'verbose']:
try:
verbosity = sys.argv[i + 1]
except IndexError:
print("No verbosity level specified")
# more code
Its not very elegant and it's not argparse, but it's one way to ensure you get the verbosity first.
You could also update your CheckFile class to include a verbosity checking function:
class CheckFile(argparse.FileType):
"""
Check whatever in the file
"""
def __init__(self, mode='r', **kwargs):
super(CheckFile, self).__init__(mode, **kwargs)
def _check_verbosity(self):
verbosity = 0
for i, sysarg in enumerate(sys.argv):
if str(sysarg).strip().lower().replace('-','') in ['v', 'verbose']:
try:
verbosity = sys.argv[i + 1]
except IndexError:
print("No verbosity level specified")
return verbosity
def __call__(self, string):
# Do whatever processing/checking/transformation
# e.g print some message according to verbosity
print("Verbosity inside CheckFile(): {}".format(self._check_verbosity()))
return super(CheckFile, self).__call__(string)
Again, I know it's not really an answer to your argparse question, but it is a solution for your problem

Adding common parameters to groups with Click

I am trying to use the Python library Click, but struggle to get an example working. I defined two groups, one of which (group2) is meant to handle common parameters for this group of commands. What I want to achieve is that those common parameters get processed by the group function (group2) and assigned to the context variable, so they can be used by the actual commands.
A use case would be a number of commands that require username and password, while some others don't (not even optionally).
This is the code
import click
#click.group()
#click.pass_context
def group1(ctx):
pass
#click.group()
#click.option('--optparam', default=None, type=str)
#click.option('--optparam2', default=None, type=str)
#click.pass_context
def group2(ctx, optparam):
print 'in group2', optparam
ctx['foo'] = create_foo_by_processing_params(optparam, optparam2)
#group2.command()
#click.pass_context
def command2a(ctx):
print 'command2a', ctx['foo']
#group2.command()
#click.option('--another-param', default=None, type=str)
#click.pass_context
def command2b(ctx, another_param):
print 'command2b', ctx['foo'], another_param
# many more more commands here...
# #group2.command()
# def command2x():
# ...
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
print 'In command2', argument1, option1
cli = click.CommandCollection(sources=[group1, group2])
if __name__ == '__main__':
cli(obj={})
And this is the result when using command2:
$ python cli-test.py command2 --optparam=123
> Error: no such option: --optparam`
What's wrong with this example. I tried to follow the docs closely, but opt-param doesn't seem to be recognised.
The basic issue with the desired scheme is that click.CommandCollection does not call the group function. It skips directly to the command. In addition it is desired to apply options to the group via decorator, but have the options parsed by the command. That is:
> my_prog my_command --group-option
instead of:
> my_prog --group-option my_command
How?
This click.Group derived class hooks the command invocation for the commands to intercept the group parameters, and pass them to the group command.
In Group.add_command, add the params to the command
In Group.add_command, override command.invoke
In overridden command.invoke, take the special args inserted from the group and put them into ctx.obj and remove them from params
In overridden command.invoke, invoke the group command, and then the command itself
Code:
import click
class GroupWithCommandOptions(click.Group):
""" Allow application of options to group with multi command """
def add_command(self, cmd, name=None):
click.Group.add_command(self, cmd, name=name)
# add the group parameters to the command
for param in self.params:
cmd.params.append(param)
# hook the commands invoke with our own
cmd.invoke = self.build_command_invoke(cmd.invoke)
self.invoke_without_command = True
def build_command_invoke(self, original_invoke):
def command_invoke(ctx):
""" insert invocation of group function """
# separate the group parameters
ctx.obj = dict(_params=dict())
for param in self.params:
name = param.name
ctx.obj['_params'][name] = ctx.params[name]
del ctx.params[name]
# call the group function with its parameters
params = ctx.params
ctx.params = ctx.obj['_params']
self.invoke(ctx)
ctx.params = params
# now call the original invoke (the command)
original_invoke(ctx)
return command_invoke
Test Code:
#click.group()
#click.pass_context
def group1(ctx):
pass
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
click.echo('In command2 %s %s' % (argument1, option1))
#click.group(cls=GroupWithCommandOptions)
#click.option('--optparam', default=None, type=str)
#click.option('--optparam2', default=None, type=str)
#click.pass_context
def group2(ctx, optparam, optparam2):
# create_foo_by_processing_params(optparam, optparam2)
ctx.obj['foo'] = 'from group2 %s %s' % (optparam, optparam2)
#group2.command()
#click.pass_context
def command2a(ctx):
click.echo('command2a foo:%s' % ctx.obj['foo'])
#group2.command()
#click.option('--another-param', default=None, type=str)
#click.pass_context
def command2b(ctx, another_param):
click.echo('command2b %s %s' % (ctx['foo'], another_param))
cli = click.CommandCollection(sources=[group1, group2])
if __name__ == '__main__':
cli('command2a --optparam OP'.split())
Results:
command2a foo:from group2 OP None
This isn't the answer I am looking for, but a step towards it. Essentially a new kind of group is introduced (GroupExt) and the option added to the group is now being added to the command.
$ python cli-test.py command2 --optparam=12
cli
command2 12
import click
class GroupExt(click.Group):
def add_command(self, cmd, name=None):
click.Group.add_command(self, cmd, name=name)
for param in self.params:
cmd.params.append(param)
#click.group()
def group1():
pass
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
print 'In command2', argument1, option1
# Equivalent to #click.group() with special group
#click.command(cls=GroupExt)
#click.option('--optparam', default=None, type=str)
def group2():
print 'in group2'
#group2.command()
def command2(optparam):
print 'command2', optparam
#click.command(cls=click.CommandCollection, sources=[group1, group2])
def cli():
print 'cli'
if __name__ == '__main__':
cli(obj={})
This is not quite what I am looking for. Ideally, the optparam would be handled by group2 and the results placed into the context, but currently it's processed in the command2. Perhaps someone knows how to extend this.

How can I pass a ctx (Context) to CliRunner?

CliRunner lists no parameter to provide a context in its documentation.
The following should qualify as a minimum working example.
The real problem is a bit different.
It could be solved by moving the click decorated function into its own function for test coverage. Then the click function would be rendered almost useless.
import click
from click.testing import CliRunner
class Config():
def __init__(self):
self.value = 651
#click.command()
#click.pass_context
def print_numberinfo(ctx):
if not hasattr(ctx.obj, 'value'):
ctx.obj = Config()
click.echo(ctx.obj.value)
def test_print_numberinfo():
ctx = click.Context(print_numberinfo, obj = Config())
ctx.obj.value = 777
runner = CliRunner()
# how do I pass ctx to runner.invoke?
result = runner.invoke(print_numberinfo)
assert result.output == str(ctx.obj.value) + '\n'
You would directly pass your Config instance as keyword argument obj to runner.invoke:
import click
from click.testing import CliRunner
class Config():
def __init__(self):
self.value = 651
#click.command()
#click.pass_obj
def print_numberinfo(obj):
if not hasattr(obj, 'value'):
obj = Config()
click.echo(obj.value)
def test_print_numberinfo():
obj = Config()
obj.value = 777
runner = CliRunner()
# how do I pass ctx to runner.invoke?
result = runner.invoke(print_numberinfo, obj=obj)
assert result.output == str(obj.value) + '\n'
For someone who just want to make context.obj works like call from command-line:
CliRunner().invoke(commands.cli, ['sayhello'], catch_exceptions=False)
The first argument should be the the root group of click, then you can pass the command you want to call to the second argument(that is sayhello).
How commands.py like:
# !/usr/bin/env python
# coding: utf-8
import click
#click.group()
#click.pass_context
def cli(ctx):
ctx.obj = {
'foo': 'bar'
}
#cli.command()
#click.pass_context
def sayehello(ctx):
click.echo('hello!' + ctx.obj)
Appreciate to geowurster providing the solution.

Shared options and flags between commands

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
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)
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.
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()

Categories

Resources