|
| 1 | +# Python 命令行之旅:深入 click(四) |
| 2 | + |
| 3 | +## 一、前言 |
| 4 | + |
| 5 | +在前面三篇文章中,我们介绍了 `click` 中的参数、选项和命令,本文将介绍 `click` 锦上添花的功能,以帮助我们更加轻松地打造一个更加强大的命令行程序。 |
| 6 | + |
| 7 | +``` |
| 8 | +本系列文章默认使用 Python 3 作为解释器进行讲解。 |
| 9 | +若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~ |
| 10 | +``` |
| 11 | + |
| 12 | +## 二、增强功能 |
| 13 | + |
| 14 | +### 2.1 Bash 补全 |
| 15 | + |
| 16 | +Bash 补全是 `click` 提供的一个非常便捷和强大的功能,这是它比 `argpase` 和 `docopt` 强大的一个表现。 |
| 17 | + |
| 18 | +在命令行程序正确安装后,Bash 补全才可以使用。而如何安装可以参考 [setup 集成](https://click.palletsprojects.com/en/7.x/setuptools/#setuptools-integration)。Click 目前仅支持 Bash 和 Zsh 的补全。 |
| 19 | + |
| 20 | +#### 2.1.1 补全能力 |
| 21 | + |
| 22 | +通常来说,Bash 补全支持对子命令、选项、以及选项或参数值得补全。比如: |
| 23 | + |
| 24 | +```plain |
| 25 | +$ repo <TAB><TAB> |
| 26 | +clone commit copy delete setuser |
| 27 | +$ repo clone -<TAB><TAB> |
| 28 | +--deep --help --rev --shallow -r |
| 29 | +``` |
| 30 | + |
| 31 | +此外,`click` 还支持自定义补全,这在动态生成补全场景中很有用,使用 `autocompletion` 参数。`autocompletion` 需要指定为一个回调函数,并且返回字符串的列表。此函数接受三个参数: |
| 32 | + |
| 33 | +- `ctx` —— 当前的 click 上下文 |
| 34 | +- `args` 传入的参数列表 |
| 35 | +- `incomplete` 正在补全的词 |
| 36 | + |
| 37 | +这里有一个根据环境变量动态生成补全的示例: |
| 38 | + |
| 39 | +```python |
| 40 | +import os |
| 41 | + |
| 42 | +def get_env_vars(ctx, args, incomplete): |
| 43 | + return [k for k in os.environ.keys() if incomplete in k] |
| 44 | + |
| 45 | +@click.command() |
| 46 | +@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars) |
| 47 | +def cmd1(envvar): |
| 48 | + click.echo('Environment variable: %s' % envvar) |
| 49 | + click.echo('Value: %s' % os.environ[envvar]) |
| 50 | +``` |
| 51 | + |
| 52 | +在 `ZSH` 中,还支持补全帮助信息。只需将 `autocompletion` 回调函数中返回的字符串列表中的字符串改为二元元组,第一个元素是补全内容,第二个元素是帮助信息。 |
| 53 | + |
| 54 | +这里有一个颜色补全的示例: |
| 55 | + |
| 56 | +```python |
| 57 | +import os |
| 58 | + |
| 59 | +def get_colors(ctx, args, incomplete): |
| 60 | + colors = [('red', 'help string for the color red'), |
| 61 | + ('blue', 'help string for the color blue'), |
| 62 | + ('green', 'help string for the color green')] |
| 63 | + return [c for c in colors if incomplete in c[0]] |
| 64 | + |
| 65 | +@click.command() |
| 66 | +@click.argument("color", type=click.STRING, autocompletion=get_colors) |
| 67 | +def cmd1(color): |
| 68 | + click.echo('Chosen color is %s' % color) |
| 69 | +``` |
| 70 | + |
| 71 | +#### 2.1.2 激活补全 |
| 72 | + |
| 73 | +要激活 Bash 的补全功能,就需要告诉它你的命令行程序有补全的能力。通常通过一个神奇的环境变量 `_<PROG_NAME>_COMPLETE` 来告知,其中 `<PROG_NAME>` 是大写下划线形式的程序名称。 |
| 74 | + |
| 75 | +比如有一个命令行程序叫做 `foo-bar`,那么对应的环境变量名称为 `_FOO_BAR_COMPLETE`,然后在 `.bashrc` 中使用 `source` 导出即可: |
| 76 | + |
| 77 | +````bash |
| 78 | +eval "$(_FOO_BAR_COMPLETE=source foo-bar)" |
| 79 | +··· |
| 80 | + |
| 81 | +或者在 `.zshrc` 中使用: |
| 82 | +```bash |
| 83 | +eval "$(_FOO_BAR_COMPLETE=source_zsh foo-bar)" |
| 84 | +```` |
| 85 | +
|
| 86 | +不过上面的方式总是在命令行程序启动时调用,这可能在有多个程序时减慢 shell 激活的速度。另一种方式是把命令放在文件中,就像这样: |
| 87 | +
|
| 88 | +```bash |
| 89 | +# 针对 Bash |
| 90 | +_FOO_BAR_COMPLETE=source foo-bar > foo-bar-complete.sh |
| 91 | + |
| 92 | +# 针对 ZSH |
| 93 | +_FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh |
| 94 | +``` |
| 95 | + |
| 96 | +然后把脚本文件路径加到 `.bashrc` 或 `.zshrc` 中: |
| 97 | + |
| 98 | +```bash |
| 99 | +. /path/to/foo-bar-complete.sh |
| 100 | +``` |
| 101 | + |
| 102 | +### 2.2 实用工具 |
| 103 | + |
| 104 | +#### 2.2.1 打印到标准输出 |
| 105 | + |
| 106 | +[echo()](https://click.palletsprojects.com/en/7.x/api/#click.echo) 函数可以说是最有用的实用工具了。它和 Python 的 `print` 类似,主要的区别在于它同时在 Python 2 和 3 中生效,能够智能地检测未配置正确的输出流,且几乎不会失败(除了 Python 3 中的[少数限制](https://click.palletsprojects.com/en/7.x/python3/#python3-limitations)。) |
| 107 | + |
| 108 | +`echo` 即支持 unicode,也支持二级制数据,如: |
| 109 | + |
| 110 | +```python |
| 111 | +import click |
| 112 | +
|
| 113 | +click.echo('Hello World!') |
| 114 | +
|
| 115 | +click.echo(b'\xe2\x98\x83', nl=False) # nl=False 表示不输出换行符 |
| 116 | +``` |
| 117 | + |
| 118 | +#### 2.2.2 ANSI 颜色 |
| 119 | + |
| 120 | +有些时候你可能希望输出是有颜色的,这尤其在输出错误信息时有用,而 `click` 在这方面支持的很好。 |
| 121 | + |
| 122 | +首先,你需要安装 `colorama`: |
| 123 | + |
| 124 | +```bash |
| 125 | +pip install colorama |
| 126 | +``` |
| 127 | + |
| 128 | +然后,就可以使用 [style()](https://click.palletsprojects.com/en/7.x/api/#click.style) 函数来指定颜色: |
| 129 | + |
| 130 | +```python |
| 131 | +import click |
| 132 | +
|
| 133 | +click.echo(click.style('Hello World!', fg='green')) |
| 134 | +click.echo(click.style('Some more text', bg='blue', fg='white')) |
| 135 | +click.echo(click.style('ATTENTION', blink=True, bold=True)) |
| 136 | +``` |
| 137 | + |
| 138 | +`click` 还提供了更加简便的函数 [secho](https://click.palletsprojects.com/en/7.x/api/#click.secho),它就是 `echo` 和 `style` 的组合: |
| 139 | + |
| 140 | +```python |
| 141 | +click.secho('Hello World!', fg='green') |
| 142 | +click.secho('Some more text', bg='blue', fg='white') |
| 143 | +click.secho('ATTENTION', blink=True, bold=True) |
| 144 | +``` |
| 145 | + |
| 146 | +#### 2.2.3 分页支持 |
| 147 | + |
| 148 | +有些时候,命令行程序会输出长文本,但你希望能让用户盘也浏览。使用 [echo_via_pager()](https://click.palletsprojects.com/en/7.x/api/#click.echo_via_pager) 函数就可以轻松做到。 |
| 149 | + |
| 150 | +例如: |
| 151 | + |
| 152 | +```python |
| 153 | +def less(): |
| 154 | + click.echo_via_pager('\n'.join('Line %d' % idx |
| 155 | + for idx in range(200))) |
| 156 | +``` |
| 157 | +
|
| 158 | +如果输出的文本特别大,处于性能的考虑,希望翻页时生成对应内容,那么就可以使用生成器: |
| 159 | +
|
| 160 | +```python |
| 161 | +def _generate_output(): |
| 162 | + for idx in range(50000): |
| 163 | + yield "Line %d\n" % idx |
| 164 | +
|
| 165 | +@click.command() |
| 166 | +def less(): |
| 167 | + click.echo_via_pager(_generate_output()) |
| 168 | +``` |
| 169 | +
|
| 170 | +#### 2.2.4 清除屏幕 |
| 171 | +
|
| 172 | +使用 [clear()](https://click.palletsprojects.com/en/7.x/api/#click.clear) 可以轻松清除屏幕内容: |
| 173 | +
|
| 174 | +```python |
| 175 | +import click |
| 176 | +click.clear() |
| 177 | +``` |
| 178 | +
|
| 179 | +#### 2.2.5 从终端获取字符 |
| 180 | +
|
| 181 | +通常情况下,使用内建函数 `input` 或 `raw_input` 获得的输入是用户输出一段字符然后回车得到的。但在有些场景下,你可能想在用户输入单个字符时就能获取到并且做一定的处理,这个时候 [getchar()](https://click.palletsprojects.com/en/7.x/api/#click.getchar) 就派上了用场。 |
| 182 | +
|
| 183 | +比如,根据输入的 `y` 或 `n` 做特定处理: |
| 184 | +
|
| 185 | +```python |
| 186 | +import click |
| 187 | +
|
| 188 | +click.echo('Continue? [yn] ', nl=False) |
| 189 | +c = click.getchar() |
| 190 | +click.echo() |
| 191 | +if c == 'y': |
| 192 | + click.echo('We will go on') |
| 193 | +elif c == 'n': |
| 194 | + click.echo('Abort!') |
| 195 | +else: |
| 196 | + click.echo('Invalid input :(') |
| 197 | +``` |
| 198 | +
|
| 199 | +#### 2.2.6 等待按键 |
| 200 | +
|
| 201 | +在 Windows 的 cmd 中我们经常看到当执行完一个命令后,提示按下任意键退出。通过使用 [pause()](https://click.palletsprojects.com/en/7.x/api/#click.pause) 可以实现暂停直至用户按下任意键: |
| 202 | +
|
| 203 | +```python |
| 204 | +import click |
| 205 | +click.pause() |
| 206 | +``` |
| 207 | +
|
| 208 | +#### 2.2.7 启动编辑器 |
| 209 | +
|
| 210 | +通过 [edit()](https://click.palletsprojects.com/en/7.x/api/#click.edit) 可以自动启动编辑器。这在需要用户输入多行内容时十分有用。 |
| 211 | +
|
| 212 | +在下面的示例中,会启动默认的文本编辑器,并在里面输入一段话: |
| 213 | +
|
| 214 | +```python |
| 215 | +import click |
| 216 | +
|
| 217 | +def get_commit_message(): |
| 218 | + MARKER = '# Everything below is ignored\n' |
| 219 | + message = click.edit('\n\n' + MARKER) |
| 220 | + if message is not None: |
| 221 | + return message.split(MARKER, 1)[0].rstrip('\n') |
| 222 | +``` |
| 223 | +
|
| 224 | +`edit()` 函数还支持打开特定文件,比如: |
| 225 | +
|
| 226 | +```python |
| 227 | +import click |
| 228 | +click.edit(filename='/etc/passwd') |
| 229 | +``` |
| 230 | +
|
| 231 | +#### 2.2.8 启动应用程序 |
| 232 | +
|
| 233 | +通过 [launch](https://click.palletsprojects.com/en/7.x/api/#click.launch) 可以打开 URL 或文件类型所关联的默认应用程序。如果设置 `locate=True`,则可以启动文件管理器并自动选中特定文件。 |
| 234 | +
|
| 235 | +示例: |
| 236 | +
|
| 237 | +```python |
| 238 | +# 打开浏览器,访问 URL |
| 239 | +click.launch("https://click.palletsprojects.com/") |
| 240 | +
|
| 241 | +# 使用默认应用程序打开 txt 文件 |
| 242 | +click.launch("/my/downloaded/file.txt") |
| 243 | +
|
| 244 | +# 打开文件管理器,并自动选中 file.txt |
| 245 | +click.launch("/my/downloaded/file.txt", locate=True) |
| 246 | +``` |
| 247 | +
|
| 248 | +#### 2.2.9 显示进度条 |
| 249 | +
|
| 250 | +`click` 内置了 [progressbar()](https://click.palletsprojects.com/en/7.x/api/#click.progressbar) 函数来方便地显示进度条。 |
| 251 | +
|
| 252 | +它的用法也很简单,假定你有一个要处理的可迭代对象,处理完每一项就要输出一下进度,那么就有两种用法。 |
| 253 | +
|
| 254 | +用法一:使用 `progressbar` 构造出 `bar` 对象,迭代 `bar` 对象来自动告知进度: |
| 255 | +
|
| 256 | +```python |
| 257 | +import time |
| 258 | +import click |
| 259 | +
|
| 260 | +all_the_users_to_process = ['a', 'b', 'c'] |
| 261 | +
|
| 262 | +def modify_the_user(user): |
| 263 | + time.sleep(0.5) |
| 264 | +
|
| 265 | +with click.progressbar(all_the_users_to_process) as bar: |
| 266 | + for user in bar: |
| 267 | + modify_the_user(user) |
| 268 | +``` |
| 269 | +
|
| 270 | +用法二:使用 `progressbar` 构造出 `bar` 对象,迭代原始可迭代对象,并不断向 `bar` 更新进度: |
| 271 | +
|
| 272 | +```python |
| 273 | +import time |
| 274 | +import click |
| 275 | +
|
| 276 | +all_the_users_to_process = ['a', 'b', 'c'] |
| 277 | +
|
| 278 | +def modify_the_user(user): |
| 279 | + time.sleep(0.5) |
| 280 | +
|
| 281 | +with click.progressbar(all_the_users_to_process) as bar: |
| 282 | + for user in enumerate(all_the_users_to_process): |
| 283 | + modify_the_user(user) |
| 284 | + bar.update(1) |
| 285 | +``` |
| 286 | +
|
| 287 | +#### 2.2.10 更多实用工具 |
| 288 | +
|
| 289 | +- [打印文件名](https://click.palletsprojects.com/en/7.x/utils/#printing-filenames) |
| 290 | +- [标准流](https://click.palletsprojects.com/en/7.x/utils/#standard-streams) |
| 291 | +- [智能打开文件](https://click.palletsprojects.com/en/7.x/utils/#intelligent-file-opening) |
| 292 | +- [查找应用程序文件夹](https://click.palletsprojects.com/en/7.x/utils/#finding-application-folders) |
| 293 | +
|
| 294 | +## 三、总结 |
| 295 | +
|
| 296 | +`click` 提供了非常多的增强型功能,本文着重介绍了它的 Bash 补全和十多个实用工具,这会让你在实现命令行的过程中如虎添翼。此外,`click` 还提供了诸如命令别名、参数修改、标准化令牌、调用其他命令、回调顺序等诸多[高级模式](https://click.palletsprojects.com/en/7.x/advanced/) 以应对更加复杂或特定的场景,我们就不再深入介绍。 |
| 297 | +
|
| 298 | +`click` 的介绍就告一段落,它将会是你编写命令行程序的一大利器。在下一篇文章中,我们依然会通过实现一个简单的 `git` 程序来进行 `click` 的实战。 |
0 commit comments