命令帮助(通过 -h),其中 `argparse` 是范围检查输入端口号

Posted

技术标签:

【中文标题】命令帮助(通过 -h),其中 `argparse` 是范围检查输入端口号【英文标题】:Command help (via -h) where `argparse` is range checking input port number 【发布时间】:2016-10-07 10:21:37 【问题描述】:

我正在使用argparse 来解析我的python3 程序的输入。我最近被要求对一些数字输入进行范围检查,这似乎是个好主意。 Argparse 有一个工具可以做到这一点。

数字输入是端口号,通常在 0-65535 范围内,因此我将解析命令行更改为:

import argparse
cmd_parser = argparse.ArgumentParser()
cmd_parser = add_argument('-p', help='Port number to connect to', dest='cmd_port', default=1234, type=int, choices=range(0,65536))
cmd_parser.parse_args(['-h'])

然而,现在,当我请求帮助时,我被来自 argparse 的所有可能值淹没了。例如。

optional arguments:
    -h, --help            show this help message and exit
    -p 0,1,2,3,4,5,6,7,8,9,10,11,12,13 ...
    65478,65479,65480,65481,65482,65483,65484,65485,65486,65487,65488,65489,
    65490,65491,65492,65493,65494,65495,65496,65497,65498,65499,65500,65501,
    65502,65503,65504,65505,65506,65507,65508,65509,65510,65511,65512,65513,
    65514,65515,65516,65517,65518,65519,65520,65521,65522,65523,65524,65525,
    65526,65527,65528,65529,65530,65531,65532,65533,65534,65535
                    Port number to connect to
...

它列出了该范围内的每个端口。有没有办法截断它或让它实现它的范围(0-65535)或让它使用省略号或其他东西让它更漂亮一点?我唯一的选择是使用 if 语句明确范围检查我的输入吗?

我一直在谷歌上搜索这个,但我很难找到人们使用 argparse 和指定选项的例子。我还检查了有关 argparse 的文档,但没有看到任何有用的信息。 https://docs.python.org/2/library/argparse.html

【问题讨论】:

你试过猴子补丁parser.print_help() 以获得所需的输出吗? print_help() 命令显示相同的夸张输出(显示 0 到 65535 之间的每个数字)。 我会尝试在 python 的问题跟踪器中打开一个错误。在生成输出时,argparse 似乎正在遍历所有可能的选择并显示所有选项。他们可以添加一个选项,仅在可用的情况下仅显示 N 选项(例如第一个 N-1 和最后一个)。 顺便说一句:非常相关的问题:***.com/questions/9366369/… 我不会以重复的形式结束,因为我没有找到令人满意的答案并且已经过去了很多年。 我看到我也提到了那个 SO 链接中的错误/问题。无论如何,对于您的数字范围,我认为使用自定义 type 进行测试比修补 choices 列表更容易。 【参考方案1】:

我使用的答案灵感来自@hpaulj 的答案。但是,我同意问题确实出在 argparse 的脚下。当我的计算机挂起尝试分配千兆字节的空间只是为了输出帮助文本时,我发现了这个 SO 查询。

我对 hpaulj 的“范围”类的问题是,当使用较大的上限时,消息仍然会推出非常大的数字。 以下类默认使用无限上限。

class ArgRange(object):
    from decimal import Decimal
    huge = Decimal('+infinity')
    huge_str = ':.4E'.format(huge)

    def __init__(self, start, stop=huge, n=3):
        self.start = start
        self.stop = stop
        self.n = n

    def __contains__(self, key):
        return self.start <= key < self.stop

    def __iter__(self):
        if self.stop < self.start+(self.n*3):
            for i in range(self.start, self.stop):
                yield i
        else:
            for i in range(self.start, self.start+self.n):
                yield I
            if self.stop is self.huge:
                yield '...' + huge_str
            else:
                yield '...'
                for i in range(self.stop - self.n, self.stop):
                    yield i

bounds = ArgRange(2)
balance = ArgRange(0, 1000)
parser = argparse.ArgumentParser(description="Do something fun")
parser.add_argument("width", type=int, choices=bounds,  default=9)
parser.add_argument("height", type=int, choices=balance, default=200)

使用不正确的值,错误是:

argument width: invalid choice: 1 (choose from 2, 3, 4, '...infinity')

argument height: invalid choice: 2000 (choose from 0, 1, 2, '...', 997, 998, 999)

用法如下:

usage: test.py 
           2,3,4,...infinity 0,1,2,...,997,998,999

【讨论】:

【参考方案2】:

关于大型choices 列表的格式设置存在一个Python 错误/问题。目前帮助中choices的格式为

    def _metavar_formatter: ...
        choice_strs = [str(choice) for choice in action.choices]
        result = '%s' % ','.join(choice_strs)

对于以下错误:

def _check_value(self, action, value):
    # converted value must be one of the choices (if specified)
    if action.choices is not None and value not in action.choices:
        args = 'value': value,
                'choices': ', '.join(map(repr, action.choices))
        msg = _('invalid choice: %(value)r (choose from %(choices)s)')
        raise ArgumentError(action, msg % args)

所以它期望choices 是一个可迭代的,但不会压缩列表或使其美观。请注意,唯一的测试是 value not in action.choiceschoices 是一个非常简单的功能。

我在之前的 SO 问题中提到了这个问题:http://bugs.python.org/issue16468。涉及提议的补丁,所以不要指望很快会有任何修复。

我建议使用您自己的type 测试而不是choices。或者解析后自己做范围测试。

def myrange(astring):
   anint = int(astring)
   if anint in range(0,1000):
        return anint
   else:
        raise ValueError()
        # or for a custom error message
        # raise argparse.ArgumentTypeError('valid range is ...')

parser.add_argument('value',type=myrange,metavar='INT',help='...')

另一个解决范围选择的旧 (2012) SO 问题。答案建议使用帮助格式化程序修复和自定义类型

Python's argparse choices constrained printing

Python argparse choices from an infinite set

=============================

出于好奇,我定义了一个自定义的range 类。它的行为类似于in 测试的常规范围(没有step),但在用作迭代器时返回自定义值。

class Range(object):
    def __init__(self, start, stop, n=3):
        self.start = start
        self.stop = stop
        self.n = n
    def __contains__(self, key):
        return self.start<=key<self.stop   
    def __iter__(self):
        if self.stop<(self.start+(self.n*3)):
            for i in range(self.start, self.stop):
                yield i
        else:
            for i in range(self.start, self.start+self.n):
                yield i
            yield '...'
            for i in range(self.stop-self.n, self.stop):
                yield i

当与参数一起使用时

parser.add_argument("-p",type=int, choices=Range(2,10,2))

它产生

1455:~/mypy$ python stack37680645.py -p 3
Namespace(p=3)

1458:~/mypy$ python stack37680645.py -h
usage: stack37680645.py [-h] [-p 2,3,...,8,9]

optional arguments:
  -h, --help        show this help message and exit
  -p 2,3,...,8,9

错误消息不是我想要的,但很接近

1458:~/mypy$ python stack37680645.py -p 30
usage: stack37680645.py [-h] [-p 2,3,...,8,9]
stack37680645.py: error: argument -p: invalid choice: 30 (choose from 2, 3, '...', 8, 9)

如果choices 格式允许action.choices 对象创建自己的strrepr 字符串会更好。

实际上iter 可以很简单(只有一个字符串):

def __iter__(self):
    yield 'a custom list'

另一种选择是使用metavar 来控制用法/帮助显示,并使用__iter__ 来控制错误显示。

使用metavar 时需要注意的一点。 usage 格式化程序在元变量中有空格、'()' 和 '[]' 等特殊字符时不起作用,尤其是当使用行扩展到 2 行或更多行时。这是一个已知的错误/问题。

【讨论】:

这很好。我对其进行了一些编辑,并在下面添加了我的答案-但荣誉是你的。【参考方案3】:

使用自定义操作...

import argparse

class PortAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if not 0 < values < 2**16:
            raise argparse.ArgumentError(self, "port numbers must be between 0 and 2**16")
        setattr(namespace, self.dest, values)

cmd_parser = argparse.ArgumentParser()
cmd_parser.add_argument('-p',
                        help='Port number to connect to',
                        dest='cmd_port',
                        default=1234,
                        type=int,
                        action=PortAction,
                        metavar="0..65535")

无效的端口号将根据引发的 ArgumentError 显示错误消息。如果输入值 65536,将打印以下行:

error: argument -p: port numbers must be between 0 and 2**16

将根据显示的元变量打印使用和帮助消息

【讨论】:

【参考方案4】:

Monkey 修补 print_help 以获得所需的输出

def my_help(): print "0-65535 range"
cmd_parser = argparse.ArgumentParser()
cmd_parser.add_argument('-p', help='Port number to connect to',   dest='cmd_port', default=1234, type=int, choices=range(0,65536))
cmd_parser.print_help = my_help
cmd_parser.parse_args()

【讨论】:

【参考方案5】:

提供一个明确的metavar 参数,而不是让argparse 为您生成一个。

cmd_parser.add_argument('-p',
                        help='Port number to connect to',
                        dest='cmd_port',
                        default=1234,
                        type=int,
                        choices=range(0,65536),
                        metavar="0..65535")

【讨论】:

这不太行。对于字母数字输入,这很好。为了获得帮助,它使它看起来更好,但如果我提供的参数超出范围,它仍会显示完整的选项列表。例如。 -p -5,然后显示所有数字。我想我必须将它与端口号类结合起来才能达到最佳效果。 哦,天哪。我转变了;这应该在argparse 本身中修复:) @LawfulEvil,请参阅我的答案以解决参数超出范围问题。和端口号类差不多,但是感觉更像argparsey...【参考方案6】:

只需在add_argument 中将int 用作type,并手动验证它是否在允许的范围内。或者,使用您自己的类型,它有一个为您进行检查的构造函数和一个用于隐式转换的 __int__ 方法:

class portnumber:
    def __init__(self, string):
        self._val = int(string)
        if (not self._val > 0) or (not self.val < 2**16):
            raise argparse.ArgumentTypeError("port numbers must be integers between 0 and 2**16")
    def __int__(self):
        return self._val

...

parser.add_argument("-p",type=portnumber)

【讨论】:

为什么不if not 0 &lt; self._val &lt; 2**16?更好读:如果值不在0-2**16范围内 改用class portnumber(int) 所以我应该使用int(args.p) 访问这个参数?因为在命名空间中出现的是一个portnumber 实例。

以上是关于命令帮助(通过 -h),其中 `argparse` 是范围检查输入端口号的主要内容,如果未能解决你的问题,请参考以下文章

argparse模块

在 argparse 中从变量而不是命令行获取值

python标准库之argparse

如何使用 2 个位置参数在 argparse 上打印帮助界面?

python命令行参数模块argparse

Argparse 命令行解析模块常用参数