命令行参数解析——argparse

发布时间 2023-04-13 16:12:05作者: LgRun

背景

命令行参数解析

示例:

# 第一种
python taos_lttb.py a b
# 第二种
python taos_lttb.py v=a a=b
# 第三种
python taos_lttb.py -v a -a b

以上三种是比较常见的传参方式。究其根本,在Python中代表两类参数:

  • args型:与输入顺序有关

    示例:

    def func(a,b):
        pass
    
    #调用
    func(*(a,b))
    
  • kwargs:与输入顺序无关,与形参名有关

    示例

    def func(a=None,b=None):
        pass
    
    #调用
    a1={'a':1,'b':2}
    a2={'b':2,'a':3}
    #示例1
    func(**a1)
    #示例3
    func(**a2)
    

具体应用过程中可能还会出现两者混用的情况:

def func1(a,b,c=None,d=None):
    pass

def func2(a,b,*,c=None,d=None):
    pass
#这里*表示分隔符,用于分隔args和kwargs型参数

现在我们重新审视上述三种命令行调用方法,优劣显而易见。

但"上什么山,唱什么歌"。每种调用方式都有其适应的场景,我们需要根据具体业务场景,做正确性抉择。

接下来将对具体的参数解析方法进行介绍。

参数解析方法

目前比较常见的参数解析方式或相关方法如下:

  • sys.argv(python标准库)
  • argparse模块(python标准库)

sys.argv

sys模块包含了一个名为argv的数组,其中包括以下内容:

  1. argv[0]包含当前Python程序的名称。
  2. argv[1:],列表的其余部分,包含传递给程序的所有Python命令行参数。

示例:

  • 定义脚本(argv.py)
# argv.py
import sys

print(f"Name of the script      : {sys.argv[0]=}")
print(f"Arguments of the script : {sys.argv[1:]=}")
  • 在命令行开始调用
$ python argv.py un deux trois quatre
Name of the script      : sys.argv[0]='argv.py'
Arguments of the script : sys.argv[1:]=['un', 'deux', 'trois', 'quatre']

输出确认sys.argv[0]的内容是Python脚本argv.py,而sys.argv列表的其余元素包含脚本['un', 'deux', 'trois', 'quatre']的参数。

总而言之,sys.argv包含所有argv.py Python命令行参数。当Python解释器执行Python程序时,它解析命令行并使用参数填充sys.argv

对于kwargs型的参数,我们也可以采用类似的方式进行传输,只不过得到的参数有可能是:

第一种:基于等号=进行拆分,但是如果对应如果包含=等号可能出现解析异常的问题;

sys.argv[1:]=['a1=un', 'a2=deux', 'a3=trois', 'a4=quatre']

第二种:将传入的kwargs直接以JSON字符串的形式传入,示例如下:

sys.argv[1:]='{"a1": "un", "a2": "deux", "a3": "trois", "a4": "quatre"}'
#然后直接json.loads,然后通过**的方式进行传参

但是存在的问题:在命令行粘贴后,如果a1的键值是SQL语句可能出现参数截断的问题,所以我们需要选择另一种方法进行解析——argparse

argparse

帮助文档

官方帮助文档

示例

# -*- coding=utf-8-*-

import argparse


def _get_input_str(x):
    """强制处理单引号问题"""
    return x.strip('"').strip("'")


class SQLAction(argparse.Action):
    """sql中空格的问题"""

    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, ' '.join(values))


class MyAction(argparse.Action):
    """自定义示例"""

    def __call__(self, parser, namespace, values, option_string=None):
        tmp = []
        for i in values:
            tmp.extend(i.split())
        # setattr(namespace, self.dest, ' '.join(values))
        setattr(namespace, self.dest, tmp)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(allow_abbrev=False,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("-u", default='root', dest="user_name",
                        type=_get_input_str, nargs='?',
                        help="请输入用户名")
    parser.add_argument('-sql', default=None, dest="select_com",
                        nargs='+',
                        help="sql查询语句",
                        action=SQLAction)
    parser.add_argument('-p', default="taosdata", dest='pwd',
                        help='用户密码', nargs='?',
                        type=_get_input_str)
    parser.add_argument('-t', default=30, dest='timeout', type=int, nargs='?',
                        help="响应时间:秒")
    parser.add_argument('-th', default=100000,
                        dest='thres', type=int, nargs='?',
                        help="阈值")
    args = parser.parse_args()
    input_kwargs = args.__dict__.copy()
    input_kwargs["select_com"] = _get_input_str(input_kwargs["select_com"])
    # input_kwargs["col_name"] = input_kwargs["col_name"][0].split()
    print(f"input_kwargs={input_kwargs}")

调用示例:

(pymatest) E:\python_scripts\python>python argv.py -u root -sql "select * from table" -p 123456
input_kwargs={'user_name': 'root', 'select_com': 'select * from table', 'pwd': '123456', 'timeout': 30, 'thres': 100000}

argparse相关核心参数解释:

  • allow_abbrev:如果缩写是无歧义的,则允许缩写长选项 (默认值:True);

  • default:默认值;

  • dest:被添加到 parse_args() 所返回对象上的属性名,即变量名,也可以直接点是形参名;

  • help: 帮助信息,是一个包含参数简短描述的字符串;当用户请求帮助时(一般是通过在命令行中使用 -h--help 的方式),这些 help 描述将随每个参数一同显示:

    (pymatest) E:\python_scripts\python>python argv.py -h
    usage: argv.py [-h] [-u [USER_NAME]] [-sql SELECT_COM [SELECT_COM ...]] [-p [PWD]] [-t [TIMEOUT]] [-th [THRES]]
    
    optional arguments:
      -h, --help            show this help message and exit
      -u [USER_NAME]        请输入用户名
      -sql SELECT_COM [SELECT_COM ...]
                            sql查询语句
      -p [PWD]              用户密码
      -t [TIMEOUT]          响应时间:秒
      -th [THRES]           阈值
    
    
  • type:命令行参数应当被转换成的类型。默认情况下,解析器会将命令行参数当作简单字符串读入。 然而,命令行字符串经常应当被解读为其他类型,例如 floatintadd_argument()type 关键字允许执行任何必要的类型检查和类型转换

    • 如果 type 关键字使用了 default 关键字,则类型转换器仅会在默认值为字符串时被应用。

    传给 type 的参数可以是任何接受单个字符串的可调用对象。 如果函数引发了 ArgumentTypeError, TypeErrorValueError,异常会被捕获并显示经过良好格式化的错误消息。 其他异常类型则不会被处理。

    示例:

    import argparse
    import pathlib
    
    parser = argparse.ArgumentParser()
    parser.add_argument('count', type=int)
    parser.add_argument('distance', type=float)
    parser.add_argument('street', type=ascii)
    parser.add_argument('code_point', type=ord)
    parser.add_argument('source_file', type=open)
    parser.add_argument('dest_file', type=argparse.FileType('w', encoding='latin-1'))
    parser.add_argument('datapath', type=pathlib.Path)
    
    • 不建议将 bool() 函数用作类型转换器。 它所做的只是将空字符串转为 False 而将非空字符串转为 True。 这通常不是用户所想要的。

    • 通常,type 关键字是仅应被用于只会引发上述三种被支持的异常的简单转换的便捷选项。 任何具有更复杂错误处理或资源管理的转换都应当在参数被解析后由下游代码来完成。

    • 例如,JSON 或 YAML 转换具有复杂的错误情况,要求给出比 type 关键字所能给出的更好的报告。 JSONDecodeError 将不会被良好地格式化而 FileNotFound 异常则完全不会被处理。

    • 即使 FileType 在用于 type 关键字时也存在限制。 如果一个参数使用了 FileType 并且有一个后续参数出错,则将报告一个错误但文件并不会被自动关闭。 在此情况下,更好的做法是等待直到解析器运行完毕再使用 with 语句来管理文件。

    • 对于简单地检查一组固定值的类型检查器,请考虑改用 choices 关键字。

  • nargs:定义参数个数和形式。

    • N:命令行中的N个参数被聚集在一个列表中。
    parser = argparse.ArgumentParser()
    parser.add_argument('--foo', nargs=2)
    parser.add_argument('bar', nargs=1)
    parser.parse_args('c --foo a b'.split())
    Namespace(bar=['c'], foo=['a', 'b'])
    
    • ?:如果可能的话,会从命令行中消耗一个参数,并产生一个单独项。 如果当前没有命令行参数,将会产生 default 值。 注意对于可选参数来说,还有一个额外情况 —— 出现了选项字符串但没有跟随命令行参数,在此情况下将会产生 const 值。 一些说明这种情况的例子如下:
    parser = argparse.ArgumentParser()
    parser.add_argument('--foo', nargs='?', const='c', default='d')
    parser.add_argument('bar', nargs='?', default='d')
    parser.parse_args(['XX', '--foo', 'YY'])
    Namespace(bar='XX', foo='YY')
    parser.parse_args(['XX', '--foo'])
    Namespace(bar='XX', foo='c')
    parser.parse_args([])
    Namespace(bar='d', foo='d')
    
    • *:所有当前命令行参数被聚集到一个列表中。注意通过 nargs='*' 来实现多个位置参数通常没有意义,但是多个选项是可能的。
    • +:和 '*' 类似,所有当前命令行参数被聚集到一个列表中。另外,当前没有至少一个命令行参数时会产生一个错误信息。
    • 如果不提供 nargs 命名参数,则消耗参数的数目将被 action 决定。通常这意味着单一项目(非列表)消耗单一命令行参数。
  • choices:定义枚举型变量

    parser = argparse.ArgumentParser(prog='game.py')
    parser.add_argument('move', choices=['rock', 'paper', 'scissors'])
    parser.parse_args(['rock'])
    Namespace(move='rock')
    parser.parse_args(['fire'])
    usage: game.py [-h] {rock,paper,scissors}
    game.py: error: argument move: invalid choice: 'fire' (choose from 'rock',
    'paper', 'scissors')
    
  • requires参数:定义哪些参数是必填哪些参数是选填的

parser = argparse.ArgumentParser()
parser.add_argument('--foo', required=True)
parser.parse_args(['--foo', 'BAR'])
Namespace(foo='BAR')
parser.parse_args([])
usage: [-h] --foo FOO
: error: the following arguments are required: --foo
  • -*或者--*:来源,用来定义参数指向

历史渊源

Standards标准

A few available standards provide some definitions and guidelines to promote consistency for implementing commands and their arguments. These are the main UNIX standards and references:
一些可用的标准提供了一些定义和指导方针,以促进实现命令及其参数的一致性。以下是主要的UNIX标准和参考:

The standards above define guidelines and nomenclatures for anything related to programs and Python command-line arguments. The following points are examples taken from those references:
上面的标准定义了与程序和Python命令行参数相关的任何内容的指南和术语。以下几点是从这些参考文献中摘录的例子:

  • POSIX

    :

    • A program or utility is followed by options, option-arguments, and operands.
      程序或实用程序后面是选项、选项参数和操作数。
    • All options should be preceded with a hyphen or minus (-) delimiter character.
      所有选项前面都应加上连字符或减号(-)分隔符。
    • Option-arguments should not be optional.
      Option-arguments不应该是可选的。
  • GNU

    :

    • All programs should support two standard options, which are --version and --help.
      所有程序应支持两个标准选项,即--version--help
    • Long-named options are equivalent to the single-letter Unix-style options. An example is --debug and -d.
      长名称选项等同于单字母Unix样式选项。一个例子是--debug-d
  • docopt多科普特

    :

    • Short options can be stacked, meaning that -abc is equivalent to -a -b -c.
      短期权可以堆叠,这意味着-abc等同于-a -b -c
    • Long options can have arguments specified after a space or the equals sign (=). The long option --input=ARG is equivalent to --input ARG.
      长选项可以在空格或等号(=)后指定参数。长选项--input=ARG相当于--input ARG

These standards define notations that are helpful when you describe a command. A similar notation can be used to display the usage of a particular command when you invoke it with the option -h or --help.
这些标准定义了在描述命令时非常有用的符号。当您使用选项-h--help调用特定命令时,可以使用类似的符号来显示该命令的用法。

The GNU standards are very similar to the POSIX standards but provide some modifications and extensions. Notably, they add the long option that’s a fully named option prefixed with two hyphens (--). For example, to display the help, the regular option is -h and the long option is --help.
GNU标准与POSIX标准非常相似,但提供了一些修改和扩展。值得注意的是,他们添加了长选项,这是一个以两个连字符(--)为前缀的完整命名选项。例如,要显示帮助,常规选项为-h,长选项为--help

Note: You don’t need to follow those standards rigorously. Instead, follow the conventions that have been used successfully for years since the advent of UNIX. If you write a set of utilities for you or your team, then ensure that you stay consistent across the different utilities.
注意:您不需要严格遵循这些标准。相反,应该遵循自UNIX出现以来已成功使用多年的约定。如果您为您或您的团队编写了一组实用程序,那么请确保您在不同的实用程序中保持一致。

_get_input_str

用于处理命令行传参过程中的一些字符串问题;

parser.add_argument("-u", dest="user_name",
                        type=str, nargs='?',
                        help="请输入用户名")
(pymatest) E:\python_scripts\python>python argv0.py -u root
input_kwargs={'user_name': 'root', 'select_com': 'aa', 'pwd': 'taosdata', 'timeout': 30, 'thres': 100000}

(pymatest) E:\python_scripts\python>python argv0.py -u "root"
input_kwargs={'user_name': 'root', 'select_com': 'aa', 'pwd': 'taosdata', 'timeout': 30, 'thres': 100000}

(pymatest) E:\python_scripts\python>python argv0.py -u 'root'
input_kwargs={'user_name': "'root'", 'select_com': 'aa', 'pwd': 'taosdata', 'timeout': 30, 'thres': 100000}

单引号时,字符串又被双引号包装了一层。

优化:

parser.add_argument("-u", dest="user_name",
                        type=_get_input_str, nargs='?',
                        help="请输入用户名")

调用:

(pymatest) E:\python_scripts\python>python argv0.py -u 'root'
input_kwargs={'user_name': 'root', 'select_com': 'aa', 'pwd': 'taosdata', 'timeout': 30, 'thres': 100000}

Action示例


class MyAction(argparse.Action):
    """自定义示例"""

    def __call__(self, parser, namespace, values, option_string=None):
        tmp = []
        for i in values:
            tmp.extend(i.split())
        # setattr(namespace, self.dest, ' '.join(values))
        setattr(namespace, self.dest, tmp)

Action 类实现了 Action API,它是一个返回可调用对象的可调用对象,返回的可调用对象可处理来自命令行的参数。 任何遵循此 API 的对象均可作为 action 形参传给 add_argument()

Action 的实例应当为可调用对象,因此所有子类都必须重载 __call__ 方法,该方法应当接受四个形参:

  • parser - 包含此动作的 ArgumentParser 对象。
  • namespace - 将由 parse_args() 返回的 Namespace 对象。 大多数动作会使用 setattr() 为此对象添加属性。
  • values - 已关联的命令行参数,并提供相应的类型转换。 类型转换由 add_argument()type 关键字参数来指定。
  • option_string - 被用来发起调用此动作的选项字符串。 option_string 参数是可选的,且此参数在动作关联到位置参数时将被略去。

__call__ 方法可以执行任意动作,但通常将基于 destvalues 来设置 namespace 的属性。