Python 命令行工具:fire (已验证)

Fire介绍

fire 可以根据任何 Python 对象自动生成命令行接口。它有如下特性:

  • 能以简单的方式生成 CLI
  • 是一个开发和调试 Python 代码的实用工具
  • 能将现存代码或别人的代码转换为 CLI
  • 使得在 Bash 和 Python 间的转换变得更容易
  • 通过预先为 REPL 设置所需的模块和变量,使得实用 REPL 更加容易

安装

pip install fire

调用方法

函数调用

import firedef hello(name="World"):  return 'Hello {name}!'.format(name=name)if __name__ == '__main__':  fire.Fire(hello)

在上述例子中定义一个 hello 函数,它接受 name 参数,并且有默认值 “World”。使用 fire.Fire(hello) 即可非常简单快速地实现命令功能,这个命令行就接受 --name 选项,不提供时使用默认值 “World”,提供时就按提供的值来。

可在命令行中执行下列命令:

$ python hello.pyHello World!$ python hello.py --name=XjHello Xj!$ python hello.py --helpINFO: Showing help with the command 'hello.py -- --help'.NAME    hello.pySYNOPSIS    hello.py FLAGS    --name=NAME

隐式使用 fire.Fire()

实现子命令最简单的方式就是定义若干个函数,每个函数名隐式就是子命令名称,然后调用 fire.Fire() 变将当前模块所有的函数解析为对应的子命令的处理函数。

import firedef add(x, y):  return x + ydef multiply(x, y):  return x * yif __name__ == '__main__':  fire.Fire()

然后我们就可以在命令行中这么调用。

$ python example.py add 10 2030$ python example.py multiply 10 20200

显式使用 fire.Fire()

有时我们可能只想把部分函数当做子命令,或者是希望子命令名称和函数名称不一样。这个时候我们就可以通过字典对象显式地告诉 fire

字典对象的形式为 {'子命令名称': 函数},比如前面的示例中,我们希望最终的子命令为 add2mul2,那么就可以这么写:

fire.Fire({  'add2': add,  'mul2': multiply,})

然后我们就可以在命令行中这么调用。

$ python example.py add2 10 2030$ python example.py mul2 10 20200 

类调用

使用函数是最简单的方式,如果我们想以更有组织的方式来实现,比如使用类,fire 也是支持的。

import fireclass Calculator(object):  """A simple calculator class."""  def double(self, number):    return 2 * number  def triple(self, number):    return 3 * numberif __name__ == '__main__':  fire.Fire(Calculator)

在上述例子中定义一个 Calculator 类,它有两个实例方法 doubletriple,并且都接受 number 参数,没有默认值。使用 fire.Fire(Calculator) 即可非常简单快速地实现命令功能,这个命令行支持两个子命令 doubletriple,位置参数 NUMBER 或选项参数 --number

可在命令行中执行下列命令:

$ python calculator.py double 1020$ python calculator.py triple --number=1545$ python calculator.py double --helpINFO: Showing help with the command 'calculator.py double -- --help'.NAME    calculator.py doubleSYNOPSIS    calculator.py double NUMBERPOSITIONAL ARGUMENTS    NUMBERNOTES    You can also use flags syntax for POSITIONAL ARGUMENTS

实例化的对象使用 fire.Fire()

将类实例化,并把实例化的对象作为 fire.Fire 的入参:

import fireclass Calculator(object):  def add(self, x, y):    return x + y  def multiply(self, x, y):    return x * yif __name__ == '__main__':  calculator = Calculator()  fire.Fire(calculator)

类使用 fire.Fire()

这里把类而非实例对象作为 fire.Fire 的入参:

fire.Fire(Calculator)

传递类和实例对象的基本作用是一样的,但传递类还有一个额外的特性:如果构造函数中定义了参数,那么这些参数都会作为整个命令行程序的选项参数。

import fireclass BrokenCalculator(object):  def __init__(self, offset=1):      self._offset = offset  def add(self, x, y):    return x + y + self._offset  def multiply(self, x, y):    return x * y + self._offsetif __name__ == '__main__':  fire.Fire(BrokenCalculator)

查看帮助命令有:

$ python example.py --helpINFO: Showing help with the command 'example.py -- --help'.NAME    example.pySYNOPSIS    example.py FLAGS    --offset=OFFSET

由此可见构造函数 BrokenCalculator.__init__(self, offset=1) 中的 offset 自动转换为了命令行中的全局选项参数 --offset,且默认值为 1

我们可以在命令行中这么调用:

$ python example.py add 10 2031$ python example.py multiply 10 20201$ python example.py add 10 20 --offset=030$ python example.py multiply 10 20 --offset=0200

命令组/嵌套命令

想要实现嵌套命令,可将多个类组织起来,示例如下:

import fireclass IngestionStage(object):  def run(self):    print('Ingesting! Nom nom nom...')class DigestionStage(object):  def run(self, volume=1):    print(' '.join(['Burp!'] * volume))  def status(self):    return 'Satiated.'class Pipeline(object):  def __init__(self):    self.ingestion = IngestionStage()    self.digestion = DigestionStage()  def run(self):    self.ingestion.run()    self.digestion.run()    print('pipeline is runing.')if __name__ == '__main__':  fire.Fire(Pipeline)

在上面的示例中:

  • IngestionStage 实现了子命令 run
  • DigestionStage 实现了子命令 runstatus
  • Pipeline 的构造函数中将 IngestionStage 实例化为 ingestion,将 DigestionStage 实例化为 digestion,就将这两个放到一个命令组中,因而支持了:
  • ingestion run
  • digestion run
  • digestion status
  • Pipeline 实现了子命令 run

因此整个命令行程序支持如下命令:

  • run
  • ingestion run
  • digestion run
  • digestion status

然后我们就可以在命令行中这么调用:

$ python example.py runIngesting! Nom nom nom...Burp!pipeline is runing.$ python example.py ingestion runIngesting! Nom nom nom...$ python example.py digestion runBurp!$ python example.py digestion statusSatiated.

属性访问

属性访问fire 相对于其他命令行库来说一个比较独特的功能。所谓访问属性是获取预置的属性所对应的值。

举个例子,在命令行中指定 --code 来告知程序要查询的程序编码,然后希望通过 zipcode 属性返回邮编,通过 city 属性返回城市名。那么属性可实现为实例成员属性:

import firecities = {  'hz': (310000, '杭州'),  'bj': (100000, '北京'),}class City(object):  def __init__(self, code):    info = cities.get(code)    self.zipcode = info[0] if info else None    self.city = info[1] if info else Noneif __name__ == '__main__':  fire.Fire(City)

使用方式如下:

$ python example.py --code bj zipcode100000$ python example.py --code hz city杭州

链式调用

Fire CLI 中,你可以通过链式调用不断地对上一个结果进行处理。

想做到这一点也很简单,就是在实例方法中返回 self 即可。

在下面的示例中,我们实现了一个简单的四则运算命令,可链式调用 addsubmuldiv

import fireclass Calculator:  def __init__(self):    self.result = 0    self.express = '0'  def __str__(self):    return f'{self.express} = {self.result}'  def add(self, x):    self.result += x    self.express = f'{self.express}+{x}'    return self  def sub(self, x):    self.result -= x    self.express = f'{self.express}-{x}'    return self  def mul(self, x):    self.result *= x    self.express = f'({self.express})*{x}'    return self  def div(self, x):    self.result /= x    self.express = f'({self.express})/{x}'    return selfif __name__ == '__main__':  fire.Fire(Calculator)

上述代码中的 addsubmuldiv 分别对应加、减、乘、除的逻辑,每个方法都接受 x 参数作为参与运算的数字,返回值均为 self,这样就可以无限次地链式调用。在命令行中链式调用结束后,会最终调用到 __str__ 方法将结果打印出来。

其中,__str__fire 中用来完成自定义序列化。如果不提供这个方法,在链式调用完成后将会打印帮助内容。

比如,我们可以这么调用:

$ python calculator.py add 1 sub 2 mul 3 div 4((+1-2)*3)/4 = -0.75$ python calculator.py add 1 sub 2 mul 3 div 4 add 4 sub 3 mul 2 div 1((((0+1-2)*3)/4+4-3)*2)/1 = 0.5

参数

位置参数和选项参数

import fireclass Building(object):  def __init__(self, name, stories=1):    self.name = name    self.stories = stories  def __str__(self):    return f'name: {self.name}, stories: {self.stories}'  def climb_stairs(self, stairs_per_story=10):    yield self.name    for story in range(self.stories):      for stair in range(1, stairs_per_story):        yield stair      yield 'Phew!'    yield 'Done!'if __name__ == '__main__':  fire.Fire(Building)

构造函数中定义的参数(如 namestories)在命令行中仅为选项参数(如 --name--stories)。我们可以这么调用:

$ python example.py --name="Sherrerd Hall" --stories=3name: Sherrerd Hall, stories: 3

构造函数中定义的参数可在命令中放于任意位置。比如下面两个调用都是可以的。

$ python example.py --name="Sherrerd Hall" climb-stairs --stairs-per-story 10Sherrerd Hall123456789Phew!Done!
$ python example.py climb-stairs --stairs-per-story 10 --name="Sherrerd Hall"Sherrerd Hall123456789Phew!Done!

构造函数和普通方法中定义的默认参数(如 stories),在命令行中是可选的。我们可以这么调用

$ python example.py --name="Sherrerd Hall"

普通方法中定义的参数(如 stairs_per_story)在命令行中即可以是位置参数,也可以是选项参数。我们可以这么调用:

# 作为位置参数$ python example.py --name="Sherrerd Hall" climb_stairs 10# 作为选项参数$ python example.py --name="Sherrerd Hall" climb_stairs --stairs_per_story=10

此外,fire 还支持在函数中定义 *args**kwargs

import firedef fargs(*args):  return str(args)def fkwargs(**kwargs):  return str(kwargs)if __name__ == '__main__':  fire.Fire()

函数中的 *args 在命令行中为位置参数。我们可以这么调用

$ python example.py fargs a b c('a', 'b', 'c')

函数中的 **kwargs 在命令行中为选项参数。我们可以这么调用

python example.py fkwargs --a a1 --b b2 --c c3 {'a': 'a1', 'b': 'b2', 'c': 'c3'}

通过分隔符 - 可显式告知分隔符后的为子命令,而非命令的参数。且看下面的示例:

# 没有使用分隔符,upper 被作为位置参数$ python example.py fargs a b c upper('a', 'b', 'c', 'upper')# 使用了分隔符,upper 被作为子命令$ python example.py fargs a b c - upper('A', 'B', 'C')

通过 fire 内置的 --separator 可以自定义分隔符,此选项参数需要跟在单独的 -- 后面

$ python example.py fargs a b c X upper -- --separator=X ('A', 'B', 'C')

参数类型

fire 中,参数的类型由其值决定,通过下面的简单代码,我们可以看到给不同的值时,fire会解析为什么类型.

import firefire.Fire(lambda obj: type(obj).__name__)$ python example.py 10int$ python example.py 10.0float$ python example.py hellostr$ python example.py '(1,2)'tuple$ python example.py [1,2]list$ python example.py Truebool$ python example.py {name: David}dict

如果想传递字符串形式的数字,那就需要小心引号了,要么把引号引起来,要么转义引号

# 数字 10$ python example.py 10int# 没有对引号处理,仍然是数字10$ python example.py "10"int# 把引号引起来,所以是字符串“10”$ python example.py '"10"'str# 另一种把引号引起来的形式$ python example.py "'10'"str# 转义引号$ python example.py \"10\"str

考虑下更复杂的场景,如果传递的是字典,在字典中有字符串,那么也是要小心引号的

# 推荐做法$ python example.py '{"name": "David Bieber"}'dict# 也是可以的$ python example.py {"name":'"David Bieber"'}dict# 错误,会被解析为字符串$ python example.py {"name":"David Bieber"}str# 错误,不会作为单个参数(因为中间有空格),报错$ python example.py {"name": "David Bieber"}

如果值为 TrueFalse 将为视为布尔值,fire 还支持通过 --namename 设为 True,或通过 --nonamename 设为 False

$ python example.py --obj=Truebool$ python example.py --obj=Falsebool$ python example.py --objbool$ python example.py --noobjbool

Fire 内置选项参数

Fire 内置了一些选项参数,以帮助我们更容易地使用命令行程序。若想使用内置的选项功能,需要将选项参数跟在 -- 后,在上文中,我们介绍了 --separator 参数,除了它,fire 还支持以下选项参数:

  • command -- --help 列出详细的帮助信息
  • command -- --interactive 进入交互式模式
  • command -- --completion [shell] 生成 CLI 程序的自动补全脚本,以支持自动补全
  • command -- --trace 获取命令的 Fire 追踪以了解调用 Fire 后究竟发生了什么
  • command -- --verbose 获取包含私有成员在内的详情

注:源文为HelloGitHub公众号的《Google 开源的 Python 命令行库:深入 fire》,已整理、验证和润色。

您的关注,是我的无限动力!
公众号 @生活处处有BUG

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章