Python 命令行之旅

本文来自: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_truestore_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 所支持的参数类型多种多样,可以是 intfloatbool等,比如:

>>> 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)

位置参数

位置参数 就是通过位置而非是 --- 开头的参数来指定参数值。

比如,我们可以指定两个位置参数 xy ,先添加的 x 位于第一个位置,后加入的 y 位于第二个位置。那么在命令行中输入 1 2的时候,分别对应到的就是 xy

>>> parser.add_argument('x')
>>> parser.add_argument('y')
>>> parser.parse_args(['1', '2'])
Namespace(x='1', y='2')

可选值

可选值 就是限定参数值的内容,通过 choices 入参指定。

有些情况下,我们可能需要限制用户输入参数的内容,只能在预设的几个值中选一个,那么 可选值 就派上了用场。

比如,指定文件读取方式限制为 read-onlyread-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=NN为一个数字,则要求该参数提供 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 进行操作,两类操作都需要指定 AccessKeyIdAccessKeySecret 来表明用户身份和权限。那么共享解析器就显得尤为必要,这样就可以少去重复代码。

我们可以这样做,在 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.pyaws.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.pyaws.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个子命令,由此即可实现整个功能。

在下面这个例子中,我们支持 createdelete 两个子命令,用来创建或删除指定路径。而 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_argumentaction 入参。当解析器解析参数时,会调用该类的 __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

我们将使用 argparsegitpython 库来实现这 4 个子命令。

关于 gitpython

gitpython 是一个和 git 仓库交互的 Python 第三方库。
我们将借用它的能力来实现真正的 git 逻辑。

安装:

pip install gitpython

思考

在实现前,我们不妨先思考下会用到 argparse 的哪些功能?整个程序的结构是怎样的?

argparse

  • 要实现子命令,那么之前介绍到的 嵌套解析器 必不可少
  • 当用户键入子命令时,子命令所对应的子解析器需要作出响应,那么需要用到子解析器的 set_defaults 功能
  • 针对 git add [pathspec [pathspec ...]],我们需要实现位置参数,而且数量是任意个
  • 针对 git commit --message msggit 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 中的 titlemetavar 参数主要用于命令行帮助信息,最终的效果如下:

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 的函数签名感到困惑,这里的 gitargs 是怎么传入的呢?这其实是由我们自己控制的,将在本文最后讲解。

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,传入 gitargs 对象,用以处理对应命令

至此,我们就实现了一个简单的 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 命令,然后提出实现它的思路,最终一步步地使用 argparsegitpython 实现了 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()
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值