docopt:构造一个漂亮的命令行工具

1,675 阅读4分钟

最近拜读了覃超在知乎专栏的文章《谁说程序员不是潜力股?!让这位世界前五名的天才程序员来颠覆你的三观!》,受到了深深的一击。于是跪着爬进了Kenneth的Github,汲取点营养。

我先找了一个单份文件的小工具pip-pop看起(潜台词:没敢一开始就从requests干起,怕齁着了==)。这是一个用于分析requirements.txt文件的程序,挺简单的。其中引起我注意的是它用到的docopt。

docopt是一个命令行接口描述语言,用于定义命令行程序的各项参数,并且生成一个处理分析参数的分析器。

说明

先看一份官方文档中的例子:

"""Naval Fate.

Usage:
  naval_fate.py ship new ...
  naval_fate.py ship  move   [--speed=]
  naval_fate.py ship shoot  
  naval_fate.py mine (set|remove)   [--moored | --drifting]
  naval_fate.py (-h | --help)
  naval_fate.py --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.

"""
from docopt import docopt


if __name__ == '__main__':
    arguments = docopt(__doc__, version='Naval Fate 2.0')
    print(arguments)

首先,在Usage下定义了这个命令行工具(naval_fate.py)的6个使用模式,真正调用时一定要匹配到这6个的其中之一。

在每个模式中,<>包围的是位置参数,[]包围的是可选参数,()包围的是必选参数,|用于分割两个互斥的参数。省略号…用于表示格式为数组的参数。

其次,在Options下面是对参数的描述。参数与其描述之间用两个以上的空格分隔,如果参数有默认值的话,则在描述字符串之后,用[default: val]注明。

最后,使用docopt(__doc__, version=’Naval Fate 2.0’)生成传入参数组成的dict,dict的key就是上面描述中的参数字符串。

那么,arguments可能的形式如下:

{'--drifting': False,
 '--help': False,
 '--moored': False,
 '--speed': '10',
 '--version': False,
 '': ['abc'],
 '': '0',
 '': '0',
 'mine': False,
 'move': True,
 'new': False,
 'remove': False,
 'set': False,
 'ship': True,
 'shoot': False}

实践

我们用一个小程序实践一下。

在我的工作中,有这样一个目录:

- code
  - server
  - client
    - data
  - common
    - cdata
  - common_server
    -data

其中server、client、common、common_server下面都有python代码文件,而data、cdata目录下都是python数据文件。python数据文件由csv文件转化而来,数据量庞大。当我在code目录下需要grep某一个关键字时,往往会搜索所有的数据文件,耗时严重,其实我只想要在程序文件中搜索。

一般的,我们可以这样组合find命令和grep命令,实现这一功能:

find . \( -wholename ./client/data -prune \) -o \( -wholename ./common/cdata -prune \) -o \( -wholename ./common_server/data -prune \) -o -name "*.py" -print | xargs grep -n --color sth_you_want_to_grep

我们也可以写一个python程序,结合正则表达式re模块,实现这一功能。

分析我们这个小程序:

  1. 有两个参数必不可少,即搜索的根目录,以及所搜索的字符串(或者正则表达式)
  2. 一个可选参数,用于表明搜索过程中,强制跳过的目录序列
  3. 一个可选参数,用于表示是否忽略大小写

那么,设计出来的docopt描述字符串如下:

"""Usage:
    py-grep   [-i] [--ignorepath ...]
    py-grep (-h | --help)
Options:
    -h --help        Show this screen
    -i               Ignore case
    --ignore_paths   Ignored directories
"""

接下来,根据此字符串可以解析传递给py-grep程序的各项参数:

def main():
    args = docopt(__doc__, version="py-grep")

    if args['-i']:
        pattern = re.compile(args[''], re.IGNORECASE)
    else:
        pattern = re.compile(args[''])
    
    kwargs = {
        'search_path': args[''],
        'pattern': pattern,
        'ignore_paths': args[''],
    }
    py_grep(**kwargs)
    return

在py_grep函数中,我们将利用os.walk遍历目录下的文件,跳过ignore_paths,使用pattern对文件的每一行进行搜索。

def py_grep(search_path, pattern, ignore_paths=None):
    ignore_paths = ignore_paths if ignore_paths else []
    ignore_paths = [os.path.abspath(p) for p in ignore_paths]
    for parent, dirnames, filenames in os.walk(search_path):
        abs_parent = os.path.abspath(parent)
        is_ignore = False
        for ig_path in ignore_paths:
            if abs_parent.startswith(ig_path):
                is_ignore = True
                break
        if is_ignore:
            continue
        for fn in filenames:
            fn = os.path.join(parent, fn)
            with open(fn, 'r') as fobj:
                for n, line in enumerate(fobj):
                    if pattern.search(line):
                        print fn, n+1, ':', line.strip()
    return

在如下目录结构中尝试一下:

|-- example.sh
|-- py-grep
|-- test_file1
|-- test_path1
|   |-- inner_path
|   |   `-- test_file4
|   `-- test_file2
`-- test_path2
    `-- test_file3

输出如下:

./py-grep . "(\w)+@(\w)+((\.\w+)+)" --ignorepath ./test_path1
./test_file1 2 : My email is yubo1911@163.com.
./test_file1 4 : I have another email: usher@gmail.com
./test_path2/test_file3 2 : My email is yubo1911@163.com.
./test_path2/test_file3 4 : I have another email: usher@gmail.com
====================
./py-grep . "(\w)+@(\w)+((\.\w+)+)" --ignorepath test_path1
./test_file1 2 : My email is yubo1911@163.com.
./test_file1 4 : I have another email: usher@gmail.com
./test_path2/test_file3 2 : My email is yubo1911@163.com.
./test_path2/test_file3 4 : I have another email: usher@gmail.com

输出结果符合预期。

总结

docopt的简介就到这里了。更详细的信息,请参阅其官方文档

PS. 这里写的py-grep一定是有性能问题的,只用于熟悉docopt的用法,请不要将其用于日常工作中。

完整代码详见docopt

转载请注明出处: blog.guoyb.com/2016/09/26/…

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋