利用argparse模块开发带复杂参数的命令行程序

目录

背景

最近看了一些 ecFlow 的文档,与目前我们正在用的 SMS 相比有不少优点,其中一点就是 ecFlow 提供一个统一的客户端命令行程序 ecflow_client,替代之前的一组 sms 命令

smsinit
smscomplete
smsabort
...

和 cdp 命令接口。之前的用过 cdp 接口,感觉虽然比 ecflow_client 的命令短,但很难在线获得帮助信息,有时解析失效,不太好用。ecflow_client 尽管需要输入更多的信息,但胜在有详细的帮助信息,总体上来说更加方便易用。
ecflow_client 类似 git 等命令行程序,将多个子命令组合到一起,通过选项区分。
git 命令举例:

git add
git clone
git commit
...

ecflow_client 命令举例:

ecflow_client --init
ecflow_client --complete
ecflow_client --abort
...

我对这种命令行程序设计方式很感兴趣,就想到模拟 ecflow_client,为 SMS 创建一个简单的客户端接口,实现我平时用到的几个 cdp 命令功能:加载 def 文件、替换节点、删除节点。

需求

一个客户端程序,根据选项实现不同的子命令功能,每个子命令可能有不同的命令行参数,也有通用的命令行参数。

sms_client load
sms_client replace
sms_client delete

实现

Python 的 argparse 模块提供构建复杂命令行参数的功能,可以满足上述需求。

通用参数

SMS 客户端命令需要登录到 SMS 服务器后才能运行,所以所有的子命令都需要 SMS 服务器主机名和用户名这两个参数。
argparse 提供共享参数解析器的机制,在创建解析器时可以设置 parents 解析器列表,列表中每个解析器的参数都会添加到新创建的解析器中。所以先创建主机和用户名的解析器。

login_parser = argparse.ArgumentParser(add_help=False)
login_parser.add_argument("-n", "--name", help="sms host name")
login_parser.add_argument("-u", "--user", help="sms user name")

注意,不要引入重复参数,所以要设置 {py}add_help=False{/py},防止通用解析器自动添加 help 参数。

子命令

sms_client 以不同的子命令提供不同的功能,argparse 模块通过 ArgumentParser.add_subparsers 支持子命令。
创建 sms_client 的参数解析器

parser = argparse.ArgumentParser(description="SMS client tool.")

添加子命令解析器

subparsers = parser.add_subparsers(title='sub commands',  dest='sub_command')

用户选用某种子命令后,该子命令名会保存在 dest 属性设置的变量中,也就是 sub_command 变量中,后续程序可以根据 sub_command 的值执行不同的操作。
添加不同的子命令

# delete
parser_delete = subparsers.add_parser('delete',
                                      parents=[login_parser],
                                      description="Delete node(s) given. Same to cancel(cdp) in sms."
                                                  "By default, user -y option in cdp commands.")
parser_delete.add_argument('node', help='The name of the node to be deleted')
# load
parser_load = subparsers.add_parser("load",
                                    parents=[login_parser],
                                    description="Define, validate and send the suites to the SMS. "
                                                "Same to play(cdp) in sms.")
parser_load.add_argument('def_file', help='The name of the file that contain the definitions.')
# replace
parser_replace = subparsers.add_parser("replace",
                                       parents=[login_parser],
                                       description="Replace the node given in the SMS")
parser_replace.add_argument('node',
                            help='path to node. must exist in the client definition. '
                                 'This is also the node we want to replace in the server')
parser_replace.add_argument('def_file', help='path to client definition file. '
                                             'provides the definition of the new node')

参数解析

使用 parse_args() 解析参数,返回解析后的结果。

args = parser.parse_args()

根据子命令执行对应的操作

if args.sub_command == 'delete':
    delete_handler(args.node, name=args.name, user=args.user)
elif args.sub_command == 'load':
    load_handler(args.def_file, name=args.name, user=args.user)
elif args.sub_command == 'replace':
    replace_handler(args.node, args.def_file, name=args.name, user=args.user)

运行示例

命令帮助

$ sms_client.py --help
usage: sms_client.py [-h] {delete,load,replace} ...
SMS client tool.
optional arguments:
  -h, --help            show this help message and exit
sub commands:
  {delete,load,replace}

子命令帮助

$ sms_client.py replace --help
usage: sms_client.py replace [-h] [-n NAME] [-u USER] node def_file
Replace the node given in the SMS
positional arguments:
  node                  path to node. must exist in the client definition.
                        This is also the node we want to replace in the server
  def_file              path to client definition file. provides the
                        definition of the new node
optional arguments:
  -h, --help            show this help message and exit
  -n NAME, --name NAME  sms host name
  -u USER, --user USER  sms user name