今天收到 weekly 的邮件,其中有一篇文章就是将如何良好得组织一个 python 命令行应用,讲得不错很简单也很直接,但是,我觉得仅仅讲一下如何组织文件结构还不够,还可以再进一步,再介绍一下如何处理命令行参数,所以也就有了这篇文章。

首先,我也先来套路一番,讲一下 python 命令行应用的组织结构,通常 python 如果要安装的话都会带一个 setup.py 文件,这个文件可以将对应的 python 安装到运行的查找库中,或者安装到系统的命令查找目录中,这些都依赖于你怎么写,至于怎么写,有兴趣可以参考一下我之前写过的一篇文章:python打包发布工具

文件组织结构

一个通用的命令行应用的目录结构正常是这样的:

  1. foo/
  2. ├── foo/
  3. ├── classmodule.py
  4. ├── funcmodule.py
  5. ├── __init__.py
  6. └── __main__.py
  7. ├── LICENSE
  8. ├── README.md
  9. └── setup.py
  1. 首先是一个大的目录,表示一个这是一个项目,应该有常写 python 代码的都比较熟悉
  2. 然后是真正的应用目录,里面就存放真正的应用代码
  3. LICENSE 和 README.md 都是提供给使用者看的,没有特别说的
  4. setup.py 就是安装和卸载应用使用的啦

这里除了 2 和 4 这两点需要讲解一下之外,另外两点可以不细说,那就先从 2 说起。

init 文件

这个文件在 Python 中很通用,表示这是一个模块,据说在 Python3.4 版本之后就不需要这个文件了,我一直在使用着 python2.7,短期内没有转向 python3 的需要和需求,所以没细探究。因为是只是表示模块而已,所以里面根本不需要放置代码,直接保持一个空文件即可。

main 文件

这个文件很重要了,因为我们将把它当做整个应用的入口,也就是我们的应用将会在这里开始被启动起来,调试的时候可以以它为启动文件进行调试,同时,发布的时候在 setup.py 中也是将这个文件设置为应用入口。

既然是应用的入口,那么我们和应用有关但是又不是强相关的内容就可以放置到这了,例如参数解析之类的。这里我就以我个人阅读优秀库的源码结合个人的理解,介绍一下如何编写这个 __main__ 模块比较合适。

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import sys
  4. from .classmodule import FooCommand
  5. def main():
  6. argv = sys.argv
  7. cmd = FooCommand()
  8. cmd.execute_from_commandline(argv)
  9. if __name__ == '__main__':
  10. main()

可以发现其实这里就从 classmodule 中导入了一个 FooCommand 的类,然后执行一下 execute_from_commandline 方法,这里需要说明的是,其实这个 FooCommand 是继承自 Command 基类来的,为什么要这么设计?假设我们这是一个封装的网络工具包,那么我们可能一个应用程序支持很多子命令:

  1. foo ping www.google.com
  2. foo telnet www.google.com

对于每个自命令我们的参数的含义都是不一样的,那是不是我们都要重新写一次所有的逻辑?事实上,那么多参数和方法,不一样的可能也就执行的逻辑代码解析参数的代码,其他代码都可以抽象成一个框架,这样就有意思了,假如设计是这样的:

图 1:整体结构

这里给大家解释一下这个 Command 基类中各个方法的含义:

通过这些方法,我们可以看到,一半以上的方法都是可以重用的,也就是说对于一个新命令,我们仅仅只需要覆盖一些方法即可。

classmodule.py

接下来是时候看下真正的代码是怎样的了,就从基类 Command 开始吧。

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import os
  5. import random
  6. import sys
  7. class Command(object):
  8. args = ''
  9. supports_args = True
  10. option_list = ()
  11. doc = None
  12. respects_app_option = True
  13. prog_name = 'foo'
  14. def __init__(self):
  15. pass
  16. def run(self, *args, **options):
  17. raise NotImplementedError('subclass responsibility')
  18. def __call__(self, *args, **kwargs):
  19. random.seed() # maybe we were forked.
  20. self.verify_args(args)
  21. try:
  22. ret = self.run(*args, **kwargs)
  23. return ret if ret is not None else 'OK'
  24. except self.UsageError as exc:
  25. self.on_usage_error(exc)
  26. return exc.status
  27. except self.Error as exc:
  28. self.on_error(exc)
  29. return exc.status
  30. def verify_args(self, given, _index=0):
  31. pass
  32. def execute_from_commandline(self, argv=None):
  33. if argv is None:
  34. argv = list(sys.argv)
  35. self.prog_name = os.path.basename(argv[0])
  36. return self.handle_argv(self.prog_name, argv[1:])
  37. def run_from_argv(self, prog_name, argv=None, command=None):
  38. return self.handle_argv(prog_name,
  39. sys.argv if argv is None else argv, command)
  40. def usage(self, command):
  41. return '%prog {0} [options] {self.args}'.format(command, self=self)
  42. def get_options(self):
  43. return self.option_list
  44. def handle_argv(self, prog_name, argv, command=None):
  45. options, args = self.prepare_args(
  46. *self.parse_options(prog_name, argv, command))
  47. return self(*args, **options)
  48. def prepare_args(self, options, args):
  49. pass
  50. def check_args(self, args):
  51. pass
  52. def error(self, s):
  53. pass
  54. def die(self, msg, status='EX_FAILURE'):
  55. self.error(msg)
  56. sys.exit(status)
  57. def parse_options(self, prog_name, arguments, command=None):
  58. self.parser = self.create_parser(prog_name, command)
  59. return self.parser.parse_args(arguments)
  60. def create_parser(self, prog_name, command=None):
  61. pass
  62. def prepare_parser(self, parser):
  63. pass
  64. def parse_preload_options(self, args):
  65. pass
  66. def preparse_options(self, args, options):
  67. pass
  68. @property
  69. def colored(self):
  70. if self._colored is None:
  71. self._colored = term.colored(enabled=not self.no_color)
  72. return self._colored
  73. @colored.setter
  74. def colored(self, obj):
  75. self._colored = obj
  76. @property
  77. def no_color(self):
  78. return self._no_color
  79. @no_color.setter
  80. def no_color(self, value):
  81. self._no_color = value
  82. if self._colored is not None:
  83. self._colored.enabled = not self._no_color

这里为了简约就去掉了一些方法的实现,具体的实现思路讲给大家听就好了,按照这个方法继续继承其他的 Command 并且填写需要实现的方法,这样就把逻辑代码给填补好了。

funcmodule.py

这个文件在我们这个sample 中没有使用到,作用呢主要用于定义各种函数,例如我们要使用的辅助函数啊之类的都可以放在这里。

setup.py

是时候来讲一下这个文件了,因为这个文件关系着我们的应用要怎么使用,告诉 Python 如何处理这个应用。我就给个简单点的实现吧,如果有需要的话,还是前面说的,参考一下 setup.py 的各个参数的意思去填:

  1. from setuptools import setup
  2. setup(
  3. name = 'foo',
  4. version = '0.1.0',
  5. packages = ['foo'],
  6. entry_points = {
  7. 'console_scripts': [
  8. 'foo = foo.__main__:main'
  9. ]
  10. })

很熟悉的套路,关键就在于 console_scripts,这个 key 的值就是我们要抛出的应用名字,当我们执行 python setup.py install 时,那么安装在系统的目录中的命令行应用应该就是 foo 了,然后我们也就可以使用 foo 命令了。

Summary

讲道理,当我们有一些繁琐的重复性工作的时候,命令行应用还是非常赞的;或者说一些我们常用的脚本,将它丰富一下,封装成应用也是很好。我们用 C 编写的话还不方便,还要编译安装之类的,也繁琐一些,而 python 的快速开发,方便修改也给我们提供了很大的方便。

如果你觉得本文的示例有点意思的话,我在 Github 上编写一个完整的可运行的 Demo,你可以来试试:项目地址

Reference