轻松构建Python命令行工具

自从玩 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

    • help 的作用其实很明显了,就是显示这份帮助信息
    • debug 的作用是是否显示调试信息
    • quiet 则是不显示所有的输出信息

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

    • 定义钩子
    • 定义扩展、日志、配置、插件....
    • 设置信号处理函数
    • 加载 扩展 和 handlers
    • 读取配置文件,设置日志等信息
    • 关闭应用
  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 看看。

· EOF ·

最近爬虫很是凶猛,标注下文章的原文地址: https://liuliqiang.info/post/build-python-cli-app-with-cement/