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


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

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

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

说明

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""Naval Fate.

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

Options:
-h --help Show this screen.
--version Show version.
--speed=<kn> 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可能的形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{'--drifting': False,
'--help': False,
'--moored': False,
'--speed': '10',
'--version': False,
'<name>': ['abc'],
'<x>': '0',
'<y>': '0',
'mine': False,
'move': True,
'new': False,
'remove': False,
'set': False,
'ship': True,
'shoot': False}

实践

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

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

1
2
3
4
5
6
7
8
- code
- server
- client
- data
- common
- cdata
- common_server
-data

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

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

1
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描述字符串如下:

1
2
3
4
5
6
7
8
"""Usage:
py-grep <searchpath> <pattern> [-i] [--ignorepath <igpaths>...]
py-grep (-h | --help)
Options:
-h --help Show this screen
-i Ignore case
--ignore_paths Ignored directories
"""

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def main():
args = docopt(__doc__, version="py-grep")

if args['-i']:
pattern = re.compile(args['<pattern>'], re.IGNORECASE)
else:
pattern = re.compile(args['<pattern>'])

kwargs = {
'search_path': args['<searchpath>'],
'pattern': pattern,
'ignore_paths': args['<igpaths>'],
}
py_grep(**kwargs)
return

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

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

1
2
3
4
5
6
7
8
9
|-- example.sh
|-- py-grep
|-- test_file1
|-- test_path1
| |-- inner_path
| | `-- test_file4
| `-- test_file2
`-- test_path2
`-- test_file3

输出如下:

1
2
3
4
5
6
7
8
9
10
11
./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

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

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

评论