概述

自从玩 Python 之后,除了平时工作用的编写 Web/Rest 应用程序之外,工作中也有不少地方会用 Python 来完成,例如一些工作脚本(之前有一个很火的帖子讲述一个外国小伙超过30秒的工作都写脚本完成),甚至于平时生活中的一些数据分析我都会用 Python 来完成。但是,很多脚本都是一次性的,写多了之后就会发现很多重复的功能,除了写成模块复用之外,其实我觉得放在一个命令行应用程序中也是不错的想法(我之前也写过一篇命令行程序的设计: python创建结构良好的命令行应用,但是,这就需要加很多命令行参数来控制了。

在 Python 中,以前很少关于命令行的库,原生的比较老的有 options 等,但是都比较难用,直到后来出现了 argparse,被比较多人使用,变得流行起来。同时,在使用 Flask 的时候,有个扩展叫做 Flask-Script 也很不错,不过只能用于 Flask 的扩展,而在 Flask 0.11 版本发布的时候,顺带发布了 Click 这个命令行库,我觉得这就是用来替换 Flask-Script 的,也渐渐流行起来了。但是,这对于我的需求来说,还是有点低层,我可能更需要的是一个命令行框架,一直没有找到合适的,直到我看到了 Cement 这个库,我觉得这就是我想要的。

Cement 简介

Cement 是一个 Python 的命令行框架,它功能强大,不仅仅可以用于编写小脚本,甚至你可以用它来编写大型的命令行工具。同时,Cement 也是有强大的扩展性,通过 handlerinterface,你可以很轻松得定制命令行功能,后面我将会给大家介绍这两个功能。

虽然 Cement 功能非常强大,但是安装却也是非常简单,直接使用 pip 安装就好了:

pip install cement

安装完之后,我们就可以来试试怎么玩 Cement 这个框架了。

1. 一个简单的应用

对于一个简单的应用,我们需要这么编写:

from cement.core.foundation import CementApp

app = CementApp('helloworld')
app.setup()
app.run()
print('Hello World')
app.close()

可能这里有点奇怪,为什么写个命令行程序还要操作那么多?其实认真看一下,其实也比较明了,可以看成这里做了几件事情:

  1. 程序运行前预处理
  2. 运行命令行程序
  3. 程序运行后清理

这样看的话觉得就很顺理成章了,所以,为了简化我们不关心的部分,这段代码还可以这么写:

from cement.core.foundation import CementApp

with CementApp('helloworld') as app:
    app.run()
    print('Hello World')

清晰多了!

2. 默认参数

我们将上面两段代码中的任一段都行,保存为 python 脚本,然后加上参数 -h 看看,例如:

$ python helloworld.py -h
usage: helloworld [-h] [--debug] [--quiet]

optional arguments:
-h, --help  show this help message and exit
--debug     toggle debug output
--quiet     suppress all output

可以看到 Cement 为我们默认添加了 3 个参数选项,分别为 helpdebugquiet

debug 选项默认是关闭的,如果你打开 debug 参数,你会发现 Cement 的大概执行过程:

  1. 定义钩子
  2. 定义扩展、日志、配置、插件….
  3. 设置信号处理函数
  4. 加载 扩展 和 handlers
  5. 读取配置文件,设置日志等信息
  6. 关闭应用

3. 自定义参数

刚才说到 Cement 有 3 个默认参数,那么我们是否可以添加自己的参数选项?答案是显然的啦,不然怎么有人会用这个框架呢?怎么加参数也是很简单的,例如这里给程序添加一个 foo 参数选项:

from cement.core.foundation import CementApp

with CementApp("arg-test") as app:
    app.args.add_argument('-f', '--foo', action='store', metavar='STR',
        help='the notorious foo option')
    app.log.debug("About to run my myapp application!")
    app.run()
    if app.pargs.foo:
        app.log.info("Received option: foo => %s" % app.pargs.foo)

可以看到这里添加一个参数和 argparse 差不多,基本上就是继承 argparse 的写法。然后使用也很简单,直接从 app 里面的 pargs 参数中获取就可以了。

4. 添加命令

用惯了 Django 的同学可能都会很习惯 python manage.py runserver 这种形式的命令,这在 Cement 中也是非常容易实现的,例如:

from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose

class MyBaseController(CementBaseController):
    class Meta:
        label = 'base'
        stacked_on = 'base'

    @expose(help="this command does relatively nothing useful")
    def command1(self):
        self.app.log.info("Inside MyBaseController.command1()")

    class MySecondController(CementBaseController):
        class Meta:
            label = 'second'
            stacked_on = 'base'

        @expose(help='this is some command', aliases=['some-cmd'])
        def second_cmd1(self):
            self.app.log.info("Inside MySecondController.second_cmd1")

    class MyApp(CementApp):
        class Meta:
            label = 'myapp'
            base_controller = 'base'
            handlers = [MyBaseController, MySecondController]

    with MyApp() as app:
        app.run()

这里就很轻松得定义了两个命令,而且可以是在不同的 Controller 上,其实可以理解成划分到两个不同的模块上。但是,有点蛋疼的是,必须制定 Meta 信息,不指定还不行。同时,我因为用惯了 Flask-Script,所以我习惯用二级命令,例如 python manage.py db init 这种。虽然 Cement 也可以实现,但是实在是有点麻烦,所以这是做的不好的地方。

总结

本文以 Cement 的简介以及个人的简单使用为例,给大家介绍了这款不错的命令行框架,我个人在生活和工作中几乎都用它来编写命令行程序,非常使用。当然,正如文中所说,并非所有特性都是支持得很好,有些个人习惯的特性支持还是比较差的,我也在考虑在业余时间是否考虑修改一下提个 PR 看看。