• python制作命令行工具——fire


    前言

    本篇教程的目的是希望大家可以通读完此篇之后,可以使用python制作一款符合自己需求的linux工具。

    本教程使用的是google开源的python第三方库:fire

    无论是学生党自己做着练手,还是工作中确有需求,本篇都尽可能通过简单的例子来示范该第三方库的用法,其中若有描述不当的地方,望留言指出。


    一、快速介绍

    来一波官方介绍。

    • Python Fire是一个库,用于从任何Python对象自动生成命令行接口。
    • 是用python创建CLI的一种简单方法。
    • 是开发和调试Python代码的一个有用工具。
    • Python Fire帮助探索现有代码或将其他人的代码转换为CLI。
    • 使得Bash和Python之间的转换更加容易。
    • 通过使用已经导入和创建的模块和变量来设置REPL, Python Fire使使用Python REPL变得更容易。

    没听懂 ???

    不是太明白 ???

    不要紧,看完本篇就懂了。


    二、快速安装

    • pip安装:pip install fire
    • conda安装:conda install fire -c conda-forge
    • 源码安装:
    1. git clone https://github.com/google/python-fire.git
    2. cd python-fire
    3. python setup.py install
    

    Github地址:python-fire


    三、快速上手

    实践出真知

    创建一个test.py文件,写入以下内容

    import fire
    
    def test(your_var="default_value"):
    	return 'This is a test ! value : %s' % your_var
    
    if __name__ == '__main__':
        fire.Fire(test)
    

    咱们来看一下效果

    # 缺省参数
    root@node:~# python test.py 
    This is a test ! value : default_value
    
    # 关键字参数
    root@node:~# python test.py --your_var=newValue
    This is a test ! value : newValue
    
    # 位置参数
    root@node:~# python test.py localtionValue
    This is a test ! value : localtionValue
    

    现在呢,我们反过头来看一下官方介绍的第一行:

    Python Fire是一个库,用于从任何Python对象自动生成命令行接口。

    注意关键字:任何python对象。这意味着什么?

    我们来看一段代码:

    import fire
    
    boy_name = 'XiaoMing'
    girl_name = 'XiaoHong'
    
    if __name__ == '__main__':
      fire.Fire()
    

    试一下:python test.py boy_name

    是不是明白了些什么。

    聊完这缺省参数关键字参数位置参数,当然不能少了 *args 和 ** kwargs .

    还是来看代码示例:

    import fire
    
    def run(*args):
        arg_list = list(args)
        return ' | '.join(arg_list)
    
    if __name__ == '__main__':
      fire.Fire(run)
    

    跑一下就懂啦

    root@node:~# python test.py run qwe rty uio asd fgh
    qwe | rty | uio | asd | fgh
    

    官方给的示例是这个样子的~~~

    import fire
    
    def order_by_length(*items):
      """Orders items by length, breaking ties alphabetically."""
      sorted_items = sorted(items, key=lambda item: (len(str(item)), str(item)))
      return ' '.join(sorted_items)
    
    if __name__ == '__main__':
      fire.Fire(order_by_length)
    

    就是加了个长度和字母顺序的排序,来跑一下,看一下效果:

    $ python example.py dog cat elephant
    cat dog elephant
    

    除此之外呢,我们还可以给输出结果加点料,还是刚才我们写的那个例子:

    root@node:~# python test.py run qwe rty uio asd fgh - upper
    QWE | RTY | UIO | ASD | FGH
    

    在这里,我们通过命令行对传入的对象和调用结果执行相同的操作,譬如这里的 upper

    敲黑板划重点:分隔符 “ - ” 之后的所有参数都将用于处理函数的结果,而不是传递给函数本身。默认的分隔符是连字符 “ - ”。

    默认的分隔符也是可以改的,用到了fire的内置参数。

    root@node:~# python test.py run qwe rty uio asd fgh X upper -- --separator=X
    QWE | RTY | UIO | ASD | FGH
    

    其中的separator就是fire的一个内置参数,更多内置参数文末有提到。

    我们再来看一下fire给我们提供的命令行传参时,数据的类型。比较特殊的是,fire根据值决定类型。

    import fire
    fire.Fire(lambda obj: type(obj).__name__)
    

    如果有刚学python的小伙伴,记得一定要学一下lambda函数,在这里我可以转化为普通写法。

    import fire
    
    def test(obj):
        return type(obj).__name__
    
    if __name__ == '__main__':
        fire.Fire(test)
    

    通过简单的一行代码来看一下各种数据类型如何通过命令行传参:

    $ python example.py 10
    int
    $ python example.py 10.0
    float
    $ python example.py hello
    str
    $ python example.py '(1,2)'
    tuple
    $ python example.py [1,2]
    list
    $ python example.py True
    bool
    $ python example.py {name:David}
    dict
    

    但是当你想传递一个str类型的10,你就要注意了,看以下例子:

    $ python example.py 10
    int
    $ python example.py "10"
    int
    $ python example.py '"10"'
    str
    $ python example.py "'10'"
    str
    $ python example.py "10"
    str
    

    我们可以看到,你虽然敲了"10",但是依然被判定为int,bash会自动处理掉你参数的第一层引号。所以,如果想传str类型的10,要再加一层引号,单双引号分开用,或者把引号转义。

    如果要传的是dict参数,那就更要小心谨慎了。

    # 标准写法
    $ 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"}  # Wrong. This isn't even treated as a single argument.
    <error>
    

    到这里,我想大家应该大概明白了 fire 的方便快捷之处。

    到了这一步的时候,虽然实现了基本功能,但还是和平时我们使用的 linux 命令行工具有很大的区别:

    1. 每次跑命令都要再敲一个python

    2. 每次还要指向指定的py文件或到指定的目录下

      首先说第一个问题,每次多敲六个字母和一个空格,作为一个linux命令行工具是非常不合格的,本来命令行工具就在追求简单化,这种指定解释器的操作我们当然要尽可能省掉咯

      第二个问题,老是指定文件的目录就更麻烦了,日常使用的时候在不同的服务器跑命令还要想想放在哪里,而且如果使用绝对路径的话,更会导致命令的冗长。

    下面我们来解决一下这两个“小”问题:

    1. 在文件的第一行指定python解释器,这样就无需在我们运行该文件时再指定解释器
    #!/usr/bin/python
    
    import fire
    
    def test(your_var="default_value"):
    	return 'This is a test ! value : %s' % your_var
    
    if __name__ == '__main__':
        fire.Fire(test)
    
    1. 增加文件的可执行权限
    root@node:~# chmod +x test.py
    
    1. 美化以下,去掉小尾巴(仅仅是给文件改了个名字, 这一步非必须)
    root@node:~# mv test.py mytool
    
    1. 做个软连接,可以随时随地找得到该命令
    root@node:~# ln -s /root/mytool /usr/bin/mytool
    

    附:如果需要指定编码的话,可以在文件头部加一行,比如

    #!/usr/bin/python
    # coding: utf-8
    

    这个时候,我们随意在服务器的任意位置执行

    root@node:~# mytool
    This is a test ! value : default_value
    
    root@node:~# mytool --your_var=newValue
    This is a test ! value : newValue
    
    root@node:~# mytool localtionValue
    This is a test ! value : localtionValue
    

    Perfection !

    如果你已经走到这一步的话,其实已经能写很多简单的命令行工具了。

    为什么说简单呢,目前都是使用函数来完成一个个命令的逻辑,多一个子命令多写一个函数,慢慢的就会让这个文件变的庞杂和冗余。而且久而久之,肯定会出现一些看起来很相似,却要使用ctrl + c-v大法去完成的事情。甚至有一些逻辑,想要实现还要自己去做更复杂的逻辑。


    四、快速进阶

    此时,一年级的已经可以下课了,二年级的请注意听讲了,下面,我们要讲的是:

    类的使用

    命令嵌套

    属性访问

    链式调用

    4.1 类的使用

    通过一个简单的算数类来了解其用法,在下列用法中,我们在fire中注册了一个类对象。

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

    以下是调用测试

    $ python example.py add 10 20
    30
    $ python example.py multiply 10 20
    200
    

    当然我们也可以注册一个类。

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

    跑一下看看:

    $ python example.py add 10 20
    30
    $ python example.py multiply 10 20
    200
    

    就这?当然不会,我们还可以通过参数控制实例属性,就像下面的例子:

    import fire
    
    class 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._offset
    
    if __name__ == '__main__':
      fire.Fire(BrokenCalculator)
    

    我们可以看到,新增了一个offset的实例属性,缺省值是1.

    $ python example.py add 10 20
    31
    $ python example.py multiply 10 20
    201
    

    重点来了,我们可以直接给属性赋值,以此来增加你命令行工具的自由度。

    $ python example.py add 10 20 --offset=0
    30
    $ python example.py multiply 10 20 --offset=0
    200
    

    4.2 命令嵌套

    通过不同的类来控制某些同名命令,其实也是将各个命令分门别类更具条理性的管理。可以看到以下用法。

    import fire
    
    class Sing:
        def run(self):
            print('sing sing sing ...')
    
    class Dance:
        def run(self):
            print('dance dance dance ...')
    
        def status(self):
            print('Around.')
    
    
    class Pipeline:
        def __init__(self):
            self.sing = Sing()
            self.dance = Dance()
    
        def run(self):
            self.sing.run()
            self.dance.run()
            self.dance.status()
    
    if __name__ == '__main__':
        fire.Fire(Pipeline)
    

    跑跑看:

    # python3 ball.py run
    sing sing sing ...
    dance dance dance ...
    Around.
    # python3 ball.py sing run
    sing sing sing ...
    # python3 ball.py dance run
    dance dance dance ...
    # python3 ball.py dance status
    Around.
    

    根据自定义的一个Pipeline类,我们可以自己组合想要的命令行效果,给子命令再分配不同的子集。

    4.3 属性访问

    其实前面说到类的时候已经简单的说过属性访问(就是那个offset的例子,行啦,忘了就不用往上翻了),这里再详细举例说明一下。

    # python3 test.py --age=6 outinfo
    Xiao Ming is 6 years old and in the First grade
    
    # python3 test.py --age=7 outinfo
    Xiao Ming is 7 years old and in the Second grade
    
    # python3 test.py --age=8 outinfo
    Xiao Ming is 8 years old and in the Third grade
    
    

    综上,我们可以通过控制类的属性来构造类对象。

    唠到这儿了,再唠一个骚操作

    4.4 链式调用

    官方给的例子不太好看,没有那么让人一眼就看懂的感觉,找了个四则运算的简单示例:

    import fire
    
    class 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 self
    
    if __name__ == '__main__':
      fire.Fire(Calculator)
    

    函数名呢,addsubmuldiv分别对应 加、减、乘、除四则运算,每个方法都接受 x 参数去运算,返回self,这样不论往后链式调用多少次都可以,结束调用到 __str__ 打印出结果。

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

    # python3 test.py add 2 sub 1.5 mul 3 div 2
    ((0+2-1.5)*3)/2 = 0.75
    
    # python3 test.py add 4 sub 2.5 mul 2 div 4 mul 3 sub 5 add 2
    (((0+4-2.5)*2)/4)*3-5+2 = -0.75
    

    看完这个大家应该明白链式调用的运用了,这个时候再来看一下官方示例也许会轻松一些。

    import fire
    
    class BinaryCanvas(object):
      """A canvas with which to make binary art, one bit at a time."""
    
      def __init__(self, size=10):
        self.pixels = [[0] * size for _ in range(size)]
        self._size = size
        self._row = 0  # The row of the cursor.
        self._col = 0  # The column of the cursor.
    
      def __str__(self):
        return '
    '.join(' '.join(str(pixel) for pixel in row) for row in self.pixels)
    
      def show(self):
        print(self)
        return self
    
      def move(self, row, col):
        self._row = row % self._size
        self._col = col % self._size
        return self
    
      def on(self):
        return self.set(1)
    
      def off(self):
        return self.set(0)
    
      def set(self, value):
        self.pixels[self._row][self._col] = value
        return self
    
    if __name__ == '__main__':
      fire.Fire(BinaryCanvas)
    

    跑一下看看:

    $ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 1 0 0 1 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 1 0 0 1 0 0 0
    0 0 0 0 1 1 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    

    PS:我要不说,谁能看出来这是个笑脸???


    最后一课

    最后看看官方给出的 fire 的内置参数吧,具体怎么应用大家就自己研究咯。

    Flags
    Using a CLI Command Notes
    Help command -- --help 显示命令的帮助和使用信息。
    REPL command -- --interactive 进入交互模式。
    Separator command -- --separator=X 这将分隔符设置为' X '。默认分隔符是“-”。
    Completion command -- --completion [shell] 为CLI生成一个补全的shell脚本。
    Trace command -- --trace 跟踪fire命令调用后发生了啥子。
    Verbose command -- --verbose 在输出中包含私有成员。

    fire-GitHub地址

  • 相关阅读:
    Gentoo 使用genkernel之后,修改了kernel,重新编译kernel和生成initramfs
    如何让git自动在commit message中加入你需要的sob?
    Gentoo:请安装bashcompletion package
    Kernel开发 SubmittingPatches,有关ifdef和static inline & macro
    Kernel开发 Thunderbird配置 From kernel documentation
    Gentoo 添加gentoozh overlay
    linux把文件压缩成.tar.gz的命令
    redhat下装ftp服务器(vsftpd)
    GDB调试精粹及使用实例
    linux中常用的头文件
  • 原文地址:https://www.cnblogs.com/shu-sheng/p/14133641.html
Copyright © 2020-2023  润新知