本文主要参考和对官方文档进行二次浓缩而成
Click
是一个利用很少的代码以可组合的方式创造优雅命令行工具接口的 Python
库。 它是高度可配置的,但却有合理默认值的命令行接口创建工具。它致力于将创建命令行工具的过程变的快速而有趣,免除你因无法实现一个 CLIAPI
的挫败感。
- 支持任意嵌套命令
- 自动生成帮助页面
- 支持在运行时延迟加载子命令
下面,我们一起看一个官方文档中提及的 click
项目的使用示例代码。Click
是通过装饰器声明命令的,在内部高级用例有一个非装饰器接口,但不鼓励直接使用高级用法。一个函数通过装饰器 click.command()
成为一个 Click
命令行工具 。
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('Hello %s!' % name)
if __name__ == '__main__':
hello()
# 当它运行的时候是这样的
$ python hello.py --count=3
Your name: John
Hello John!
Hello John!
Hello John!
# 自动生成美观的格式化帮助页面
$ python hello.py --help
Usage: hello.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.
1. 添加方式
参数(包括选项和参数)都接受一些参数声明的位置参数。
Click
支持两种类型的脚本参数形式:选项(option
) 和 参数(argument
) 。正如其名称所示,选项是可选的,虽然参数在合理的范围内是可选的,但是它们在选择的方式上会受到更多的限制。参数功能略少于选项,以下功能仅适用于选项。另一方面与选项不同,参数可以接受任意数量的参数,选项可以严格地只接受固定数量的参数,默认是一个。
- 选项可自动提示缺少输入
- 选项可作为标志(布尔值或其他)
- 选项值可以从环境变量中拉出来,但参数不能
- 选项能完整记录在帮助页面中,但参数不能
# 分别使用了两种装饰器
@click.command()
@click.option('--count', default=1, help='number of greetings')
@click.argument('name')
def hello(count, name):
for x in range(count):
click.echo('Hello %s!' % name)
# 运行效果如下所示
$ python hello.py --help
Usage: hello.py [OPTIONS] NAME
Options:
--count INTEGER number of greetings
--help Show this message and exit.
2. 选项使用
选项不是位置参数
选项通过 option()
装饰器给命令增加选项,使用配置参数来控制不同的选项类型。这里有一个坑,那就是 Click
中的选项不同于函数中的位置参数,所以在使用的时候需要特别注意下。
- 基本使用
# dots --n=2
@click.command()
@click.option('--n', default=1)
def dots(n):
click.echo('.' * n)
- 提示信息
# hello
@click.command()
@click.option('--name', prompt='Your name please')
def hello(name):
click.echo('Hello %s!' % name)
- 密码提示
# encrypt
@click.command()
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True)
def encrypt(password):
click.echo('Encrypting password to %s' % password.encode('rot13'))
# encrypt
@click.command()
@click.password_option()
def encrypt(password):
click.echo('Encrypting password to %s' % password.encode('rot13'))
- 变量获取
@click.command()
@click.option('--username', prompt=True, default=lambda: os.environ.get('USER', ''))
def hello(username):
print("Hello,", username)
- 多值选项
# findme --pos 2.0 3.0
@click.command()
@click.option('--pos', nargs=2, type=float)
def findme(pos):
click.echo('%s / %s' % pos)
# commit -m foo -m bar
@click.command()
@click.option('--message', '-m', multiple=True)
def commit(message):
click.echo('\n'.join(message))
- 元组选项
# putitem --item peter 1338
@click.command()
@click.option('--item', type=(unicode, int))
def putitem(item):
click.echo('name=%s id=%d' % item)
# putitem --item peter 1338
@click.command()
@click.option('--item', nargs=2, type=click.Tuple([unicode, int]))
def putitem(item):
click.echo('name=%s id=%d' % item)
- 选项计数
# log -vvv
@click.command()
@click.option('-v', '--verbose', count=True)
def log(verbose):
click.echo('Verbosity: %s' % verbose)
- 布尔标记
# info --shout
# info --no-shout
import sys
@click.command()
@click.option('--shout/--no-shout', default=False)
def info(shout):
rv = sys.platform
if shout:
rv = rv.upper() + '!!!!111'
click.echo(rv)
# info --upper
# info --lower
import sys
@click.command()
@click.option('--upper', 'transformation', flag_value='upper', default=True)
@click.option('--lower', 'transformation', flag_value='lower')
def info(transformation):
click.echo(getattr(sys.platform, transformation)())
- 选择选项
# digest --hash-type=md5
# digest --hash-type=sha1
@click.command()
@click.option('--hash-type', type=click.Choice(['md5', 'sha1']))
def digest(hash_type):
click.echo(hash_type)
- Yes 参数
# dropdb
# dropdb --yes
def abort_if_false(ctx, param, value):
if not value:
ctx.abort()
@click.command()
@click.option('--yes', is_flag=True, callback=abort_if_false,
expose_value=False,
prompt='Are you sure you want to drop the db?')
def dropdb():
click.echo('Dropped all tables!')
# dropdb
# dropdb --yes
@click.command()
@click.confirmation_option(prompt='Are you sure you want to drop the db?')
def dropdb():
click.echo('Dropped all tables!')
- 前缀参数
# chmod +w
# chmod -w
@click.command()
@click.option('+w/-w')
def chmod(w):
click.echo('writable=%s' % w)
if __name__ == '__main__':
chmod()
- 范围选项
# repeat --count=1000 --digit=5
@click.command()
@click.option('--count', type=click.IntRange(0, 20, clamp=True))
@click.option('--digit', type=click.IntRange(0, 10))
def repeat(count, digit):
click.echo(str(digit) * count)
if __name__ == '__main__':
repeat()
- 优先选项和回调选项
# hello
# hello --version
def print_version(ctx, param, value):
if not value or ctx.resilient_parsing:
return
click.echo('Version 1.0')
ctx.exit()
@click.command()
@click.option('--version', is_flag=True, callback=print_version,
expose_value=False, is_eager=True)
def hello():
click.echo('Hello World!')
- 回调函数进行验证
# roll --rolls=42
def validate_rolls(ctx, param, value):
try:
rolls, dice = map(int, value.split('d', 2))
return (dice, rolls)
except ValueError:
raise click.BadParameter('rolls need to be in format NdM')
@click.command()
@click.option('--rolls', callback=validate_rolls, default='1d6')
def roll(rolls):
click.echo('Rolling a %d-sided dice %d time(s)' % rolls)
if __name__ == '__main__':
roll()
3. 参数使用
参数是位置参数
由于其语法性质,参数也仅支持 options
全部功能的一个子集,Click
不会记录参数,希望你手动记录它们以避免跳出帮助页面。
- 基本参数
# touch foo.txt
@click.command()
@click.argument('filename')
def touch(filename):
click.echo(filename)
- 可变参数
# copy foo.txt bar.txt my_folder
@click.command()
@click.argument('src', nargs=-1)
@click.argument('dst', nargs=1)
def copy(src, dst):
for fn in src:
click.echo('move %s to folder %s' % (fn, dst))
- 文件参数
# inout - hello.txt
# inout hello.txt -
@click.command()
@click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb'))
def inout(input, output):
while True:
chunk = input.read(1024)
if not chunk:
break
output.write(chunk)
- 文件路径参数
# touch hello.txt
@click.command()
@click.argument('f', type=click.Path(exists=True))
def touch(f):
click.echo(click.format_filename(f))
- 环境变量
# export SRC=hello.txt
# echo
@click.command()
@click.argument('src', envvar='SRC', type=click.File('r'))
def echo(src):
click.echo(src.read())
- Option-Like 参数
# touch -- -foo.txt bar.txt
@click.command()
@click.argument('files', nargs=-1, type=click.Path())
def touch(files):
for filename in files:
click.echo(filename)
4. 命令嵌套
将多个命令进行排列组合,使工具更加复杂。
顾名思义,命令可以附加到其他 Group
类型的命令之中,下面例子中的脚本实现了两个管理数据库相关的命令。正如你所看到的那样, group()
装饰器就像 command()
装饰器一样工作,但创建一个 Group
对象,可以通过 Group.add_command()
赋予多个可以附加的子命令。
@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)
对于简单的脚本,也可以使用 Group.command()
装饰器自动附加和创建命令,我们将上面的脚本可以,如下方式的改写,即可简单代码。
@click.group()
def cli():
pass
@cli.command()
@click.argument("date")
def initdb(date):
click.echo(f'[{date}] Initialized the database')
@cli.command()
@click.option('--count', default=1, type=click.IntRange(1, 100000))
def dropdb(count):
click.echo(f'[{count}] Dropped the database')
- 回调调用
# tool.py --debug sync
@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
click.echo('Debug mode is %s' % ('on' if debug else 'off'))
@cli.command()
def sync():
click.echo('Synching')
- 合并多个指令
# cli --help
import click
@click.group()
def cli1():
pass
@cli1.command()
def cmd1():
"""Command on cli1"""
@click.group()
def cli2():
pass
@cli2.command()
def cmd2():
"""Command on cli2"""
cli = click.CommandCollection(sources=[cli1, cli2])
if __name__ == '__main__':
cli()
- 嵌套处理和上下文
@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
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={})
5. 实用功能
这里介绍一下其他有趣的小功能,可以使我们写的工具更加出彩。
- [1] 用户输入提示
Click
支持两个不同地方的提示,第一种是参数处理发生时自动提示,第二种是后来单独要求提示。这可以通过 prompt()
功能实现,该功能要求根据类型进行有效输入,或者使用 confirm()
功能,并在使用时要求确认的功能是/否。
# 输入提示
value = click.prompt('Please enter a number', default=42.0)
value = click.prompt('Please enter a valid integer', type=int)
# 确认提示
if click.confirm('Do you want to continue?'):
click.echo('Well done!')
click.confirm('Do you want to continue?', abort=True)
- [2] 彩色字体打印
从 Click 2.0
开始,该 echo()
函数获得了处理ANSI
颜色和样式的额外功能。 在Windows
上,此功能仅在安装了 colorama 时可用。如果安装,则ANSI
代码将被智能处理。
import click
click.echo(click.style('Hello World!', fg='green'))
click.echo(click.style('Some more text', bg='blue', fg='white'))
click.echo(click.style('ATTENTION', blink=True, bold=True))
- [3] 分页支持
在某些情况下,您可能希望在终端上显示长文本,并让用户滚动浏览。这可以通过使用与 echo_via_pager()
函数类似的 echo()
函数来实现 ,但是总是写入标准输出,如果可能的话,可以使用分页器。
@click.command()
def less():
click.echo_via_pager('\n'.join('Line %d' % idx for idx in range(200)))
- [4] 屏幕清除
从Click 2.0
开始提供的功能,要清除终端屏幕,您可以使用 clear()
。它的名字的确如此:它以平台不可知的方式清除整个可见的屏幕。
import click
click.clear()
- [5] 从终端获取字符
通常,当从终端读取输入时,您将从标准输入读取。但是,这是缓冲输入,直到线路终止才显示。在某些情况下,您可能不想这样做,而是在写入时读取单个字符。为此,Click
提供了 getchar()
函数,从终端缓冲区读取单个字符并将其作为Unicode
字符返回的功能。
import click
click.echo('Continue? [yn] ', nl=False)
c = click.getchar()
click.echo()
if c == 'y':
click.echo('We will go on')
elif c == 'n':
click.echo('Abort!')
else:
click.echo('Invalid input :(')
- [6] 打印文件名
由于文件名可能不是Unicode
,格式化它们可能有点棘手。通常,在Python2
中比在Python3
上更容易,因为你可以用这个 print
函数把字节写到stdout
中,但是在Python3
中,你总是需要使用Unicode
。这与点击工作的方式是通过 format_filename()
它尽最大努力将文件名转换为Unicode
,永远不会失败。这使得可以在完整的Unicode
字符串的上下文中使用这些文件名。
click.echo('Path: %s' % click.format_filename(b'foo.txt'))
- [7] 智能文件打开
从Click 3.0
开始, File
通过 open_file()
函数公开从文件中打开文件的逻辑,它可以智能地打开标准输入/标准输出以及任何其他文件。
with click.open_file(filename, 'w') as f:
f.write('Hello World!\n')
- [8] 显示进度条
有时候,你有命令行脚本需要处理大量的数据,但是你想要快速地向用户展示一下这个过程需要多长时间。点击支持通过该 progressbar()
函数进行简单的进度条渲染。基本用法非常简单:这个想法是你有一个你想要操作的迭代器。对于迭代中的每个项目,可能需要一些时间来处理。
import click
with click.progressbar(all_the_users_to_process) as bar:
for user in bar:
modify_the_user(user)
Click
会自动将进度条打印到终端,并计算剩余的时间。剩余时间的计算要求可迭代的长度。如果它没有长度,但你知道长度,你可以明确地提供它。
with click.progressbar(all_the_users_to_process, length=number_of_users) as bar:
for user in bar:
modify_the_user(user)
另一个有用的功能是将标签与将在进度条之前显示的进度条相关联
with click.progressbar(all_the_users_to_process,
label='Modifying user accounts',
length=number_of_users) as bar:
for user in bar:
modify_the_user(user)
有时,可能需要迭代外部迭代器,并不规则地前进进度条。为此,您需要指定长度(并且不可迭代),并在上下文返回值上使用 update 方法,而不是直接在其上进行迭代。
with click.progressbar(length=total_size,
label='Unzipping archive') as bar:
for archive in zip_file:
archive.extract()
bar.update(archive.size)
6. 高级功能
介绍官网文档中重要的高级功能和特性!
- [1] 类装饰使用
如果我们需要一个可以容纳所有命令的组,在这种情况下,我们使用 click.group()
允许我们在其下面注册其他 Click
命令。
import click
class Repo(object):
def __init__(self, home=None, debug=False):
self.home = os.path.abspath(home or '.')
self.debug = debug
def hello(self):
print('hello world!')
@click.command()
@click.option('--repo-home', envvar='REPO_HOME', default='.repo')
@click.option('--debug/--no-debug', default=False, envvar='REPO_DEBUG')
@click.pass_context
def cli(ctx, repo_home, debug):
ctx.obj = Repo(repo_home, debug)
ctx.obj.hello()
if __name__ == "__main__":
cli()
import click
class Repo(object):
def __init__(self, home=None, debug=False):
self.home = os.path.abspath(home or '.')
self.debug = debug
def hello(self):
print('hello world!')
@click.command()
@click.argument('repo-home')
@click.argument('debug', default=False)
@click.pass_context
def cli(ctx, repo_home, debug):
ctx.obj = Repo(repo_home, debug)
ctx.obj.hello()
if __name__ == "__main__":
cli()
import os
import click
class Environment:
def __init__(self):
self.home = os.getcwd()
def get_home(self, home):
"""Logs a message to stderr."""
if not home is None:
self.home = home
print(f'{self.home}')
pass_environment = click.make_pass_decorator(Environment, ensure=True)
@click.command()
@click.option('--home', help='test home')
@pass_environment
def cli(ctx, home):
ctx.verbose = home
ctx.get_home('/home/escape')
if __name__ == "__main__":
cli()
import os
import click
import posixpath
class Repo:
def __init__(self, home):
self.home = home
self.config = {}
self.verbose = False
def set_config(self, key, value):
self.config[key] = value
if self.verbose:
click.echo(f" config[{key}] = {value}", file=sys.stderr)
def __repr__(self):
return f"<Repo {self.home}>"
pass_repo = click.make_pass_decorator(Repo)
@click.group()
@click.option("--repo-home", envvar="REPO_HOME", default=".repo", metavar="PATH", help="Changes the repository folder location.")
@click.option("--config", nargs=2, multiple=True, metavar="KEY VALUE", help="Overrides a config key/value pair.")
@click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode.")
@click.pass_context
def cli(ctx, repo_home, config, verbose):
ctx.obj = Repo(os.path.abspath(repo_home))
ctx.obj.verbose = verbose
for key, value in config:
ctx.obj.set_config(key, value)
@cli.command()
@click.argument("src")
@click.argument("dest", required=False)
def clone(repo, src, dest):
if dest is None:
dest = posixpath.split(src)[-1] or "."
click.echo(f"Cloning repo {src} to {os.path.basename(dest)}")
repo.home = dest
@cli.command()
@click.option("--username", prompt=True, help="The developer's shown username.")
@click.option("--email", prompt="E-Mail", help="The developer's email address")
@click.password_option(help="The login password.")
@pass_repo
def setuser(repo, username, email, password):
repo.set_config("username", username)
repo.set_config("email", email)
repo.set_config("password", "*" * len(password))
click.echo("Changed credentials.")
if __name__ == "__main__":
cli()
- [2] 多命令管道
多命令管道,顾名思义就是让一个命令处理前一个命令的结果,有多种方法可以实现该功能。最显而易见的方法是在上下文对象上存储一个值,并将其从一个函数处理到另一个函数。这可以通过装饰一个 pass_context()
函数来完成,在这个函数之后提供上下文对象,子命令可以在那里存储它的数据。
另一种方法是通过返回处理函数来设置管线。可以这样想:当一个子命令被调用时,它处理所有的参数,并提出一个如何处理的计划。在那时,它返回一个处理函数并返回。链式多命令可以通过 resultcallback()
遍历所有这些函数来注册回调,然后调用它们。
import click
@click.group(chain=True, invoke_without_command=True)
@click.option('-i', '--input', type=click.File('r'))
def cli(input):
pass
@cli.resultcallback()
def process_pipeline(processors, input):
iterator = (x.rstrip('\r\n') for x in input)
for processor in processors:
iterator = processor(iterator)
for item in iterator:
click.echo(item)
@cli.command('uppercase')
def make_uppercase():
def processor(iterator):
for line in iterator:
yield line.upper()
return processor
@cli.command('lowercase')
def make_lowercase():
def processor(iterator):
for line in iterator:
yield line.lower()
return processor
@cli.command('strip')
def make_strip():
def processor(iterator):
for line in iterator:
yield line.strip()
return processor
if __name__ == "__main__":
cli()
- [3] 上下文默认值
从 Click 2.0
开始,不仅可以在调用脚本时覆盖上下文的默认值,还可以在声明命令的装饰器中覆盖默认值。举个例子,前面定义了一个自定义的例子,default_map
现在也可以在装饰器中完成。
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('--port', default=8000)
def runserver(port):
click.echo('Serving on http://127.0.0.1:%d/' % port)
if __name__ == '__main__':
cli(default_map={
'runserver': {
'port': 5000
}
})
import click
CONTEXT_SETTINGS = dict(
default_map={'runserver': {'port': 5000}}
)
@click.group(context_settings=CONTEXT_SETTINGS)
def cli():
pass
@cli.command()
@click.option('--port', default=8000)
def runserver(port):
click.echo('Serving on http://127.0.0.1:%d/' % port)
if __name__ == '__main__':
cli()
7. Bash 参数补全
即打包之后,通过命令行可以直接补全使用。
Click 2.0
中的任何 Click
脚本都支持 Bash
参数补全。在完成这项工作时,会有一些限制,但大多数情况下,它是可以正常工作的。只有当脚本正确安装时,才可以使用 Bash
参数补全,而不是通过 python
命令执行。 如何做到这一点,参见 Setuptools 集成。此外,Click
目前只支持 Bash
参数补全。
$ repo <TAB><TAB>
clone commit copy delete setuser
$ repo clone -<TAB><TAB>
--deep --help --rev --shallow -r
8. Setuptools 集成
说白了就是,将工具打包成为库进行使用,方便且简单。
就目前你写的代码,文件末尾有一个类似这种的代码块: if __name__ == '__main__':
,这是一个传统独立的 Python
文件格式。使用 Click
你可以继续这样做,但使用 setuptools
是一个更好的办法。
而使用setuptools
的两个主要的原因是:第一个原因是 setuptools
自动为 Windows
生成可执行的包装器,所以你的命令行工具也可以在 Windows
的系统上面工作。第二个原因是 setuptools
脚本和 Unix
上的 virtualenv
一起工作,而不需要激活 virtualenv
。这是一个非常有用的概念,它允许你将你的脚本和所有依赖绑定到一个虚拟环境中。
# 目录结构
.
|__ setup.py
|__ yourscript.py
# yourscript.py内容
import click
@click.command()
def cli():
"""Example script."""
click.echo('Hello World!')
# setup.py内容
from setuptools import setup
setup(
name='yourscript',
version='0.1',
py_modules=['yourscript'],
install_requires=[
'Click',
],
entry_points='''
[console_scripts]
yourscript=yourscript:cli
''',
)
# 测试效果(创建新环境并安装)
$ virtualenv venv
$ . venv/bin/activate
$ pip install --editable .
$ yourscript
Hello World!