0%

Python 解析命令行参数

使用 Python 来做一些运维类的脚本工作的时候,少不了要读取命令行参数来控制脚本的工作逻辑。一般情况下我们直接的使用 sys 模块的 sys.argv 就可以得到命令行参数的列表(其中 sys.argv[0] 是脚本名)。

但是用 sys 不好的地方在于不够灵活,对于多个参数只能依顺序传入,可以说是严格按照 位置 来确定参数的。当然啦如果是配合管道或者 xargs 使用其实也很合适,但没有办法按照 命名 来指定参数确实比较原始。

这里所说的命令行的命名参数形如 -o output.txt,其中 -o 是命名参数中的命名(也可以理解为指定),output.txt 是命名参数中的参数(这个不一定需要)。

Python 在标准库中也包含了两个相关的库 getopt 以及 argparseoptparse 在 2.7 后已经被弃用),但是基本上找到的说明都比较模糊,用法不太明确,所以在这里简单 mark 下自己过去的用法以备查看。

一、 getopt

getopt 模块是 python 的标准库之一,对于位置参数可以简单的返回参数列表,它的改进是可以简单地解析带 --- 格式的参数。

其核心使用函数如下:

1
getopt.getopt(args, options[, long_options])

这个函数主要是创建一个解释器,然后套用到命令行参数上解释它。根据 help 方法的解释,其中:

参数 说明
args 指定需要被解析的参数列表,通常情况下,它意味着 sys.argv[1:]
shortopts 是由那些想要被脚本识别的可选字母(可选字母作为命名参数的命名)组成的字符串,如 -o 中的 o-v 中的 v。对于需要在 命令行参数 中实现 -o <outputfile> 这样字母选项后面跟随入参的,要在 定义 的字母后面加上 :,如 o:这个格式和 Unix 下的 getopt() 函数是一致的
long_options 是可选的,它是为了提供形如 -- 开头的字符串作为命名参数命名的支持。long_options 是一个字符串列表。类似于 shortopts,如果需要在命名后跟随入参的,要在 定义 的字符串后面加上 =

函数返回两个值,其中:

  1. 第一个是由形如 (option, value) 形式组成的元组列表;其中返回的 option 是带有前缀 --- 的;value 的值为字符串,也有可能为空字符串(对于命名参数没有入参的情况)。
  2. 第二个是把可以识别的命名参数(选项型参数)剥离出来之后剩下的参数列表

1. 实例

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
# coding: utf-8

import sys
import getopt

def main(argv):
input_file = ''
output_path = ''
try:
opts, args = getopt.getopt(argv, "hvi:o:", ["input=", "output="])
except getopt.GetoptError:
print("usage: python %s -i <inputfile> -o <outputpath> [-h] [-v]" % __file__)
sys.exit(2)

for opt, arg in opts:
if opt == "-h":
print("usage: python %s -i <inputfile> -o <outputpath> [-h] [-v]" % __file__)
elif opt == "-v":
print("version 1.0")
if opt in ("-i", "--input"):
input_file = arg
if opt in ("-o", "--output"):
output_path = arg

if input_file:
print("输入文件名为: %s" % input_file)
if output_path:
print("输出路径为: %s" % output_path)
if args:
print("其余入参为: %s" % ','.join(args))


if __name__ == '__main__':
argv = sys.argv[1:]
main(argv)

在 shell 中运行以下命令:

1
2
3
4
5
6
7
8
9
10
# 1. 查看帮助信息
$ python3 tim.py -h
usage: python tim.py -i <inputfile> -o <outputpath> [-h] [-v]
# 2. 完整测试
$ python3 tim.py -i input.json -o .
输入文件名为: input.json
输出路径为: .
# 3. ※ 异常处理
$ python3 tim.py -i -o
输入文件名为: -o # 这里需要我们特别在程序中判断处理

2. 稍微总结一下

可以看到 getopt 的使用相当简洁。但问题是我们要特别留心处理额外的情况;其次,我们对于参数的用途以及命令用法等基本上都需要自己手动给出提示信息和说明,搬砖工作,不太优雅。可想而知作为参数解释器来说,getopt 还是比较简陋的。因此我们有别的选择:argparse

二、 argparse

argparse 模块也是由 Python 标准库中提供出来的,同样它的主要用法也是一个解释器,去解释给到的 args 命令行参数。听上去和 getopt 没有区别啊,也是定义一个解释器(就是自己定义一些解析规则)去套命令行参数就完事了。但是我们看一下模块介绍就觉得不一般:

1
2
3
4
5
This module is an optparse-inspired command-line parsing library that:(是基于 optparse 的命令行解释库)

- handles both optional and positional arguments(支持命名参数/选项型参数以及位置参数)
- produces highly informative usage messages(提供丰富的使用信息说明)
- supports parsers that dispatch to sub-parsers(支持子解析器的分发)

哦,好像把之前 getopt 总结出来的不足都解决掉了,那么它到底是怎么做到的呢?关键在于,它的这个解释器的定义,就远比 getopt 定义的复杂的得多。

1. ArgumentParser

一开始,使用 argparse 也必须定义出一个解释器,使用 argparse.ArgumentParser 方法,得到一个用于将命令行参数字符串转变为 Python 对象的对象(是有点拗口)。

1
argparse.ArgumentParser(prog=None, usage=None, description=None, epilog=None, parents=[], formatter_class=<class 'argparse.HelpFormatter'>, prefix_chars='-', fromfile_prefix_chars=None, argument_default=None, conflict_handler='error', add_help=True, allow_abbrev=True)

简直让人头皮发麻的入参列表……但是我们一般用不着指定那么多,大部分人都是无参调用,充其量指定一下 description(描述一下这个脚本或程序是干什么的) 和 usage(使用说明,但一般不会在这里写而是在具体选项定义),奇葩一点的操作大概是改一下 prog(程序名,默认是取 sys.argv[0])或者 prefix_chars(默认的命名参数的命名都以 - 开头)。

到此为止我们也只是定义了一个只有描述的解释器,那么怎么定义我们的解释规则呢?重点来了,add_argument 函数 ——

2. add_argument

先从定义入手:

1
2
3
4
# add_argument 的 signature 信息
add_argument(self, *args, **kwargs)
add_argument(dest, ..., name=value, ...)
add_argument(option_string, option_string, ..., name=value, ...)

help 得到的看上去平平无奇还有些简陋,不怕,我们去看文档库,果然得到的 signature 信息就具体多了:

1
ArgumentParser.add_argument(name or flags...[, action][, nargs][, const][, default][, type][, choices][, required][, help][, metavar][, dest])

这里的话,不得不慢慢说一下这些参数代表的含义了:

参数 说明
name or flags 指定这个命名参数(或选项型参数)的命名或者标识 (Either a name or a list of option strings, e.g. foo or -f, –foo.)
action 指定一些基本的 action 来处理这个参数 (The basic type of action to be taken when this argument is encountered at the command line.)
nargs 一个数值,用来指定命名参数的命名后面跟随的 nargs 个字符串作为命名参数的值看待 (The number of command-line arguments that should be consumed.)
const action 或者 nargs 选择项保留一个常量(A constant value required by some action and nargs selections.)
default 如果这个命名参数的值为空,使用这个默认值 (The value produced if the argument is absent from the command line.)
type 指定这个命令行参数的值的处理类型 (The type to which the command-line argument should be converted.)
choices 这个参数的可选值容器,可以理解为枚举列表啦 (A container of the allowable values for the argument.)
required 指定这个参数是否是不可省略的 —— 仅针对命名参数而言 (Whether or not the command-line option may be omitted (optionals only).)
help 一个关于这个参数是干什么用的基本描述 (A brief description of what the argument does.)
metavar 指定参数在帮助信息中要显示的名称 (A name for the argument in usage messages.)
dest 指定通过 parse_args 获取值的时候所要使用的名称 (The name of the attribute to be added to the object returned by parse_args().)

大部分的内容都可以从这份参数解释表得出,我们尝试利用我们的理解去实现一些例子 ——

① 示例:指定位置参数

1
2
3
4
5
6
7
8
9
10
11
12
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("-v", "--version")
>>> parser.add_argument("name")
_StoreAction(option_strings=[], dest='name', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)
>>> args = parser.parse_args(["Tom"])
>>> args.name
'Tom'
>>> parser.add_argument("echo")
>>> parser.add_argument("-v", "--version")
>>> args = parser.parse_args(["Tom", "TextNode"])
>>> args
Namespace(echo='TextNode', name='Tom', version=None)

如上所示,对于不是以 - 前缀开头的所有的 name or flag,ArgumentParser 都默认的把他们当成 位置参数 依次对应赋值。

② 示例:指定命名参数

1
2
3
4
5
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("-v", "--version", action="store_true", help="print the version of program")
>>> parser.add_argument("-d", "--depth", type=int, choices=[1, 2, 3], help="specify the depth of level what the program should handle", default=3)
>>> parser.add_argument("-o", "--output-path", nargs="+", help="specify the output-paths that the program should use to output")
>>> parser.add_argument("echo")

定义了如上所示的一个解释器,让我们去执行一下这个命令行解释器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. 打印帮助信息
>>> parser.parse_args(["-h"])
usage: [-h] [-v] [-d {1,2,3}] [-o OUTPUT_PATH [OUTPUT_PATH ...]] echo

positional arguments:
echo

optional arguments:
-h, --help show this help message and exit
-v, --version print the version of program
-d {1,2,3}, --depth {1,2,3}
specify the depth of level what the program should
handle
-o OUTPUT_PATH [OUTPUT_PATH ...], --output-path OUTPUT_PATH [OUTPUT_PATH ...]
specify the output-paths that the program should use
to output

# 2. 形如 python xx.py -v -o . Tom 调用
>>> parser.parse_args(["Tom", "-v", "-o ."])
Namespace(depth=3, echo='Tom', output_path=[' .'], version=True)
>>>

注意:

  1. 对于之前那种命名参数类型只有命名没有值的情况(即如 add_argument("-v", "--version") 这种),当命令行中没有对应的入参,在 Namespance 上反馈的是 version=None。如果像上面那样定义了 action="store_true",则会改为以 bool 类型来表示。
  2. choices 可以提供一个可选值的列表,如果对应的命名参数类型的值不在列表中就会报错,弹出帮助提示(比 getopt 省心)
  3. default 可以指定默认值(针对命名参数类型而言)。
  4. nargs 的值可以是整数,也可以是 ?*+,类似于正则表达式,? 表示匹配后面 0 到 1 个字符串作为命名参数的值,* 表示匹配命名后面 0 到 n 个字符串作为命名参数的值,+ 表示匹配后面 1 到 n 个字符串作为命名参数的值。

③ 互斥参数实现

使用 ArgumentParser.add_mutually_exclusive_group 可以实现一组互斥的入参,具体如下所示:

1
2
3
4
5
>>> parser = argparse.ArgumentParser()
>>> group = parser.add_mutually_exclusive_group() # 定义一个互斥参数组
# 分别在组内添加互斥的参数,必须加上 action="store_true"
>>> group.add_argument('-a', '--Apple', help='consume apples', action='store_true')
>>> group.add_argument('-b', '--Banana', help='consume banana', action='store_true')

组 group 内不管加入多少个参数 add_argument,都只能出现一个,否则就会报错,弹出帮助提示。

④ action 是什么

action 参数主要时 ArgumentParser 定义时指定对特定的命令行参数要执行什么样的处理,尽管大多数情况下它们都只是简单地为 parse_args 返回的 Namespace 对象上加一个属性。内置支持的 action 有以下几种:

action 说明
store 存储起命名参数的值,这是 默认 的 action
store_const 同样时存储起命名参数的值,但是值是由 const 参数指定的
store_true/store_false 以 bool 类型存储命名参数的值
append 存储的是一个列表,它们作为值都属于同一个命名参数,常用于命令行有重复指定参数的情况:parser.parse_args('--foo 1 --foo 2'.split())
count 存储的命名参数的值是命名出现的次数,例如 parser.parse_args(['-vvv']) 得到的值会是 3
help 使得命名参数具有和 -h 默认的行为,下一节会提到
version 通常和 version= 参数搭配使用,当命令行参数出现对应的命名时,打印 version 的值

更多关于 action 的信息以及如何自定义 action,可以参考官方文档

⑤ 默认的 -h 行为

在 argparse 中,-h 命名参数具有特殊的含义:用于打印帮助消息并退出程序(执行 sys.exit())。这个 -h 的行为不可更改,如果想要控制使用 -h 显示帮助同时不退出交互界面,我们可以在程序中捕获 SystemExit 异常:

1
2
3
4
5
6
7
8
# 定义命令行参数解释器
.....
while True:
cmd = input(">>>")
try:
parser.parse_args(cmd.split())
except SystemExit:
print("ignoring SystemExit")

※ 题外话

这里 有一篇在 docs python 上的文章告诉你怎么使用 argparse,更具体来说,就是告诉你 add_argument 怎么配置参数达到你想要的效果。有兴趣的请自行食用😄

3. parse_args

通过上面的示例可以看出,如果说 argparse.ArgumentParser.add_argument 是用来定义解释器的,那么 parse_args 就是用来对参数执行解释器的。它的 signature 如下所示:

1
ArgumentParser.parse_args(args=None, namespace=None)

其中:

  • args —— 需要解释的参数字符串列表,默认是从 sys.argv 中取得
  • namespace —— Namespace 对象,用以对参数划分命名空间 ,默认是一个空的 Namespace 对象。