Python命令行工具argparse模块的使用

-- TOC --

用Python编写一些命令行小工具,配合标准库中的argparse模块,可以实现非常漂亮和专业的命令行参数的实现。

一些简单的命令行,直接读取sys.argv也可以了。复杂一些的,才需要使用argparse模块。

C语言一般使用getopt接口来实现命令参数。

固定位置参数

添加的不带---的参数,就是固定参数。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('echo')
args = parser.parse_args()
print(args.echo)

以上测试代码定义了一个固定位置参数echo,由于是必须出现的参数,程序后面在打印echo参数的值的时候,不需要判断是否配置了这个参数。因为如果没有配置,parser.parse_args()这个函数就会报错。

$ python3 arg.py
usage: arg.py [-h] echo
arg.py: error: the following arguments are required: echo
$ python3 arg.py abcde
abcde
$ python3 arg.py 'abcde 12345'
abcde 12345

固定位置参数在命令行中的出现和配置顺序,与代码中参数的定义顺序是对应的。如果定义了多个位置参数,命令行输入参数的时候,顺序要与多个参数的定位顺序保持一致!

可选参数

添加的带---的参数,属于可选参数。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-s','--sn')
args = parser.parse_args()
if args.sn: print(args.sn)
else: print('no argument')

这里有一个约定俗成的惯例:单个字母只是用一个-,多个字母使用--。python也支持一个-后面跟多个字母,不过看起来有关怪异。上面的定义 -s 显然是 --sn 的简写。

我们也可以只定义 -s 简写,或者之定义 --sn 这种,代码直接去掉一个参数即可。在两者都有的情况下,后面的代码引用参数的值,需要使用--后面的那个词。

$ python3 arg.py
no argument
$ python3 arg.py -s 123
123
$ python3 arg.py --sn abc
abc

灵活的必选参数

其实,可选参数也可以变成必选参数!必选参数跟位置参数还不一样,必选参数的位置可以比较灵活。

固定参数,可选参数,必选参数,都是我自己取的名字,还有下面的多值参数。:)

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-s', required=True)
parser.add_argument('-e','--echo')
args = parser.parse_args()
if args.s: print(args.s)
if args.echo: print(args.echo)

这段代码 -s 参数是必须参数,但是它出现的位置是灵活的,因为使用了required=True

$ python3 arg.py -h
usage: arg.py [-h] -s S [-e ECHO]

optional arguments:
  -h, --help            show this help message and exit
  -s S
  -e ECHO, --echo ECHO
$ python3 arg.py -s 123
123
$ python3 arg.py -s 123 -e abc
123
abc
$ python3 arg.py -e abc
usage: arg.py [-h] -s S [-e ECHO]
arg.py: error: the following arguments are required: -s
$ python3 arg.py -e abc -s 123
123
abc

-e 和 -s 出现的位置可以调换,而且 -s 必须要有。

(任意)参数类型

命令行参数的值,可以定义类型,可以是任意类型。

type=str,这个是默认值,前面的测试代码已经有示例。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('echo', type=int)
args = parser.parse_args()
print(args.echo)
print(type(args.echo))

修改为type=int后的测试:

E:\py>python arg.py 123
123
<class 'int'>
E:\py>python arg.py "123 456"
usage: arg.py [-h] echo
arg.py: error: argument echo: invalid int value: '123 456'

实际上就是将命令行参数值的字符串,尝试转换成int,如果不成功,就抛出异常。

这里可以通过type,实现任何类型。因为type后面接的,其实是一个函数接口,将用户输入的字符串进行转换的函数接口。

下面的示例,在命令行输入一个0x开始的十六进制字节数,转换函数实现0x开始的十六进制字节到int的转换:

import argparse

def take1byte(string):
    """change one-hex-byte to a real byte number"""
    check_list = ['0','1','2','3','4','5','6','7','8','9',
                  'A','B','C','D','E','F']
    if (len(string) != 4 or
        string[0:2] != '0x' or
        string[2] not in check_list or
        string [3] not in check_list): 
        msg = 'Not a valid one-hex-byte with 0x prefix, in upper case.'
        raise argparse.ArgumentTypeError(msg)
    return int(string[2:],16) 

parser = argparse.ArgumentParser()
parser.add_argument('--hex', required=True, type=take1byte)
args = parser.parse_args()
print('your input number is :', str(args.hex))

下面是这段测试代码的运行效果:

E:\py>python arg.py --hex=0x10
your input number is : 16
E:\py>python arg.py --hex=0xAA
your input number is : 170
E:\py>python arg.py --hex=0xFF
your input number is : 255
E:\py>python arg.py --hex=FF
usage: arg.py [-h] --hex HEX
arg.py: error: argument --hex: Not a valid one-hex-byte with 0x prefix, in upper
 case.

type=take1byte,转换函数take1byte函数的入参是一个字符串,如果在解析参数的时候出现错误,通过抛出argparse.ArgumentTypeError异常来实现命令行界面的提示信息,最后return返回真正的参数值。

再来一个示例,这个参数通过1+2+3...这样的方式获取参数,数字范围从1到12,可以重复+某个数字,转换函数将其转换成一个集合set类型:

import argparse

def takeslotset(string):
    """get slot set"""
    slot_set_str = set(string.split('+'))
    slot_set = set()
    for item in slot_set_str:
        if item not in ['1','2','3','4','5','6','7','8','9','10','11','12']:
            raise argparse.ArgumentTypeError(f'slot number error:{item}')
        slot_set.add(int(item))
    return slot_set

parser = argparse.ArgumentParser()
parser.add_argument('--slot', required=True, type=takeslotset)
args = parser.parse_args()
print('slot set:', args.slot)

运行效果如下:

E:\py>python arg.py --slot=1+2+3
slot set: {1, 2, 3}

E:\py>python arg.py --slot=1+2+3+12
slot set: {1, 2, 3, 12}

E:\py>python arg.py --slot=1+2+3+12+24
usage: arg.py [-h] --slot SLOT
arg.py: error: argument --slot: slot number error:24

E:\py>python arg.py --slot=1+2+3+3+3+3
slot set: {1, 2, 3}

--slot=1+2+3,与--slot 1+2+3等价。命令行参数值可以用=号指定(无空格),也可以用一个空格分割。

FileType

argparse.FileType是一个特殊的type类型,它可以用来打开一个stream,作为命令行参数的值。

import sys
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('infile', nargs='?',
                    type=argparse.FileType(),  # default is 'r'
                    help='give me a file here, i open it and reture to you',
                    default=sys.stdin)
parser.add_argument('outfile', nargs='?',
                    type=argparse.FileType('w'),
                    help='give me a file here, i write it for you with in file',
                    default=sys.stdout)

args = parser.parse_args()

with args.infile as f, args.outfile as g:
    g.write(f.read())

infile和outfile都是固定位置的可选参数,用nargs='?'来实现。

当这两个参数都不指定的时候,他们默认分别指向stdin和stdout。

运行效果如下:

$ echo 'abcde12345' > t1.txt
$ python3 arg.py t1.txt t2.txt
$ cat t2.txt
abcde12345
$ echo -e 'abcde\n12345' | python3 arg.py
abcde
12345
$ python3 arg.py < t1.txt
abcde12345

FileType objects understand the pseudo-argument - and automatically convert this into sys.stdin for readable FileType objects and sys.stdout for writable FileType objects. 原来-是一个命令行伪参数,它代表stdin或stdout。

上面的测试代码,使用伪参数的效果:

$ python3 arg.py - -
1234567890  # input by hand
1234567890  # output to stdout

narg,多值参数

所谓多值参数,即某个命令行参数的值,可以有多个,每个值通过空格分开。这需要使用nargs参数。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--one', nargs=1, required=True)
args = parser.parse_args()
print(args.one)

nargs=1,表示one参数只能而且必须有1个输入值。

E:\py>python arg.py --one  1
['1']

E:\py>python arg.py --one  1 2 3
usage: arg.py [-h] --one ONE
arg.py: error: unrecognized arguments: 2 3

args.one是一个list,只有一个输入值时,也是list。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--three', nargs=3, required=True)
args = parser.parse_args()
print(args.three)

以上nargs=3,表示必须有3个输入值,不等于3个输入值,都是错误:

E:\py>python arg.py --three  1 2 3
['1', '2', '3']

E:\py>python arg.py --three  1 2 3 4 5
usage: arg.py [-h] --three THREE THREE THREE
arg.py: error: unrecognized arguments: 4 5

nargs=?,表示0个或1个输入值,当没有输入值时,代码对此命令行参数赋None;

nargs=*,表示0个或多个输入值;

nargs=+,表示1个或多个输入值;

nargs= argparse.REMAINDER,表示后面所有的输入值,都属于这个命令行参数,比如当某个值带有---时,为了避免混淆,此时就要这样用nargs。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--remainder', nargs=argparse.REMAINDER, required=True)
args = parser.parse_args()
print(args.remainder)

运行效果如下:

E:\py>python arg.py --remainder
[]

E:\py>python arg.py --remainder 1
['1']

E:\py>python arg.py --remainder 1 2 3 4 5
['1', '2', '3', '4', '5']

E:\py>python arg.py --remainder 1 2 3 4 5 -k 1 2 3 4 5
['1', '2', '3', '4', '5', '-k', '1', '2', '3', '4', '5']

E:\py>python arg.py --remainder ls -lh
['ls', '-lh']

action

action用来指定某个命令行参数的“动作”。

默认值为action='store',表示保存用户输入的值(转换使用type),如果是一个可选参数,用户没有输入,此时为None。

store_true(模式选项)

命令行参数后面,不一定需要有值,配与不配,就已经不同了。貌似有一个词来形容这个情况,叫做模式选项。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-s', action='store_true')
parser.add_argument('-e', action='store_true')
args = parser.parse_args()
if args.s: print('have s')
if args.e: print('have e')

以上代码定义了两个模式选项,-s 和 -e,需要配合action='store_true'这个参数,表示只要配置了,对应的值就是True,不配置就是False(None的真值判断是False)。如下是这段代码在命令行调用的情况:

$ python3 arg.py -s -e
have s
have e
$ python3 arg.py -e -s
have s
have e
$ python3 arg.py -es
have s
have e
$ python3 arg.py -se
have s
have e

-s 和 -e可以分开,也可以混在一起,打乱顺序也没关系。模式选项这种命令行参数风格,来自Unix,python的argparse模块支持得还不错。

store_fase

与store_true反过来,如果配置了,值为False。

store_const

有add_argument接口的const参数提供命令行参数的值。

$ cat arg_action.py 
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--const', action='store_const', const=123)
args = parser.parse_args()
print(args.const)
$ python3 arg_action.py --const
123

const也可以等于一个字符串,如果没有配置,args.const就是None。

append

将命令行参数值放入list对象中,并且,允许这个命令行参数多次出现(nargs参数实现一个命令行参数对应多个值,但是不允许同一个命令行参数多次出现)。

$ cat arg_action.py
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--tolist', action='append')
args = parser.parse_args()
print(args.tolist)
$ python3 arg_action.py --tolist 1 --tolist 2 --tolist 3
['1', '2', '3']

如果在配合上nargs='+'参数,效果如下:

$ cat arg_action.py
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--tolist', action='append', nargs='+')
args = parser.parse_args()
print(args.tolist)
$ python3 arg_action.py --tolist 1 2 --tolist 3 4 5 --tolist 6 7
[['1', '2'], ['3', '4', '5'], ['6', '7']]

出现了嵌套多个list的效果。

count

这个action用来统计命令行参数出现的次数,出现次数不同,含义也不一样:

$ cat arg_action.py 
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-c', '--cc', action='count')
args = parser.parse_args()
print(args.cc)
xinlin@ubuntu:~/test$ python3 arg_action.py -c
1
xinlin@ubuntu:~/test$ python3 arg_action.py -ccccc
5
xinlin@ubuntu:~/test$ python3 arg_action.py --cc --cc --cc
3

version

提供软件的版本信息:

$ cat arg_action.py 
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-c', '--cc', action='count')
parser.add_argument('-V', action='version', version='cs.pynote.net V1.2345')
args = parser.parse_args()
print(args.cc)
$ python3 arg_action.py -V
pynote.net V1.2345

extend

前面介绍的action='append'时,多次出现的同一个命令行参数,参数值保存成了嵌套list的形式。action='extend'优化了此问题,让多个list以extend的方式合并成一个list。

$ cat arg_action.py 
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--onelist', action='extend', nargs='+')
parser.add_argument('-V', action='version', version='pynote.net V1.2345')
args = parser.parse_args()
print(args.onelist)
$ python3 arg_action.py --onelist 1 2 3 --onelist 4 5 6
['1', '2', '3', '4', '5', '6']

append_const

...

description和epilog

用python编写一个命令行接口的程序,少不了 -h 时的丰富的命令行信息显示,这就需要 ArgumentParser 对象使用description和epilog参数。description参数显示在usage和命令行参数(positional或optional)之间,我喜欢用这个区域来写程序的usage example。epilog参数显示在最后,我喜欢用这个区域留下自己的github页面和博客链接。

import argparse

parser = argparse.ArgumentParser(
                description = 'this is description',
                epilog = 'cs.pynote.net')
parser.print_help()

运行效果如下:

E:\py>python cmd_info.py
usage: cmd_info.py [-h]

this is description

optional arguments:
  -h, --help  show this help message and exit

cs.pynote.net

description部分好好写的haul,一般要写不少内容,会有很多行,默认情况下,ArgumentParser对象会对多行内容进行line-wrap,就是把所有的换行和连续的空格都变成一个空格。同时,我们还希望能够完美保留代码中的格式。

import argparse
import textwrap

parser = argparse.ArgumentParser(
                formatter_class = argparse.RawDescriptionHelpFormatter,
                description = textwrap.dedent('''\
                this is description
                which have a few lines and many line
                feed and space...this is description
                which have a few lines and many line
                feed and space...
                '''),
                epilog = 'cs.pynote.net')
parser.print_help()

textwrap也是标准库中的模块,dedent表示取消indent。

运行效果如下:

E:\py>python cmd_info.py
usage: cmd_info.py [-h]

this is description
which have a few lines and many line
feed and space...this is description
which have a few lines and many line
feed and space...

optional arguments:
  -h, --help  show this help message and exit

cs.pynote.net

choices

给命令行参数,设置一个值的选择范围。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--cc', choices=('1','2','3'))
args = parser.parse_args()

--cc这个命令行参数的取值范围是'1','2'和'3',没有别的。

D:\temp>python test.py --cc 2

D:\temp>python test.py --cc 4
usage: test.py [-h] [--cc {1,2,3}]
test.py: error: argument --cc: invalid choice: '4' (choose from '1', '2', '3')

choices后来跟一个iterable,这样就可以是一个字符串对象了:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--cc', choices='abc')
args = parser.parse_args()

运行效果如下:

D:\temp>python test.py --cc a

D:\temp>python test.py --cc d
usage: test.py [-h] [--cc {a,b,c}]
test.py: error: argument --cc: invalid choice: 'd' (choose from 'a', 'b', 'c')

default

default表示当某一个可选参数没有出现在命令行时(没有配置),此时,这个参数的默认值,default的默认值为None,即默认情况下,所有没有配置的参数,经过parse_args调用后,其值为None。

注意default与store_const的区别,后者表示如果配置了,附上const对应的值,如果没有配置,默认就是None。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-d', default='abc')
args = parser.parse_args()
print(args.d)

运行效果:

D:\temp>python test.py -d 123
123

D:\temp>python test.py
abc

default值需要与type对应起来,也需要与nargs之的含义对应起来,否则代码会报错。

default有一个特殊的值,argparse.SUPPRESS,它表示当这个命令行参数没有配置的时候,连None这个值都不要给了,直接让它不存在。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--echo', default=argparse.SUPPRESS)
args = parser.parse_args()
try:
    print(args.echo)
except Exception as e:
    print(repr(e))

运行效果如下:

D:\py>python arg_default.py --echo 123
123

D:\py>python arg_default.py
AttributeError("'Namespace' object has no attribute 'echo'")

是否使用这个特殊的值,就看你的代码逻辑了,哪种方式让代码看起来更优雅...

互斥参数

所谓互斥参数,就是在某几个命令行参数中,用户只能选择其中一个输入。

import argparse


def main():
    parser = argparse.ArgumentParser()
    timeType = parser.add_mutually_exclusive_group(required=True)
    timeType.add_argument('--day', type=int, 
            help='keep the last x days files')
    timeType.add_argument('--month', type=int, 
            help='keep the last x months files')
    timeType.add_argument('--year', type=int, 
            help='keep the last x years files')
    args = parser.parse_args()
    print(args.day)
    print(args.month)
    print(args.year)


if __name__ == '__main__':
    main()

以上测试代码,实现了一组互斥参数,required=True的功能,与前面的介绍相同。

运行效果如下:

D:\temp>python test.py -h
usage: test.py [-h] (--day DAY | --month MONTH | --year YEAR)

optional arguments:
  -h, --help     show this help message and exit
  --day DAY      keep the last x days files
  --month MONTH  keep the last x months files
  --year YEAR    keep the last x years files

D:\temp>python test.py --day 1
1
None
None

D:\temp>python test.py --month 2
None
2
None

D:\temp>python test.py --year 3
None
None
3

metavar

add_argument函数的metavar参数,用来控制命令行参数的显示,注意:它只是影响参数的显示信息,不影响代码内部获取命令行参数的对象名称。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-d', metavar='DDD')
args = parser.parse_args()
print(args.d)

如果没有metavar='DDD',显示为-d D,现在的显示如下:

D:\temp>python test.py -h
usage: test.py [-h] [-d DDD]

optional arguments:
  -h, --help  show this help message and exit
  -d DDD

当与nargs配合的时候,metavar要写成一个list,或tuple。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-d', nargs=2, metavar=('D1','D2'))
args = parser.parse_args()
print(args.d)

运行效果:

D:\temp>python test.py -h
usage: test.py [-h] [-d D1 D2]

optional arguments:
  -h, --help  show this help message and exit
  -d D1 D2

dest

用dest可以改变在代码中获取命令行参数值的对象名称。

前面的测试代码已经充分演示了,通过args的属性对象获取命令行参数的值,而这个属性对象的名称,默认为使用add_argument接口注册的名称,基本与命令行上显示的名称一直。如果绝对代码不够优雅,可以用dest来修改这个名称。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-d', dest='pump')
args = parser.parse_args()
print(args.pump)

用了dest,代码就要使用args.pump这个含义更明确的属性来获取参数值,可以提到代码的可读性。

prog

这是add_argument接口的一个参数,用来指定程序名称,默认的程序名称,就是这个python代码文件的名称,带.py后缀。

import argparse

parser = argparse.ArgumentParser(prog='abc')
parser.add_argument('-d', dest='pump')
args = parser.parse_args()
print(args.pump)

运行效果,注意看abc出现的位置:

D:\temp>python test.py -h
usage: abc [-h] [-d PUMP]

optional arguments:
  -h, --help  show this help message and exit
  -d PUMP

貌似一般情况下,这个功能都用不上。

子命令sub-command

首先要搞清楚什么是子明玲玲sub-command?

其实,我们每天都在使用的git,就是典型的sub-command命令行。

比如:git add, git push, git rebase......这些命令行中的add,push,rebase都叫做sub-command!(他们可不是固定位置的参数哦,每一个sub-command都有一整套属于自己的其它可选或必选参数等)

python自带的argparse模块,也可以让我们实现sub-command的功能。

import sys
import argparse

parser = argparse.ArgumentParser()

# sub command definition
subparser = parser.add_subparsers(dest='subcmd',
                                  title='sub-commands')
parser_inline = subparser.add_parser('inline')
parser_infile = subparser.add_parser('infile')

# sub command inline
parser_inline.add_argument('-a')

# sub command infile
parser_infile.add_argument('-b')

args = parser.parse_args()

以上测试代码,定义了两个sub-command,分别是inline和infile,然后再分别对inline和infile进行命令行参数的定义。

运行效果如下:

D:\temp>python test.py -h
usage: test.py [-h] {inline,infile} ...

optional arguments:
  -h, --help       show this help message and exit

sub-commands:
  {inline,infile}

D:\temp>python test.py inline -h
usage: test.py inline [-h] [-a A]

optional arguments:
  -h, --help  show this help message and exit
  -a A

D:\temp>python test.py infile -h
usage: test.py infile [-h] [-b B]

optional arguments:
  -h, --help  show this help message and exit
  -b B

本文链接:https://cs.pynote.net/sf/python/202208222/

-- EOF --

-- MORE --