Kahn's blogs

使用Python的Plumbum库创建你自己的CLI工具箱

2018/07/06

前言

上周学习了Python的基本语法,本着学以致用的想法。就试着用Python写一个自己的命令行工具,把所有常用的命令,工具都放进来,需要用的时候就不用再去google,或者history了。

为什么学习Python

既然学习一门语言,就应该有个目标,我学python,不是把它当成主要的开发语言。而是把它当成辅助性脚本语言。需要的时候,可以写一些工具。没事还可以尝试一下网上现在很流行各种python爬虫和AI库。

命令行工具应该怎么写?是什么样的?

想写一个命令行工具,首先想到的是系统自带的shell,可是这个太low了,不考虑。而且我们的主要目的还是为了用python练手。

最简单的CLI应该是,在命令行中运行一个程序,接收一个参数,在程序中接收这个参数,进行一些运算,并返回。但是这样只能称之为一个可以命令行运行并接收参数的程序。

而我们常用的系统命令,ls,cd,mkdir都是这样的。而后加装的,比如android的adb。mac os上的brew

1
2
3
➜ MyUtils brew --version
Homebrew 1.6.9
Homebrew/homebrew-core (git revision 2f937f7; last commit 2018-07-02)

我们在使用第三方程序的时候,都知道它们肯定有help,或者version等等命令。其实这些都是在遵守一个规范,同时也方便大家使用。如果想详细了解这个规范请自行google POSIX规范

Plumbum

plumbum库是一个python库,帮助我们实现命令行程序,在这里我也用到了它。

官网写的很详细,用法也很多,我们这里只重点介绍CLI的部分。

使用

1
pip install plumbum

命令可以安装这个库,如果你使用的是Anaconda,可以使用下面的命令下载

1
conda install -c conda-forge plumbum

开始咯

基本信息设置

首先我们先新建一个类,继承刚刚提到的Plumbum模块包中的cli.Application

1
2
3
4
5
6
7
8
9
10
from plumbum import cli, colors
class DemoApp(cli.Application):
def main(self, *args):
pass
# 可以直接运行
if __name__ == '__main__':
DemoApp.run()

我们去命令行里试先试一下,应该是什么都不会有。

1
python3 cmddemo.py

其实这时候如果输入

1
python3 cmddemo.py -h 或者 python3 cmddemo.py --help

就会出现以下信息

1
2
3
4
5
6
7
Usage:
cmddemo.py [SWITCHES] args...
Meta-switches:
-h, --help Prints this help message and quits
--help-all Print help messages of all subcommands and quit
-v, --version Prints the program's version and quits

如果你想调用python3 cmddemo.py就显示这些帮助信息,可以在main中加入一句这样的代码:super().help()就可以了。

好的,帮助信息已经出来了,我们继续。

cli.Application类中为我们提供一些字段,可以直接显示信息,直接加上即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from plumbum import cli, colors
class DemoApp(cli.Application):
# 程序名称
PROGNAME = "kahn的工具包" | colors.green
VERSION = "1.0" | colors.blue
DESCRIPTION = "kahn的CLI集合"
USAGE = "demo"
def main(self, *args):
super().help()
pass
if __name__ == '__main__':
DemoApp.run()

我们再试一下命令,会输出一下信息

1
2
3
4
5
6
7
8
9
10
kahn的工具包 1.0
kahnCLI集合
Usage:
demo
Meta-switches:
-h, --help Prints this help message and quits
--help-all Print help messages of all subcommands and quit
-v, --version Prints the program's version and quits

是不是有点感觉了?

代码里有颜色的输出设置,详细配置可见官网。也可以不设置颜色

主参数设置

现在试着添加一条参数

1
2
3
4
5
6
"""上传下载命令集合"""
@cli.autoswitch(str)
def pull(self, param):
"""从git上拉取源代码"""
print("pull " + param)

上面的方法增加了一条主参数,我们再试一下命令,应该会显示

1
2
3
4
5
6
7
8
9
10
11
12
13
kahn的工具包 1.0
kahnCLI集合
Usage:
demo
Meta-switches:
-h, --help Prints this help message and quits
--help-all Print help messages of all subcommands and quit
-v, --version Prints the program's version and quits
Switches:
--pull PARAM:str 从git上拉取源代码

这时候可以把main里面的help删掉了,不然输入什么都会打出帮助信息。

pull命令可以不带值,把pull的参数,和@cli.autoswitch的参数去掉就可以。–pull的注释可以通过python commen语法编写

子命令

我们在使用命令行的时候,一般会使用子命令进行操作。比如 git pull,git branch。像pull就是子命令

在同一个文件中添加一个类

1
2
3
4
5
6
7
8
9
10
11
12
@DemoApp.subcommand("sync")
class KahnCmdSyncOpt(cli.Application):
"""上传下载命令集合"""
@cli.autoswitch()
def pull(self):
"""从git上拉取源代码"""
pass
@cli.autoswitch(str, argname="模块名")
def upload(self, args):
"""上传模块包。例:--upload=calendar"""
pass

再去输入python3 cmddemo.py -h命令试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kahn的工具包 1.0
kahnCLI集合
Usage:
demo
Meta-switches:
-h, --help Prints this help message and quits
--help-all Print help messages of all subcommands and quit
-v, --version Prints the program's version and quits
Switches:
--pull PARAM:str 从git上拉取源代码
Subcommands:
sync 上传下载命令集合; see 'kahn的工具包 sync --help' for
more info

子命令的帮助信息也出来了。这想看子命令的参数,需要输入python3 cmddemo.py sync -h

这里我已经设置好了两个子命令,一个pull,一个upload,输入python3 cmddemo.py sync -h看一下效果

1
2
3
4
5
6
7
8
9
10
11
Usage:
kahn的工具包 sync [SWITCHES] args...
Meta-switches:
-h, --help Prints this help message and quits
--help-all Print help messages of all subcommands and quit
-v, --version Prints the program's version and quits
Switches:
--pull 从git上拉取源代码
--upload 模块名:str 上传模块包。例:--upload=calendar

到这里基本设置已经ok了。

规划

在我规划的命令行工具的功能暂时先分两种

  1. 直接调用系统的shell命令执行
  2. 需要自己写逻辑的命令

先说一下1

调用系统命令

在python调用系统命令是很简单的,自行google就可以了,下面是代码示例

1
2
3
4
5
6
7
# 执行系统命令
def executeSystemCmd(workDir, cmd):
print("executing...[%s]" % cmd)
os.chdir(workDir)
result = os.popen(cmd)
for message in result.readlines():
print(message)

读取配置文件

我们需要一个配置文件存放写好的命令,这里可以使用Plumbum封装好的conf

1
2
3
4
5
6
# 初始化配置
with cli.Config('./conf/.myapp_rc') as conf:
_confSection = conf['confSection']
_sourceDir = conf['%s.sourceDir' % _confSection]
_mainAppDir = conf['%s.mainAppDir' % _confSection]
_package = conf['%s.package' % _confSection]

使用with cli.Config(‘./conf/.myapp_rc’) as conf:就可以从文件中读取一个标准的python配置文件,默认从[DEFAULT]这个section中读取。简单的说下python配置文件的样式

一般是这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
[DEFAULT]
confSection = test1
[test1]
package = com.test.kahn
sourceDir = /Users/kahn/Dev/test
mainAppDir = app
[test2]
package = com.test.kahn2
sourceDir = None
mainAppDir = None

我们使用conf[test1.package]来读取[test1]中的值,不写test1.默认从DEFAULT中读取

用一个配置文件存放写好的命令

1
2
3
4
5
6
7
8
9
[DEFAULT]
0 = buck(testDebug);buck install -r appZroTestDebug
1 = buck(testRealese);buck install -r appZroTestRealese
2 = buck(productDebug);buck install -r appZroProductDebug
4 = 切换到最新分支;python xxx.py
5 = 输出数据库;adb pull /data/data/%%s/databases/ ~/
6 = 获取当前页面名称;adb shell dumpsys activity | grep "mFocusedActivity"
7 = 清楚应用缓存;adb shell pm clear xxx
8 = 卸载xxx;adb uninstall xxx

接下来定义一个子命令cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@KahnUtils.subcommand("cmd")
class KahnCmdPredefine(cli.Application):
"""预定义命令集"""
# 读取命令配置
# 命令字典
cmdDict = {}
# 命令帮助字典
cmdDictHelp = {}
with cli.Config('./conf/.myapp_rc_cmd_conf') as cmdConf:
for key, option in cmdConf.parser.items("DEFAULT"):
optionArr = option.split(";")
cmdDictHelp[key] = optionArr[0]
cmdDict[key] = optionArr[1]
@cli.switch(
["-h", "--help"], group = "Meta-switches",
help="Prints this help message and quits")
def help(self):
super().help()
print("可输入的命令:")
for k,v in self.cmdDictHelp.items():
print("%s : %s" % (k, v))
print("示例:mt cmd 1")
def main(self, *args):
for key, option in self.cmdDict.items():
# print(option)
if args.__len__() == 1 and args[0] == key:
# 执行导出数据库操作要区分是哪个app
optionTemp = option
if key == "5":
optionTemp = (option % _package)
elif key == "13":
optionTemp = (option % _package)
else:
optionTemp = option
executeSystemCmd(os.path.join(_sourceDir, _mainAppDir), optionTemp)

在这个子命令中,我先读取了所有已配置的命令,然后重写了它的help参数方法,把命令配置文件的命令输出到help中

以便让用户了解到,该命令都可以执行哪些功能。执行python3 cmddemo.py cmd -h后大致输出是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
预定义命令集
Usage:
kahn的工具包 cmd [SWITCHES] args...
Meta-switches:
Prints this help message and quits
-h, --help Prints this help message and quits
--help-all Print help messages of all subcommands and quit
-v, --version Prints the program's version and quits
可输入的命令:
0 : buck(testDebug)
1 : buck(testRealese)
2 : buck(productDebug)
3 : buck(productRealse)
4 : 切换到最新分支
5 : 输出数据库
6 : 获取当前页面名称
7 : 清楚应用缓存
8 : 卸载孕期
9 : 批量打aar包
10 : 小工具模块打包
11 : 查看tcp接口
12 : 导出数据库
13 : monkey压力测试

在这个类中,我的main方法接收了一个*args参数,然后判断参数的个数,最后找到相应的命令,调用刚刚写好的executeSystemCmd去执行它。

做一个示范:调用python cmddemo.py cmd 1

输出了

1
2
executing...[buck install -r appZroTestRealese]
/bin/sh: buck: command not found

表示正在执行的命令。因为我系统没有buck命令,所以会输出/bin/sh: buck: command not found

再来调用一下python cmddemo.py -h看看输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kahn的工具包 1.0
kahnCLI集合
Usage:
demo
Meta-switches:
-h, --help Prints this help message and quits
--help-all Print help messages of all subcommands and quit
-v, --version Prints the program's version and quits
Switches:
--pull PARAM:str 从git上拉取源代码
Subcommands:
cmd 预定义命令集; see 'kahn的工具包 cmd --help' for more
info
sync 上传下载命令集合; see 'kahn的工具包 sync --help' for
more info

一个CLI的骨架就差不多搭好了。

安装

上面一个基本的命令行程序弄好了,那么怎么分发出去呢。总不能一直使用python cmddemo.py这种调用方式吧?而且执行的机器有可能没有安装Plumbum模块。

这里我们用到了setuptools模块。setuptools的使用方法,这里不展开讨论,大概配置是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from setuptools import setup
setup(
name='myutil',
version='1.0',
packages=['xxx'],
include_package_data=True,
install_requires=[
'plumbum'
],
entry_points='''
[console_scripts]
myutil = myutil:cli
''',
)

上面只是一个示例,详细的配置请自行google

有了setuptools,我们就可以分发出我们的python工具包了。

使用python setup.py install安装程序后。

你就可以在任何地方使用这样的命令调用了

1
myutil cmd 1

就像你的adb一样

而不用再,非要找到你的源码路径,再去执行下面这样的命令

1
python cmddemo.py

通过对setuptools的配置,它还可以帮你安装你所需要的依赖包

结尾

做为程序员的你,应该有一个自己的工具包

具体代码后面会更新在github上