|
| 1 | +# Python 命令行之旅:深入 fire(二) |
| 2 | + |
| 3 | +## 一、前言 |
| 4 | + |
| 5 | +在上一篇文章中我们介绍了 `fire` 的子命令、嵌套命令和属性访问等内容,今天我们将继续深入了解 `fire` 的其他功能。 |
| 6 | + |
| 7 | +``` |
| 8 | +本系列文章默认使用 Python 3 作为解释器进行讲解。 |
| 9 | +若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~ |
| 10 | +``` |
| 11 | + |
| 12 | +## 二、功能 |
| 13 | + |
| 14 | +### 2.1 最简命令实现 |
| 15 | + |
| 16 | +在上一节中,我们介绍了只要定义一个函数就可以实现命令行程序。比如: |
| 17 | + |
| 18 | +```python |
| 19 | +import fire |
| 20 | + |
| 21 | +def english(): |
| 22 | + return 'Hello, fire!' |
| 23 | + |
| 24 | +def chinese(): |
| 25 | + return '你好,fire!' |
| 26 | + |
| 27 | +if __name__ == '__main__': |
| 28 | + fire.Fire() |
| 29 | +``` |
| 30 | + |
| 31 | +但这还不是最简单的实现方式,`fire` 甚至允许你通过定义变量的方式来实现命令行! |
| 32 | +上面的例子可以写成下面这种形式: |
| 33 | + |
| 34 | +```python |
| 35 | +import fire |
| 36 | + |
| 37 | +english = 'Hello, fire!' |
| 38 | +chinese = '你好,fire!' |
| 39 | + |
| 40 | +if __name__ == '__main__': |
| 41 | + fire.Fire() |
| 42 | +``` |
| 43 | + |
| 44 | +### 2.2 链式调用 |
| 45 | + |
| 46 | +在 `Fire CLI` 中,你可以通过链式调用不断地对上一个结果进行处理。 |
| 47 | + |
| 48 | +想做到这一点也很简单,就是在实例方法中返回 `self` 即可。 |
| 49 | + |
| 50 | +在下面的示例中,我们实现了一个简单的四则运算命令,可链式调用 `add`、`sub`、`mul` 和 `div`。 |
| 51 | + |
| 52 | +```python |
| 53 | +import fire |
| 54 | + |
| 55 | +class Calculator: |
| 56 | + |
| 57 | + def __init__(self): |
| 58 | + self.result = 0 |
| 59 | + self.express = '0' |
| 60 | + |
| 61 | + def __str__(self): |
| 62 | + return f'{self.express} = {self.result}' |
| 63 | + |
| 64 | + def add(self, x): |
| 65 | + self.result += x |
| 66 | + self.express = f'{self.express}+{x}' |
| 67 | + return self |
| 68 | + |
| 69 | + def sub(self, x): |
| 70 | + self.result -= x |
| 71 | + self.express = f'{self.express}-{x}' |
| 72 | + return self |
| 73 | + |
| 74 | + def mul(self, x): |
| 75 | + self.result *= x |
| 76 | + self.express = f'({self.express})*{x}' |
| 77 | + return self |
| 78 | + |
| 79 | + def div(self, x): |
| 80 | + self.result /= x |
| 81 | + self.express = f'({self.express})/{x}' |
| 82 | + return self |
| 83 | + |
| 84 | +if __name__ == '__main__': |
| 85 | + fire.Fire(Calculator) |
| 86 | +``` |
| 87 | + |
| 88 | +上述代码中的 `add`、`sub`、`mul`、`div` 分别对应加、减、乘、除的逻辑,每个方法都接受 `x` 参数作为参与运算的数字,返回值均为 `self`,这样就可以无限次地链式调用。在命令行中链式调用结束后,会最终调用到 `__str__` 方法将结果打印出来。 |
| 89 | + |
| 90 | +其中,`__str__` 在 `fire` 中用来完成自定义序列化。如果不提供这个方法,在链式调用完成后将会打印帮助内容。 |
| 91 | + |
| 92 | +比如,我们可以这么调用: |
| 93 | + |
| 94 | +```bash |
| 95 | +$ python calculator.py add 1 sub 2 mul 3 div 4 |
| 96 | +((+1-2)*3)/4 = -0.75 |
| 97 | +
|
| 98 | +$ python calculator.py add 1 sub 2 mul 3 div 4 add 4 sub 3 mul 2 div 1 |
| 99 | +((((0+1-2)*3)/4+4-3)*2)/1 = 0.5 |
| 100 | +``` |
| 101 | +
|
| 102 | +### 2.3 位置参数和选项参数 |
| 103 | +
|
| 104 | +通过前面的介绍我们也都清楚了在 `fire` 中不必显式的定义位置参数或选项参数。 |
| 105 | +
|
| 106 | +通过下面的例子,我们将细化两类参数的使用: |
| 107 | +
|
| 108 | +```python |
| 109 | +import fire |
| 110 | +
|
| 111 | +class Building(object): |
| 112 | +
|
| 113 | + def __init__(self, name, stories=1): |
| 114 | + self.name = name |
| 115 | + self.stories = stories |
| 116 | +
|
| 117 | + def __str__(self): |
| 118 | + return f'name: {self.name}, stories: {self.stories}' |
| 119 | +
|
| 120 | + def climb_stairs(self, stairs_per_story=10): |
| 121 | + yield self.name |
| 122 | + for story in range(self.stories): |
| 123 | + for stair in range(1, stairs_per_story): |
| 124 | + yield stair |
| 125 | + yield 'Phew!' |
| 126 | + yield 'Done!' |
| 127 | +
|
| 128 | +if __name__ == '__main__': |
| 129 | + fire.Fire(Building) |
| 130 | +``` |
| 131 | +
|
| 132 | +- 构造函数中定义的参数(如 `name` 和 `stories`)在命令行中仅为选项参数(如 `--name` 和 `--stories`)。我们可以这么调用: |
| 133 | +
|
| 134 | +```bash |
| 135 | +$ python example.py --name="Sherrerd Hall" --stories=3 |
| 136 | +``` |
| 137 | +
|
| 138 | +- 构造函数中定义的参数可在命令中放于任意位置。比如下面两个调用都是可以的: |
| 139 | +
|
| 140 | +```bash |
| 141 | +$ python example.py --name="Sherrerd Hall" climb-stairs --stairs-per-story 10 |
| 142 | +$ python example.py climb-stairs --stairs-per-story 10 --name="Sherrerd Hall" |
| 143 | +``` |
| 144 | +
|
| 145 | +- 构造函数和普通方法中定义的默认参数(如 `stories`),在命令行中是可选的。我们可以这么调用: |
| 146 | +
|
| 147 | +```bash |
| 148 | +$ python example.py --name="Sherrerd Hall" |
| 149 | +``` |
| 150 | +
|
| 151 | +- 普通方法中定义的参数(如 `stairs_per_story`)在命令行中即可以是位置参数,也可以是选项参数。我们可以这么调用: |
| 152 | +
|
| 153 | +```bash |
| 154 | +# 作为位置参数 |
| 155 | +$ python example.py --name="Sherrerd Hall" climb_stairs 10 |
| 156 | +# 作为选项参数 |
| 157 | +$ python example.py --name="Sherrerd Hall" climb_stairs --stairs_per_story=10 |
| 158 | +``` |
| 159 | +
|
| 160 | +- 选项参数中的横杠(`-`)和下划线(`_`)是等价的。因此也可以这么调用: |
| 161 | +
|
| 162 | +```bash |
| 163 | +# 作为选项参数 |
| 164 | +$ python example.py --name="Sherrerd Hall" climb_stairs --stairs-per-story=10 |
| 165 | +``` |
| 166 | +
|
| 167 | +此外,`fire` 还支持在函数中定义 `*args` 和 `**kwargs`。 |
| 168 | +
|
| 169 | +```python |
| 170 | +import fire |
| 171 | +
|
| 172 | +def fargs(*args): |
| 173 | + return str(args) |
| 174 | +
|
| 175 | +
|
| 176 | +def fkwargs(**kwargs): |
| 177 | + return str(kwargs) |
| 178 | +
|
| 179 | +if __name__ == '__main__': |
| 180 | + fire.Fire() |
| 181 | +``` |
| 182 | +
|
| 183 | +- 函数中的 `*args` 在命令行中为位置参数。我们可以这么调用: |
| 184 | +
|
| 185 | +```bash |
| 186 | +$ python example.py fargs a b c |
| 187 | +``` |
| 188 | +
|
| 189 | +- 函数中的 `**kwargs` 在命令行中为选项参数。我们可以这么调用: |
| 190 | +
|
| 191 | +```bash |
| 192 | +$ python example.py fargs --a a1 --b b1 --c c1 |
| 193 | +``` |
| 194 | +
|
| 195 | +- 通过分隔符 `-` 可显式告知分隔符后的为子命令,而非命令的参数。且看下面的示例: |
| 196 | +
|
| 197 | +```bash |
| 198 | +# 没有使用分隔符,upper 被作为位置参数 |
| 199 | +$ python example.py fargs a b c upper |
| 200 | +('a', 'b', 'c', 'upper') |
| 201 | +
|
| 202 | +# 使用了分隔符,upper 被作为子命令 |
| 203 | +$ python example.py fargs a b c - upper |
| 204 | +('A', 'B', 'C') |
| 205 | +``` |
| 206 | +
|
| 207 | +- 通过 `fire` 内置的 `--separator` 可以自定义分隔符,此选项参数需要跟在单独的 `--` 后面: |
| 208 | +
|
| 209 | +```bash |
| 210 | +$ python example.py a b c X upper -- --separator=X |
| 211 | +('A', 'B', 'C') |
| 212 | +``` |
| 213 | +
|
| 214 | +### 2.4 参数类型 |
| 215 | +
|
| 216 | +在 `fire` 中,参数的类型由其值决定,通过下面的简单代码,我们可以看到给不同的值时,`fire`会解析为什么类型: |
| 217 | +
|
| 218 | +```python |
| 219 | +import fire |
| 220 | +fire.Fire(lambda obj: type(obj).__name__) |
| 221 | +``` |
| 222 | +
|
| 223 | +```bash |
| 224 | +$ python example.py 10 |
| 225 | +int |
| 226 | +$ python example.py 10.0 |
| 227 | +float |
| 228 | +$ python example.py hello |
| 229 | +str |
| 230 | +$ python example.py '(1,2)' |
| 231 | +tuple |
| 232 | +$ python example.py [1,2] |
| 233 | +list |
| 234 | +$ python example.py True |
| 235 | +bool |
| 236 | +$ python example.py {name: David} |
| 237 | +dict |
| 238 | +``` |
| 239 | +
|
| 240 | +如果想传递字符串形式的数字,那就需要小心引号了,要么把引号引起来,要么转义引号: |
| 241 | +
|
| 242 | +```bash |
| 243 | +# 数字 10 |
| 244 | +$ python example.py 10 |
| 245 | +int |
| 246 | +# 没有对引号处理,仍然是数字10 |
| 247 | +$ python example.py "10" |
| 248 | +int |
| 249 | +# 把引号引起来,所以是字符串“10” |
| 250 | +$ python example.py '"10"' |
| 251 | +str |
| 252 | +# 另一种把引号引起来的形式 |
| 253 | +$ python example.py "'10'" |
| 254 | +str |
| 255 | +# 转义引号 |
| 256 | +$ python example.py \"10\" |
| 257 | +str |
| 258 | +``` |
| 259 | +
|
| 260 | +考虑下更复杂的场景,如果传递的是字典,在字典中有字符串,那么也是要小心引号的: |
| 261 | +
|
| 262 | +```bash |
| 263 | +# 推荐做法 |
| 264 | +$ python example.py '{"name": "David Bieber"}' |
| 265 | +dict |
| 266 | +# 也是可以的 |
| 267 | +$ python example.py {"name":'"David Bieber"'} |
| 268 | +dict |
| 269 | +# 错误,会被解析为字符串 |
| 270 | +$ python example.py {"name":"David Bieber"} |
| 271 | +str |
| 272 | +# 错误,不会作为单个参数(因为中间有空格),报错 |
| 273 | +$ python example.py {"name": "David Bieber"} |
| 274 | +<error> |
| 275 | +``` |
| 276 | +
|
| 277 | +如果值为 `True` 或 `False` 将为视为布尔值,`fire` 还支持通过 `--name` 将 `name` 设为 `True`,或通过 `--noname` 将 `name` 设为 `False`: |
| 278 | +
|
| 279 | +```bash |
| 280 | +$ python example.py --obj=True |
| 281 | +bool |
| 282 | +$ python example.py --obj=False |
| 283 | +bool |
| 284 | +$ python example.py --obj |
| 285 | +bool |
| 286 | +$ python example.py --noobj |
| 287 | +bool |
| 288 | +``` |
| 289 | +
|
| 290 | +### 2.5 Fire 内置选项参数 |
| 291 | +
|
| 292 | +Fire 内置了一些选项参数,以帮助我们更容易地使用命令行程序。若想使用内置的选项功能,需要将选项参数跟在 `--` 后,在上文中,我们介绍了 `--separator` 参数,除了它,`fire` 还支持以下选项参数: |
| 293 | +
|
| 294 | +- `command -- --help` 列出详细的帮助信息 |
| 295 | +- `command -- --interactive` 进入交互式模式 |
| 296 | +- `command -- --completion [shell]` 生成 CLI 程序的自动补全脚本,以支持自动补全 |
| 297 | +- `command -- --trace` 获取命令的 Fire 追踪以了解调用 Fire 后究竟发生了什么 |
| 298 | +- `command -- --verbose` 获取包含私有成员在内的详情 |
| 299 | +
|
| 300 | +## 三、小结 |
| 301 | +
|
| 302 | +`fire` 让命令行程序的实现变得特别简单,本文着重介绍了它的链式调用、选项参数、位置参数、参数类型以及内置选项参数。`fire` 的概念并不多,真正践行了“把简单留给他人,把复杂留给自己”的理念。 |
| 303 | +
|
| 304 | +`fire` 的介绍就告一段落,它绝对会是你编写命令行程序的一大利器。在下一篇文章中,我们依然会通过实现一个简单的 `git` 程序来进行 `fire` 的实战。 |
0 commit comments