本文来自:https://github.com/HelloGitHub-Team/Article
文章目录
Python 命令行之旅:初探 argparse
前言
你是否好奇过在命令行中敲入一段命令后,它是如何被解析执行的?是否考虑过由自己实现一个命令行工具,帮你执行和处理任务?是否了解过陪伴在你身边的 Python 有着丰富的库,来帮你轻松打造命令行工具?
别着急,本文作为 Python 命令行之旅的第一篇将带你逐步揭开命令行解析的面纱,介绍如何使用 Python 内置的 argparse
标准库解析命令行,并在后续的系列文章中介绍各具特色的第三方命令行库,讲讲它们的异同,进而全面地体验这次探索的旅程。
本系列文章默认使用 Python 3 作为解释器进行讲解。
若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~
介绍
argparse
作为 Python 内置的标准库,提供了较为简单的方式来编写命令行接口。当你在程序中定义需要哪些参数,argparse
便会从 sys.argv
中获取命令行输入进行解析,对正确或非法输入做出响应,也可以自动生成帮助信息和使用说明。
快速开始
设置解析器
第一步要做的就是设置解析器,后续对命令行的解析就依赖于这个解析器,它能够将命令行字符串转换为 Python 对象。
通过实例化 argparse.ArgumentParser
,给定一些选填参数,我们就可以设置一个解析器:
import argparse
parser = argparse.ArgumentParser(
description='My Cmd Line Program',
)
定义参数
通过 ArgumentParser.add_argument
方法来为解析器设置参数信息,以告诉解析器命令行字符串中的哪些内容应解析为哪些类型的 Python 对象,如:
# 添加 nums 参数,在使用信息中显示为 num
# 其类型为 int,且支持输入多个,且至少需要提供一个
parser.add_argument('nums', metavar='num', type=int, nargs='+',
help='a num for the accumulator')
# 添加 --sum 参数,该参数被 parser 解析后所对应的属性名为 accumulate
# 若不提供 --sum,默认值为 max 函数,否则为 sum 函数
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the nums (default: find the max)')
解析命令行
定义好参数后,就可以使用 ArgumenteParser.parse_args
方法来解析一组命令行参数字符串了。
默认情况下,参数取自 sys.argv[1:]
,它就是你在命令行敲入的一段命令(不含文件名)所对应的一个字符串列表。
比如,若你输入 python3 cmd.py --sum 1 2 3
,那么 sys.argsv[1:]
就是 ['--sum', '1', '2', '3']
。
当然,也可以通过 parse_args
入参来指定一组命令行参数字符串:
args = parser.parse_args(['--sum', '-1', '0', '1'])
print(args) # 结果:Namespace(accumulate=<built-in function sum>, nums=[-1, 0, 1])
业务逻辑
解析好命令行后,我们就可以从解析结果中获取每个参数的值,进而根据自己的业务需求做进一步的处理。
比如,对于上文中所定义的 nums 参数,我们可以通过解析后的结果中的 accumulate
方法对其进行求最大值或求和(取决于是否提供 --sum
参数)。
result = args.accumulate(args.nums)
print(result) # 基于上文的 ['--sum', '-1', '0', '1'] 参数,accumulate 为 sum 函数,其结果为 0
代码梳理
通过上文的讲解,完成一个命令行工具的步骤是不是挺简单易懂呢?我们将上文的代码汇总下,以有一个更清晰的认识:
# cmd.py
import argparse
# 1. 设置解析器
parser = argparse.ArgumentParser(
description='My Cmd Line Program',
)
# 2. 定义参数
parser.add_argument('nums', metavar='num', type=int, nargs='+',
help='a num for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the nums (default: find the max)')
# 3. 解析命令行
args = parser.parse_args()
# 4. 业务逻辑
result = args.accumulate(args.nums)
print(result)
若我们需要对一组数字求和,只需执行:
$ python3 cmd.py --sum -1 0 1
0
若我们需要对一组数字求最大值,只需执行:
$ python3 cmd.py -1 0 1
1
如果给定的参数不是数字,则会报错提示:
$ python3 cmd.py a b c
usage: cmd.py [-h] [--sum] num [num ...]
cmd.py: error: argument num: invalid int value: 'a'
我们还可以通过 -h
或 --help
参数查看其自动生成的使用说明和帮助:
usage: cmd.py [-h] [--sum] num [num ...]
My Cmd Line Program
positional arguments:
num a num for the accumulator
optional arguments:
-h, --help show this help message and exit
--sum sum the nums (default: find the max)
小结
怎么样?揭开命令行工具的神秘面纱后,是不是发现它并没有想象中的困难?反倒是感受到一种简单而又强大的优雅呢?
不过这还远远不是 argparse
的全部面貌。对于一些复杂的情况,比如各种类型参数、参数前缀、参数组、互斥选项、嵌套解析、自定义帮助等等,我们都还没涉及探讨。
在下一篇文章中,让我们来一起深入了解 argparse
,感受它的魅力吧!
Python 命令行之旅:深入 argparse(一)
前言
在第一篇“初探 argparse”的文章中,我们初步掌握了使用 argparse
的四部曲,对它有了一个基本的体感。
但是它具体支持哪些类型的参数?这些参数该如何配置?本文将带你深入了解 argparse
的参数们。
本系列文章默认使用 Python 3 作为解释器进行讲解。
若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~
参数动作
你是否还记得?在上一篇四部曲中的第二步是定义参数,在这个步骤中,我们指定了 action
入参:
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the nums (default: find the max)')
那么这里面的 action
,也就是 参数动作
,究竟是用来做什么的呢?
想象一下,当我们在命令行输入一串参数后,对于不同类型的参数是希望做不同的处理的。
那么 参数动作
其实就是告诉解析器,我们希望对应的参数该被如何处理。比如,参数值是该被存成一个值呢,还是追加到一个列表中?是当成布尔的 True 呢,还是 False?
参数动作
被分成了如下 8 个类别:
store
—— 保存参数的值,这是默认的参数动作。它通常用于给一个参数指定值,如指定名字:
>>> parser.add_argument('--name')
>>> parser.parse_args(['--name', 'Eric'])
Namespace(name='Eric')
store_const
—— 保存被const
命名的固定值。当我们想通过是否给定参数来起到标志的作用,给定就取某个值,就可以使用该参数动作,如:
>>> parser.add_argument('--sum', action='store_const', const=sum)
>>> parser.parse_args(['--sum'])
Namespace(sum=<built-in function sum>)
>>> parser.parse_args([])
Namespace(sum=None)
store_true
和store_false
—— 是store_const
的特殊情况,用来分别保存 True 和 False。如果为指定参数,则其默认值分别为 False 和 True,如:
>>> parser.add_argument('--use', action='store_true')
>>> parser.add_argument('--nouse', action='store_false')
>>> parser.parse_args(['--use', '--nouse'])
Namespace(nouse=False, use=True)
>>> parser.parse_args([])
Namespace(nouse=True, use=False)
append
—— 将参数值追加保存到一个列表中。它常常用于命令行中允许多个相同选项,如:
>>> parser.add_argument('--file', action='append')
>>> parser.parse_args(['--file', 'f1', '--file', 'f2'])
Namespace(file=['f1', 'f2'])
append_const
—— 将const
命名的固定值追加保存到一个列表中(const
的默认值为None
)。它常常用于将多个参数所对应的固定值都保存在同一个列表中,相应的需要dest
入参来配合,以放在同一个列表中,如:
不指定 dest
入参,则固定值保存在以参数名命名的变量中
>>> parser.add_argument('--int', action='append_const', const=int)
>>> parser.add_argument('--str', action='append_const', const=str)
>>> parser.parse_args(['--int', '--str'])
Namespace(int=[<class 'int'>], str=[<class 'str'>])
指定 dest
入参,则固定值保存在 dest
命名的变量中
>>> parser.add_argument('--int', dest='types', action='append_const', const=int)
>>> parser.add_argument('--str', dest='types', action='append_const', const=str)
>>> parser.parse_args(['--int', '--str'])
Namespace(types=[<class 'int'>, <class 'str'>])
count
—— 计算参数出现次数,如:
>>> parser.add_argument('--increase', '-i', action='count')
>>> parser.parse_args(['--increas', '--increase'])
Namespace(increase=2)
>>>parser.parse_args(['-iii'])
Namespace(increase=3)
-
help
—— 打印解析器中所有选项和参数的完整帮助信息,然后退出。 -
version
—— 打印命令行版本,通过指定version
入参来指定版本,调用后退出。如:
>>> parser = argparse.ArgumentParser(prog='CMD')
>>> parser.add_argument('--version', action='version', version='%(prog)s 1.0')
>>> parser.parse_args(['--version'])
CMD 1.0
参数类别
如果说 参数动作
定义了解析器在接收到参数后该如何处理参数,那么 参数类别
就是告诉解析器这个参数的元信息,也就是参数是什么样的。比如,参数是字符串呢?还是布尔类型呢?参数是在几个值中可选的呢?还是可以给定值,等等。
下面,我们将逐一介绍不同类型的参数。
可选参数
可选参数
顾名思义就是参数是可以加上,或不加上。默认情况下,通过 ArgumentParser.add_argument
添加的参数就是可选参数。
我们可以通过 -
来指定短参数,也就是名称短的参数;也可以通过 --
来指定长参数,也就是名称长的参数。当然也可以两个都指定。
可选参数通常用于:用户提供一个参数以及对应值,则使用该值;若不提供,则使用默认值。如:
>>> parser.add_argument('--name', '-n')
>>> parser.parse_args(['--name', 'Eric']) # 通过长参数指定名称
Namespace(name='Eric')
>>> parser.parse_args(['-n', 'Eric']) # 通过短参数指定名称
Namespace(name='Eric')
>>> parser.parse_args([]) # 不指定则默认为 None
Namespace(name=None)
参数类型
参数类型
就是解析器参数值是要作为什么类型去解析,默认情况下是 str
类型。我们可以通过 type
入参来指定参数类型。
argparse
所支持的参数类型多种多样,可以是 int
、float
、bool
等,比如:
>>> parser.add_argument('-i', type=int)
>>> parser.add_argument('-f', type=float)
>>> parser.add_argument('-b', type=bool)
>>> parser.parse_args(['-i', '1', '-f', '2.1', '-b', '0'])
Namespace(b=False, f=2.1, i=1)
更厉害的是,type
入参还可以是可调用(callable
)对象。这就给了我们很大的想象空间,可以指定 type=open
来把参数值作为文件进行处理,也可以指定自定义函数来进行类型检查和类型转换。
作为文件进行处理:
>>> parser.add_argument('--file', type=open)
>>> parser.parse_args(['--file', 'README.md'])
Namespace(b=None, f=None, file=<_io.TextIOWrapper name='README.md' mode='r' encoding='cp936'>, i=None)
使用自定义函数进行处理,入参为参数值,需返回转换后的结果。
比如,对于参数 --num
,我们希望当其值小于 1 时则返回 1,大于 10 时则返回 10:
>>> def limit(string):
... num = int(string)
... if num < 1:
... return 1
... if num > 10:
... return 10
... return num
...
>>> parser.add_argument('--num', type=limit)
>>> parser.parse_args(['--num', '-1']) # num 小于1,则取1
Namespace(num=1)
>>> parser.parse_args(['--num', '15']) # num 大于10,则取10
Namespace(num=10)
>>> parser.parse_args(['--num', '5']) # num 在1和10之间,则取原来的值
Namespace(num=5)
参数默认值
参数默认值
用于在命令行中不传参数值的情况下的默认取值,可通过 default
来指定。如果不指定该值,则参数默认值为 None
。
比如:
>>> parser.add_argument('-i', default=0, type=int)
>>> parser.add_argument('-f', default=3.14, type=float)
>>> parser.add_argument('-b', default=True, type=bool)
>>> parser.parse_args([])
Namespace(b=True, f=3.14, i=0)
位置参数
位置参数
就是通过位置而非是 -
或 --
开头的参数来指定参数值。
比如,我们可以指定两个位置参数 x
和 y
,先添加的 x
位于第一个位置,后加入的 y
位于第二个位置。那么在命令行中输入 1 2
的时候,分别对应到的就是 x
和 y
:
>>> parser.add_argument('x')
>>> parser.add_argument('y')
>>> parser.parse_args(['1', '2'])
Namespace(x='1', y='2')
可选值
可选值
就是限定参数值的内容,通过 choices
入参指定。
有些情况下,我们可能需要限制用户输入参数的内容,只能在预设的几个值中选一个,那么 可选值
就派上了用场。
比如,指定文件读取方式限制为 read-only
和 read-write
:
>>> parser.add_argument('--mode', choices=('read-only', 'read-write'))
>>> parser.parse_args(['--mode', 'read-only'])
Namespace(mode='read-only')
>>> parser.parse_args(['--mode', 'read'])
usage: [-h] [--mode {read-only,read-write}]
: error: argument --mode: invalid choice: 'read' (choose from 'read-only', 'read-write')
互斥参数
互斥参数
就是多个参数之间彼此互斥,不能同时出现。使用互斥参数首先通过 ArgumentParser.add_mutually_exclusive_group
在解析器中添加一个互斥组,然后在这个组里添加参数,那么组内的所有参数都是互斥的。
比如,我们希望通过命令行来告知乘坐的交通工具,要么是汽车,要么是公交,要么是自行车,那么就可以这么写:
>>> group = parser.add_mutually_exclusive_group()
>>> group.add_argument('--car', action='store_true')
>>> group.add_argument('--bus', action='store_true')
>>> group.add_argument('--bike', action='store_true')
>>> parser.parse_args([]) # 什么都不乘坐
Namespace(bike=False, bus=False, car=False)
>>> parser.parse_args(['--bus']) # 乘坐公交
Namespace(bike=False, bus=True, car=False)
>>> parser.parse_args(['--bike']) # 骑自行车
Namespace(bike=True, bus=False, car=False)
>>> parser.parse_args(['--bike', '--car']) # 又想骑车,又想坐车,那是不行的
usage: [-h] [--car | --bus | --bike]
: error: argument --car: not allowed with argument --bike
可变参数列表
可变参数列表
用来定义一个参数可以有多个值,且能通过 nargs
来定义值的个数。
若 nargs=N
,N
为一个数字,则要求该参数提供 N 个值,如:
>>> parser.add_argument('--foo', nargs=2)
>>> print(parser.parse_args(['--foo', 'a', 'b']))
Namespace(foo=['a', 'b'])
>>> print(parser.parse_args(['--foo', 'a', 'b', 'c']))
usage: [-h] [--foo FOO FOO]
: error: unrecognized arguments: c
若 nargs=?
,则要求改参数提供 0 或 1 个值,如:
>>> parser.add_argument('--foo', nargs='?')
>>> parser.parse_args(['--foo'])
Namespace(foo=None)
>>> parser.parse_args(['--foo', 'a'])
Namespace(foo='a')
>>> parser.parse_args(['--foo', 'a', 'b'])
usage: [-h] [--foo [FOO]]
: error: unrecognized arguments: b
若 nargs=*
,则要求改参数提供 0 或多个值,如:
>>> parser.add_argument('--foo', nargs='*')
>>> parser.parse_args(['--foo'])
Namespace(foo=[])
>>> parser.parse_args(['--foo', 'a'])
Namespace(foo=['a'])
>>> parser.parse_args(['--foo', 'a', 'b', 'c', 'd', 'e'])
Namespace(foo=['a', 'b', 'c', 'd', 'e'])
若 nargs=+
,则要求改参数至少提供 1 个值,如:
>>> parser.add_argument('--foo', nargs='+')
>>> parser.parse_args(['--foo', 'a'])
Namespace(foo=['a'])
>>> parser.parse_args(['--foo'])
usage: [-h] [--foo FOO [FOO ...]]
: error: argument --foo: expected at least one argument
小结
在了解了参数动作和参数类别后,是不是渐渐开始对使用 argparse
胸有成竹了呢?至少,用现在学到的知识来完成简单的命令行工具已经不在话下了。
在下一篇文章中,我们来继续深入了解 argparse
的功能,如何修改参数前缀,如何定义参数组,如何定义嵌套的解析器,如何编写自定义动作等,让我们拭目以待吧~
Python 命令行之旅:深入 argparse(二)
前言
在上一篇“深入 argparse(一)”的文章中,我们深入了解了 argparse
的包括参数动作和参数类别在内的基本功能,具备了编写一个简单命令行程序的能力。本文将继续深入了解 argparse
的进阶玩法,一窥探其全貌,助力我们拥有实现复杂命令行程序的能力。
本系列文章默认使用 Python 3 作为解释器进行讲解。
若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~
帮助
自动生成帮助
当你在命令行程序中指定 -h
或 --help
参数时,都会输出帮助信息。而 argparse
可通过指定 add_help
入参为 True
或不指定,以达到自动输出帮助信息的目的。
>>> import argparse
>>> parser = argparse.ArgumentParser(add_help=True)
>>> parser.add_argument('--foo')
>>> parser.parse_args(['-h'])
usage: [-h] [--foo FOO]
optional arguments:
-h, --help show this help message and exit
--foo FOO
如果 add_help=False
,那么在命令行中指定 -h
则会报错:
>>> import argparse
>>> parser = argparse.ArgumentParser(add_help=False)
>>> parser.add_argument('--foo')
>>> parser.parse_args(['-h'])
usage: [--foo FOO]
: error: unrecognized arguments: -h
自定义帮助
ArgumentParser
使用 formatter_class
入参来控制所输出的帮助格式。
比如,通过指定 formatter_class=argparse.RawTextHelpFormatter
,我们可以让帮助内容遵循原始格式:
>>> import argparse
>>> parser = argparse.ArgumentParser(
... add_help=True,
... formatter_class=argparse.RawTextHelpFormatter,
... description="""
... description
... raw
... formatted"""
... )
>>> parser.add_argument(
... '-a', action="store_true",
... help="""argument
... raw
... formatted
... """
... )
>>>
>>> parser.parse_args(['-h'])
usage: [-h] [-a]
description
raw
formatted
optional arguments:
-h, --help show this help message and exit
-a argument
raw
formatted
对比下不指定 formatter_class
的帮助输出,就可以发现 descirption 和 -a 两个帮助内容上的差异:
>>> import argparse
>>> parser = argparse.ArgumentParser(
... add_help=True,
... description="""
... description
... notraw
... formatted"""
... )
>>> parser.add_argument(
... '-a', action="store_true",
... help="""argument
... notraw
... formatted
... """
... )
>>> parser.parse_args(['-h'])
usage: [-h] [-a]
description notraw formatted
optional arguments:
-h, --help show this help message and exit
-a argument notraw formatted
参数组
有时候,我们需要给参数分组,以使得在显示帮助信息时能够显示到一起。
比如某命令行支持三个参数选项 --user
、--password
和--push
,前两者需要放在一个名为 authentication
的分组中以表示它们是身份认证信息。那么我们可以用 ArgumentParser.add_argument_group
来满足:
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> group = parser.add_argument_group('authentication')
>>> group.add_argument('--user', action="store")
>>> group.add_argument('--password', action="store")
>>> parser.add_argument('--push', action='store')
>>> parser.parse_args(['-h'])
usage: [-h] [--user USER] [--password PASSWORD] [--push PUSH]
optional arguments:
-h, --help show this help message and exit
--push PUSH
authentication:
--user USER
--password PASSWORD
可以看到,当我们输出帮助信息时,--user
和 --password
选项都出现在 authentication
分组中。
选项参数前缀
不知你是否注意到,在不同平台上命令行程序的选项参数前缀可能是不同的。比如在 Unix 上,其前缀是 -
;而在 Windows 上,大多数命令行程序(比如 findstr
)的选项参数前缀是 /
。
在 argparse
中,选项参数前缀默认采用 Unix 命令行约定,也就是 -
。但它也支持自定义前缀,下面是一个例子:
>>> import argparse
>>>
>>> parser = argparse.ArgumentParser(
... description='Option prefix',
... prefix_chars='-+/',
... )
>>>
>>> parser.add_argument('-power', action="store_false",
... default=None,
... help='Set power off',
... )
>>> parser.add_argument('+power', action="store_true",
... default=None,
... help='Set power on',
... )
>>> parser.add_argument('/win',
... action="store_true",
... default=False)
>>> parser.parse_args(['-power'])
Namespace(power=False, win=False)
>>> parser.parse_args(['+power', '/win'])
Namespace(power=True, win=True)
在这个例子中,我们指定了三个选项参数前缀 -
、+
和/
,从而:
- 通过指定选项参数
-power
,使得power=False
- 通过指定选项参数
+power
,使得power=True
- 通过指定选项参数
/win
,使得win=True
共享解析器
有些时候我们需要共享解析器,以共享里面的参数配置。比如,我们的命令行工具需要支持对阿里云和 AWS 进行操作,两类操作都需要指定 AccessKeyId
和 AccessKeySecret
来表明用户身份和权限。那么共享解析器就显得尤为必要,这样就可以少去重复代码。
我们可以这样做,在 base.py
中定义一个父解析器,存放 AccessKey
相关参数配置,作为公用的解析器。由于后续的子解析器会自动生成帮助信息,这里的父解析器指定 add_help=False
以不自动生成帮助信息:
# bash.py
import argparse
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--ak-id', action="store")
parser.add_argument('--ak-secret', action="store")
然后就可以分别在 ali.py
和 aws.py
中分别定义子解析器,通过 parents
入参指定上述父解析器,从而继承公共的参数,并实现各自的参数:
# ali.py
import argparse
import base
parser = argparse.ArgumentParser(
parents=[base.parser],
)
parser.add_argument('--ros',
action="store_true",
default=False,
help='Using ROS service to orchestrate cloud resources')
print(parser.parse_args())
# aws.py
import argparse
import base
parser = argparse.ArgumentParser(
parents=[base.parser],
)
parser.add_argument('--cloudformation',
action="store_true",
default=False,
help='Using CloudFormation service to orchestrate cloud resources')
print(parser.parse_args())
最终通过 -h
参数分别看 ali.py
和 aws.py
所支持的参数,其中共同参数为 --ak-id
和 --ak-secret
,特定参数分别为 --ros
和 --cloudformation
:
$ python3 ali.py -h
usage: ali.py [-h] [--ak-id AK_ID] [--ak-secret AK_SECRET] [--ros]
optional arguments:
-h, --help show this help message and exit
--ak-id AK_ID
--ak-secret AK_SECRET
--ros Using ROS service to orchestrate cloud resources
$ python3 aws.py -h
usage: aws.py [-h] [--ak-id AK_ID] [--ak-secret AK_SECRET] [--cloudformation]
optional arguments:
-h, --help show this help message and exit
--ak-id AK_ID
--ak-secret AK_SECRET
--cloudformation Using CloudFormation service to orchestrate cloud
resources
嵌套解析器
我们之前介绍的命令行中,使用形式通常是 cli --a --b xxx
。但还有一种极为常见的命令行使用方式是 cli subcmd --a --b xxx
。比如当我们要通过 git
推送标签时,会用到 git push --tags
。
通过实现嵌套解析器,我们可以很容易地对这种子命令的形式进行解析。
在嵌套解析器中,我们定义一个父解析器来作为整个命令行的入口,再分别定义N个子解析器来对应N个子命令,由此即可实现整个功能。
在下面这个例子中,我们支持 create
和 delete
两个子命令,用来创建或删除指定路径。而 delete
命令支持 --recursive
参数来表明是否递归删除指定路径:
# cli.py
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')
# Create
create_parser = subparsers.add_parser(
'create', help='Create a directory')
create_parser.add_argument(
'dirname', action='store',
help='New directory to create')
# Delete
delete_parser = subparsers.add_parser(
'delete', help='Remove a directory')
delete_parser.add_argument(
'dirname', action='store', help='The directory to remove')
delete_parser.add_argument(
'--recursive', '-r', default=False, action='store_true',
help='Recursively remove the directory',
)
print(parser.parse_args())
直接指定 -h
来查看所支持的子命令和参数选项:
$ python3 cli.py -h
usage: cli.py [-h] {create,delete} ...
positional arguments:
{create,delete} commands
create Create a directory
delete Remove a directory
optional arguments:
-h, --help show this help message and exit
直接指定 delete -h
来查看 delete
子命令支持的参数选项:
$ python3 cli.py delete -h
usage: cli.py delete [-h] [--recursive] dirname
positional arguments:
dirname The directory to remove
optional arguments:
-h, --help show this help message and exit
--recursive, -r Recursively remove the directory
自定义动作
在上一篇“深入 argparse (一)”的文章中介绍过8种参数动作,可以说是覆盖了绝大部分场景。但是也会有一些特定需求无法被满足,比如希望获取到的参数值都是大写。在这种情况下,自定义动作就派上了用场。
实现一个自定义动作类,需继承自 argparse.Action
,这个自定义动作类要传入到 ArgumentParser.add_argument
的 action
入参。当解析器解析参数时,会调用该类的 __call__
方法,该方法的签名为 __call__(self, parser, namespace, values, option_string=None)
,其中:
- parser 为解析器实例
- namespace 存放解析结果
- values 即命令行中传入的参数值
- option_string 为参数选项
在下面的例子中,我们通过 --words
传入单词,并在自定义动作类中将其值转换为大写:
# cli.py
import argparse
class WordsAction(argparse.Action):
def __call__(self, parser, namespace, values,
option_string=None):
print(f'parser = {parser}')
print(f'values = {values!r}')
print(f'option_string = {option_string!r}')
values = [v.upper() for v in values]
setattr(namespace, self.dest, values)
parser = argparse.ArgumentParser()
parser.add_argument('--words', nargs='*', action=WordsAction)
results = parser.parse_args()
print(results)
$ python3 cli.py --words foo bar
parser = ArgumentParser(prog='cli.py', usage=None, description=None, formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
values = ['foo', 'bar']
option_string = '--words'
Namespace(words=['FOO', 'BAR'])
小节
通过对 argparse
由浅入深的介绍,相信你已经全面了解了 argparse
的威力,也具备了开发命令行工具的能力。但“纸上得来终觉浅,绝知此事要躬行”。
Python 命令行之旅:使用 argparse 实现 git 命令
前言
在前面三篇介绍 argparse
的文章中,我们全面了解了 argparse
的能力,相信不少小伙伴们都已经摩拳擦掌,想要打造一个属于自己的命令行工具。
本文将以我们日常工作中最常见的 git
命令为例,讲解如何使用 argparse
库来实现一个真正可用的命令行程序。
本系列文章默认使用 Python 3 作为解释器进行讲解。
若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~
git 常用命令
大家不妨回忆一下,平时最常使用 git
子命令都有哪些?
当你写好一段代码或增删一些文件后,会用如下命令查看文件状态:
git status
确认文件状态后,会用如下命令将的一个或多个文件(夹)添加到暂存区:
git add [pathspec [pathspec ...]]
然后使用如下命令提交信息:
git commit -m "your commit message"
最后使用如下命令将提交推送到远程仓库:
git push
我们将使用 argparse
和 gitpython
库来实现这 4 个子命令。
关于 gitpython
gitpython 是一个和 git
仓库交互的 Python 第三方库。
我们将借用它的能力来实现真正的 git
逻辑。
安装:
pip install gitpython
思考
在实现前,我们不妨先思考下会用到 argparse
的哪些功能?整个程序的结构是怎样的?
argparse
- 要实现子命令,那么之前介绍到的
嵌套解析器
必不可少 - 当用户键入子命令时,子命令所对应的子解析器需要作出响应,那么需要用到子解析器的
set_defaults
功能 - 针对
git add [pathspec [pathspec ...]]
,我们需要实现位置参数,而且数量是任意个 - 针对
git commit --message msg
或git commit -m msg
,我们需要实现选项参数,且即可长选项,又可短选项
程序结构
- 命令行程序需要一个
cli
函数来作为统一的入口,它负责构建解析器,并解析命令行参数 - 我们还需要四个
handle_xxx
函数响应对应的子命令
则基本结构如下:
import os
import argparse
from git.cmd import Git
def cli():
"""
git 命名程序入口
"""
pass
def handle_status(git, args):
"""
处理 status 命令
"""
pass
def handle_add(git, args):
"""
处理 add 命令
"""
pass
def handle_commit(git, args):
"""
处理 -m <msg> 命令
"""
pass
def handle_push(git, args):
"""
处理 push 命令
"""
pass
if __name__ == '__main__':
cli()
下面我们将一步步地实现我们的 git
程序。
实现
假定我们在 argparse-git.py 文件中实现我们的 git
程序。
构建解析器
我们需要构建一个父解析器,作为程序的根解析器,程序名称指定为 git
。然后在上面添加子解析器,为后续的子命令的解析做准备:
def cli():
"""
git 命名程序入口
"""
parser = argparse.ArgumentParser(prog='git')
subparsers = parser.add_subparsers(
title='These are common Git commands used in various situations',
metavar='command')
add_subparsers
中的 title
和 metavar
参数主要用于命令行帮助信息,最终的效果如下:
usage: git [-h] command ...
optional arguments:
-h, --help show this help message and exit
These are common Git commands used in various situations:
command
...
status 子命令
我们需要在 cli
函数中添加一个用于解析 status
命令的子解析器 status_parser
,并指定其对应的处理函数为 handle_status
。
def cli():
...
# status
status_parser = subparsers.add_parser(
'status',
help='Show the working tree status')
status_parser.set_defaults(handle=handle_status)
需要说明的是,在 status_parser.set_defaults
函数中,能接收任意名称的关键字参数,这个参数值会存放于父解析器解析命令行参数后的变量中。
比如,在本文示例程序中,我们为每个子解析器定义了 handle
,那么 args = parser.parse_args()
中的 args
将具有 handle
属性,我们传入不同的子命令,那么这个 handle
就是不同的响应函数。
定义了 status
的子解析器后,我们再实现下 handle_status
即可实现 status
命令的响应:
def handle_status(git, args):
"""
处理 status 命令
"""
cmd = ['git', 'status']
output = git.execute(cmd)
print(output)
不难看出,我们最后调用了真正的 git status
来实现,并打印了输出。
你可能会对 handle_status
的函数签名感到困惑,这里的 git
和 args
是怎么传入的呢?这其实是由我们自己控制的,将在本文最后讲解。
add 子命令
同样,我们需要在 cli
函数中添加一个用于解析 add
命令的子解析器 add_parser
,并指定其对应的处理函数为 handle_add
。
额外要做的是,要在子解析器 add_parser
上添加一个 pathspec
位置参数,且其数量是任意的:
def cli():
...
# add
add_parser = subparsers.add_parser(
'add',
help='Add file contents to the index')
add_parser.add_argument(
'pathspec',
help='Files to add content from',
nargs='*')
add_parser.set_defaults(handle=handle_add)
然后,就是实现 handle_add
函数,我们需要用到表示文件路径的 args.pathspec
:
def handle_add(git, args):
"""
处理 add 命令
"""
cmd = ['git', 'add'] + args.pathspec
output = git.execute(cmd)
print(output)
commit 子命令
同样,我们需要在 cli
函数中添加一个用于解析 commit
命令的子解析器 commit_parser
,并指定其对应的处理函数为 handle_commit
。
额外要做的是,要在子解析器 commit_parser
上添加一个 -m
/--message
选项参数,且要求必填:
def cli():
...
# commit
commit_parser = subparsers.add_parser(
'commit',
help='Record changes to the repository')
commit_parser.add_argument(
'--message', '-m',
help='Use the given <msg> as the commit message',
metavar='msg',
required=True)
commit_parser.set_defaults(handle=handle_commit)
然后,就是实现 handle_commit
函数,我们需要用到表示提交信息的 args.message
:
def handle_commit(git, args):
"""
处理 -m <msg> 命令
"""
cmd = ['git', 'commit', '-m', args.message]
output = git.execute(cmd)
print(output)
push 子命令
同样,我们需要在 cli
函数中添加一个用于解析 push
命令的子解析器 push_parser
,并指定其对应的处理函数为 handle_push
。
它同 status
子命令的实现方式一致:
def cli():
...
# push
push_parser = subparsers.add_parser(
'push',
help='Update remote refs along with associated objects')
push_parser.set_defaults(handle=handle_push)
然后,就是实现 handle_push
函数,和 handle_status
类似:
def handle_push(git, args):
cmd = ['git', 'push']
output = git.execute(cmd)
print(output)
解析参数
在定义完父子解析器,并添加参数后,我们就需要对参数做解析,这项工作也是实现在 cli
函数中:
def cli():
...
git = Git(os.getcwd())
args = parser.parse_args()
if hasattr(args, 'handle'):
args.handle(git, args)
else:
parser.print_help()
- 通过
git.cmd.Git
实例化出git
对象,用来和git
仓库交互 - 通过
parser.parse_args()
解析命令行 - 通过
hasattr(args, 'handle')
判断是否输入了子命令。- 由于每个子解析器都定义了
handle
,那么如果当用户在命令行不输入任何命令时,args
就没有handle
属性,那么我们就输出帮助信息 - 如果用户输入了子命令,那么就调用
args.handle
,传入git
和args
对象,用以处理对应命令
- 由于每个子解析器都定义了
至此,我们就实现了一个简单的 git
命令行,使用 python argparse-git.py -h
查看帮助如下:
usage: git [-h] command ...
optional arguments:
-h, --help show this help message and exit
These are common Git commands used in various situations:
command
status Show the working tree status
add Add file contents to the index
commit Record changes to the repository
push Update remote refs along with associated objects
然后我们就可以愉快地使用亲手打造的 git
程序啦!
想看整个源码,请戳 argparse-git.py 。
小结
本文简单介绍了日常工作中常用的 git
命令,然后提出实现它的思路,最终一步步地使用 argparse
和 gitpython
实现了 git
程序。是不是很有成就感呢?
关于 argparse
的讲解将告一段落,回顾下 argparse
的四步曲,加上今天的内容,感觉它还是挺清晰、简单的。
不过,这还只是打开了命令行大门的一扇门。
你是否想过,argparse
的四步曲虽然理解简单,但略微麻烦。有没有更简单的方式?
如果我很熟悉命令行帮助语法,我能不能写个帮助字符串就把所有的命令行元信息给定义出来?然后就直接轻松愉快地获取解析后的参数信息呢?
在下篇文章中,将为大家讲解另一个站在一个全新的思路,又无比强大的库 docopt
。
"""
Git 命令行
前置条件:
1. pip install gitpython
2. 安装了 git
支持的命令:
git status
git add [pathspec [pathspec ...]]
git commit --message msg
-m msg
git push
"""
import os
import argparse
from git.cmd import Git
def cli():
"""
git 命名程序入口
"""
parser = argparse.ArgumentParser(prog='git')
subparsers = parser.add_subparsers(
title='These are common Git commands used in various situations',
metavar='command')
# status
status_parser = subparsers.add_parser(
'status',
help='Show the working tree status')
status_parser.set_defaults(handle=handle_status)
# add
add_parser = subparsers.add_parser(
'add',
help='Add file contents to the index')
add_parser.add_argument(
'pathspec',
help='Files to add content from',
nargs='*')
add_parser.set_defaults(handle=handle_add)
# commit
commit_parser = subparsers.add_parser(
'commit',
help='Record changes to the repository')
commit_parser.add_argument(
'--message', '-m',
help='Use the given <msg> as the commit message',
metavar='msg',
required=True)
commit_parser.set_defaults(handle=handle_commit)
# push
push_parser = subparsers.add_parser('push', help='Update remote refs along with associated objects')
push_parser.set_defaults(handle=handle_push)
# parse args
git = Git(os.getcwd())
args = parser.parse_args()
if hasattr(args, 'handle'):
args.handle(git, args)
else:
parser.print_help()
def handle_status(git, args):
"""
处理 status 命令
"""
cmd = ['git', 'status']
output = git.execute(cmd)
print(output)
def handle_add(git, args):
"""
处理 add 命令
"""
cmd = ['git', 'add'] + args.pathspec
output = git.execute(cmd)
print(output)
def handle_commit(git, args):
"""
处理 -m <msg> 命令
"""
cmd = ['git', 'commit', '-m', args.message]
output = git.execute(cmd)
print(output)
def handle_push(git, args):
"""
处理 push 命令
"""
cmd = ['git', 'push']
output = git.execute(cmd)
print(output)
if __name__ == '__main__':
cli()