vlambda博客
学习文章列表

python之强大的命令行工具click

什么是click

click是flask的开发团队pallets的另一款开源项目,用于快速创建命令行工具的模块。

git:https://github.com/pallets/click
文档:https://click.palletsprojects.com/en/8.1.x/

quickStart

click的思路非常简单。

  • 定义处理函数,通过@click.command()装饰一个函数,使之成为命令行接口。
  • 并且通过@click.option()、@click.arguments等装饰器装饰函数,为其添加命令行选项、参数等。
import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
              help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo(f"Hello {name}!")

if __name__ == '__main__':
    hello()

在上面的示例中,使用@click.command()装饰函数hello,使之成为命令行接口。使用click.option来定义选项:--count和—-name。

执行代码,指定count和name:

% python3 test.py --count=2 --name=xixi
Hello xixi!
Hello xixi!

我们还可以通过 --help 参数查看自动生成的帮助信息:

% python3 test.py --help               
Usage: test.py [OPTIONS]

  Simple program that greets NAME for a total of COUNT times.

Options:
  --count INTEGER  Number of greetings.
  --name TEXT      The person to greet.
  --help           Show this message and exit.

click把命令行分为3个组成部分:参数、选项和命令。

  • 参数:参数就是在命令后的除选项外的内容。比如git add a.txt中的a.txt就是表示文件路径的参数
  • 选项:就是以-或—开头的命令。如-f或--file。
  • 命令:即命令行程序的本质,比如git是命令、git add中的add是git的子命令。

接下来,看下click是怎么帮助开发人员定义参数、选项和命令的。

定义参数

看个demo

import click


@click.command()
@click.argument('x')
@click.argument('y')
def hello(x, y):
    print(x, y)


if __name__ == '__main__':
    hello()

执行代码

% python3 test.py 1 2
1 2

可以看到可以使用@click.argument来定义参数,第一个参数为参数名。参数默认情况下字符串类型,我们可以通过type来指定参数类型。

import click


@click.command()
@click.argument('x'type=click.INT)
@click.argument('y'type=click.INT)
def hello(x, y):
    print(x + y)


if __name__ == '__main__':
    hello()

"""
执行结果
% python3 test.py 1 2
3
"
""

click 支持的多种参数类型:

  • str / click.STRING 表示字符串类型,这也是默认类型
  • int / click.INT 表示整型
  • float / click.FLOAT 表示浮点型
  • bool / click.BOOL 表示布尔型。对于 1、yes、y 和 true 会转化为 True;0、no、n 和 false 会转化为 False
  • click.UUID 表示 UUID,会自动将参数转换为 uuid.UUID 对象
  • click.FILE 表示文件,会自动将参数转换为文件对象,并在命令行结束时自动关闭文件
  • click.PATH 表示路径
  • click.Choice 表示选择选项
  • click.IntRange 表示范围选项

除此之外,还支持自定义参数类型。click定义自定义类型,需要编写click.ParamType的子类,并重载convert方法。我们看一个在celery中使用的自定义参数类型的例子。

class App(ParamType):
    """Application option."""

    name = "application"

    def convert(self, value, param, ctx):
        try:
            return find_app(value)
        except ModuleNotFoundError as e:
            if e.name != value:
                exc = traceback.format_exc()
                self.fail(
                    UNABLE_TO_LOAD_APP_ERROR_OCCURRED.format(value, exc)
                )
            self.fail(UNABLE_TO_LOAD_APP_MODULE_NOT_FOUND.format(e.name))
        except AttributeError as e:
            attribute_name = e.args[0].capitalize()
            self.fail(UNABLE_TO_LOAD_APP_APP_MISSING.format(attribute_name))
        except Exception:
            exc = traceback.format_exc()
            self.fail(
                UNABLE_TO_LOAD_APP_ERROR_OCCURRED.format(value, exc)
            )


APP = App()

我们在使用celery启动worker时会需要通过-A指定app, 那么这个app的类型就是这里的App(),其convert方法中,包含对app是否存在,是否能加载的校验(注:这里的-A其实属于option了,但是含义是相似的)。

定义选项

我们可以通过@click.option可以给命令增加选项,并通过配置函数的参数来配置不同功能的选项。

Click.option接收的前两个参数是长、短选项(顺序随意)。

  • 长选项以“—”开头,例如“—file”。
  • 短选项以“-”开头,例如“-s”。

第三个参数为选项参数的名称。

import click


@click.command()
@click.option('-s''--string-to-echo''string')
def echo(string):
    click.echo(string)


if __name__ == '__main__':
    echo()

"""
% python3 test.py -s test
test
"
""

如果不指定第三个参数,将会使用长选项的下划线形式名称:

import click


@click.command()
@click.option('-s''--string-to-echo')
def echo(string_to_echo):
    click.echo(string_to_echo)


if __name__ == '__main__':
    echo()

选择项的type和上面介绍的参数type类似。

定义命令和嵌套命令

通过上文,我们已经知道可以使用@click.command来非常简单地定义一个命令。然而click真正强大的地方在于其借助@click.group实现的命令组的功能。所谓命令组就是若干个命令(或叫子命令)的集合。

import click


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


@click.command()
def initdb():
    click.echo('Initialized the database')


@click.command()
def dropdb():
    click.echo('Dropped the database')


cli.add_command(initdb)
cli.add_command(dropdb)


if __name__ == '__main__':
    cli()

如上所示,@click.group装饰器的工作方式类似于@click.command装饰器,但它创建了一个group对象,该对象可以被赋予多个子命令。这些子命令可以通过group.add_command附加。

除此之外,也可以使用Group.command装饰器来自动附加和创建命令。

import click

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

@cli.command() #注意这里是 @cli.command(), 不是@click.command()!
def initdb():
    click.echo('Initialized the database')

@cli.command() #注意这里是 @cli.command(), 不是@click.command()!
def dropdb():
    click.echo('Dropped the database')

# cli.add_command(initdb)
# cli.add_command(dropdb)


if __name__ == '__main__':
    cli()

上下文传递

默认情况下,命令组接收的参数和子命令彼此独立,但有时候我们希望可以在子命令中获取命令组的参数,这时候就可以用Context来实现。具体是通过pass_context装饰器来显式地让click传递上下文,这时候,context会作为第一个参数进行传递。

import click

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    # 确保 ctx.obj 存在并且是个 dict。 (以防 `cli()` 指定 obj 为其他类型
    ctx.ensure_object(dict)

    ctx.obj['DEBUG'] = debug


@cli.command()
@click.pass_context
def sync(ctx):
    click.echo('Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off'))


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


默认情况下,调用子命令的时候才会调用命令组,否则会报“Error: Missing command.”,而有时我们可能想直接调用命令组,通过指定 click.group的invoke_without_command=True 来实现:

@click.group(invoke_without_command=True)

总结

通过上文的介绍,我们已经可以通过定义参数、选项和命令来轻松地实现一个基本的命令行程序。此外,click还提供了许多buffer功能,比如参数自动补全、支持运行时延迟加载子命令等等,感兴趣可以去官网一探究竟。

参考文档:https://zhuanlan.zhihu.com/p/90307978 https://blog.csdn.net/weixin_38278993/article/details/100052961 https://click.palletsprojects.com/en/8.1.x/