argparse和optparse的子命令替代方案

12

是否有适用于子命令的直观替代方案可用于argparse/optparse?它们都不好 - 要么是疯狂的配置,要么是疯狂的输出。

现实世界的例子(盗用的,不被需要):

>>> parser = argparse.ArgumentParser()
>>> subparsers = parser.add_subparsers(title='subcommands',
...                                    description='valid subcommands',
...                                    help='additional help')
>>> subparsers.add_parser('foo')
>>> subparsers.add_parser('bar')
>>> parser.parse_args(['-h'])
usage:  [-h] {foo,bar} ...

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

subcommands:
  valid subcommands

  {foo,bar}   additional help

招聘:

>>> parser = cmdline.Parser(
...   tplheader='Usage: tool [command] [options]',
...   tplcommandhead='Available commands:',
...   tplfooter='Use \"tool help\" to get full list of supported commands.')
>>> parser.add('foo', help='foo.')
>>> parser.add('bar', help='bar.')
>>> parser.parse(['-h'])
Usage: tool [command] [options]
Available commands:

  foo        foo.
  bar        bar.

Use "tool help" to get full list of supported commands.

更新: 我会接受提供命令验证和解析示例的答案,该示例能够准确地给出与最后一段代码片段完全相同的帮助信息。


“output” - 是指帮助信息还是已解析值的对象? - hpaulj
你可能想要看一下Click。有关子命令的详细信息,请参阅命令和组的文档。 - Rob Kennedy
@ hpaulj,“output”是帮助信息。修复了描述。 - anatoly techtonik
5个回答

8
你可以通过稍微改变你的argparse代码来接近你请求的输出:
  1. 通过将usage参数指定为ArgumentParser来设置用法文本。
  2. 省略add_subparsers中的descriptionhelp参数。
  3. title参数更改为Available subcommands
  4. 使用metavar参数覆盖不美观的{foo,bar}文本。
  5. 使用add_parser中可用的help参数。
这是最终产品:
import argparse
parser = argparse.ArgumentParser(usage='tool [command] [options]')
subparsers = parser.add_subparsers(title='Available commands', metavar='')
subparsers.add_parser('foo', help='foo.')
subparsers.add_parser('bar', help='bar.')
parser.parse_args(['-h'])

该代码会输出以下内容:

用法:tool [命令] [选项]
可选参数: -h,--help 显示此帮助信息并退出
可用命令:
foo foo。 bar bar。

添加一个“epilog”和“add_help=False”,你会更接近答案。我对我的回答过于深思熟虑了。 - hpaulj
我故意省略了结语文本,因为这需要额外的工作才能使请求的文本成为行为的准确描述,@Hpaulj。现有的命令似乎在是否通过专用子命令或选项访问帮助方面存在分歧,而选项方法已经内置到argparse库中。 - Rob Kennedy
@RobKennedy,尝试不错。这可能是一个很好的答案,以说明为什么“argparse”无法完成工作。它看起来几乎可以,但我怀疑要精确地按要求获得所需的输出并不容易。 - anatoly techtonik
@hpaulj,它显示了“可选参数”垃圾,并且没有显示页脚。 - anatoly techtonik

1
我会尽力帮助您翻译中文。以下是需要翻译的内容:

听起来你正在寻找argh

这是主页演示文稿中的一小段。

一个具有多个命令的潜在模块化应用程序:

import argh

# declaring:

def echo(text):
    "Returns given word as is."
    return text

def greet(name, greeting='Hello'):
    "Greets the user with given name. The greeting is customizable."
    return greeting + ', ' + name

# assembling:

parser = argh.ArghParser()
parser.add_commands([echo, greet])

# dispatching:

if __name__ == '__main__':
    parser.dispatch()

当然它有效:
$ ./app.py greet Andy
Hello, Andy

$ ./app.py greet Andy -g Arrrgh
Arrrgh, Andy

网站上的帮助信息有所删减。以下是它在我的(argh 0.26.1)输出的实际内容。
$ ./app.py --help
usage: app.py [-h] {greet,echo} ...

positional arguments:
  {greet,echo}
    echo        Returns given word as is.
    greet       Greets the user with given name. The greeting is customizable.

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

./app.py -h怎么样? - anatoly techtonik
1
罐子上写的内容完全没有帮助。但它所做的事情要好得多。 - Hugues Fontenelle
https://pypi.python.org/pypi/plac 是另一个建立在argparse之上的解析器。您可以通过指定将被调用的函数来定义“命令”。它使用函数参数来填充解析器参数。 - hpaulj
@tripleee,它允许更改“位置参数:”行吗?此外,“svn help”大约有10个命令。它们都将堆叠在使用行中吗? - anatoly techtonik
@tripleee,你的自动生成帮助文档的示例与最新的 argh 0.26.1 和 Python 2/3 生成的代码不同。 =/ - anatoly techtonik
显示剩余4条评论

1

这个能赢得奖项吗? :)

自定义参数

Rob Kennedy有更好的定制化。

In [158]: parser=argparse.ArgumentParser(usage='tool [command] [options]',
  description= "Available commands:\n\n   foo    foo.\n   bar    bar.\n",
  epilog= 'Use "tool help" to get full list of supported commands',
  formatter_class=argparse.RawDescriptionHelpFormatter, add_help=False)

In [159]: parser.print_help()
usage: tool [command] [options]

Available commands:

   foo    foo.
   bar    bar.

Use "tool help" to get full list of supported commands

我所做的是使用可用参数自定义help

替代API和/或解析器?

但你其他的代码行,parse.add(),表明你不喜欢argparse方法来定义“命令”。你可以向你的解析器添加一些方法,使用更紧凑的语法,但最终仍会调用现有的subparser机制。
但也许你想用自己的解析方案来替换整个解析方案。例如,期望第一个参数是“命令”的方案。那么其他的“位置参数”呢?谁或什么处理“选项”?
你是否意识到argparse子解析器方案是建立在更基本的optionalspositionals解析方案之上的。 parser.add_subparsers命令是add_argument的一种专门形式。subparsers对象是一个位置参数,具有特殊的Action类。 {foo,bar}实际上是您为此参数定义的choices值列表(子命令的名称或别名)。 子命令本身就是解析器。

自定义前端命令解析器

如果sys.argv [1]项始终是command名称,则可以设置以下内容:
if sys.argv[1:]:
    cmd = sys.argv[1]
    rest = sys.argv[2:]
    parser = parser_dict.get(cmd, None)
    if parser:
        args = parser.parse_args(rest)
else:
    print_default_help()

parser_dict 是一个字典,将 cmd 字符串与定义的解析器相匹配。实际上这只是一个前端,它捕获第一个参数字符串,并将其余部分的处理分派给其他已定义的解析器。它们可以是混合使用的 argparseoptparse 和自定义解析器。如果它只处理第一个“命令”字符串,那么这个前端不需要太花哨。

print_default_help 只是对 parser_dict 进行漂亮的打印。

进一步思考后,我意识到 argparse 子解析器对象的 sp.choices 属性正是这样一个字典——以命令字符串为键,以解析器为值。

自定义 format_help 方法

这里有几个自定义帮助格式化程序。

一个简单的方法只获取来自 parserprog_choices_actionssubparsers._choices_actions 是一个包含各个子解析器的帮助和别名信息的对象列表。

def simple_help(parser, subparsers):
    # format a help message with just the subparser choices
    usage = "Usage: %s command [options]"%parser.prog
    desc = "Available commands:\n"
    epilog = '\nUse "%s help" to get full list of supported commands.'%parser.prog
    choices = fmt_choices(subparsers._choices_actions)
    astr = [usage]
    astr.append(desc)
    astr.extend(choices)
    astr.append(epilog)
    return '\n'.join(astr)

def fmt_choices(choices):
    # format names and help in 2 columns
    x = max(len(k.metavar) for k in choices)
    fmt = '   {:<%s}   {}'%x
    astr = []
    for k in choices:
        # k.metavar lists aliases as well
        astr.append(fmt.format(k.dest, k.help))
    return astr

这个函数是基于parser.format_help模型,利用了Formatter及其包装和间距信息。 我编写它时尽可能使用了非默认参数。 但是,很难抑制空行。
def special_help(parser, subparsers=None, usage=None, epilog=None):
    # format help message using a Formatter
    # modeled on parser.format_help
    # uses nondefault parameters where possible
    if usage is None:
        usage = "%(prog)s command [options]"
    if epilog is None:
        epilog = "Use '%(prog)s help' for command list"
    if subparsers is None:
        # find the subparsers action in the parser
        for action in parser._subparsers._group_actions:
            if hasattr(action, '_get_subactions'):
                subparsers = action
                break
        # if none found, subparsers is still None?
    if parser._subparsers != parser._positionals:
        title = parser._subparsers.title
        desc = parser._subparsers.description
    else:
        title = "Available commands"
        desc = None
    if subparsers.metavar is None:
        subparsers.metavar = '_________'
        # restore to None at end?
    formatter = parser._get_formatter()
    if parser.usage is None:
        formatter.add_usage(usage, [], [])
    else:
        formatter.add_usage(parser.usage,
            parser._actions, parser._mutually_exclusive_groups)
    # can I get rid of blank line here?
    formatter.start_section(title)
    formatter.add_text(desc)
    formatter.add_arguments([subparsers])
    formatter.end_section()
    formatter.add_text(epilog)
    return formatter.format_help()

这些可以以不同的方式调用。任何一种方式都可以替换解析器的format_help方法,并因此由-h选项生成,以及由parser.print_help()生成。
或者您可以包含一个help子命令。 这将与epilog消息相适应。 -h仍然会产生完整且难看的帮助信息。
sp3 = sp.add_parser('help')  # help='optional help message'

并测试args

if args.cmd in ['help']:
    print(simple_help(parser, sp))
    # print(special_help(parser))

另一个选项是在 parser.parse_args 之前检查 sys.argv,并在该列表不够长或包含 help 字符串时调用帮助函数。这大致是 Ipython 用于绕过常规 argparse 帮助的方法。

我可能可以通过复制粘贴多个类定义页面来破解argparser,但我宁愿使用一个简短直观的库,不会过度负担我的命令行助手。 - anatoly techtonik
通常情况下,通过更改格式化程序类的几个方法来调整argparse帮助格式化程序。显然,您必须研究足够的代码以知道在哪里进行这些少量更改。没有人会为您提供一个定制的格式化程序。 - hpaulj
1
这就是为什么这个问题涉及到argparse的替代方案,其中编写自定义格式化程序与我的问题一样容易,并且不需要花费数天时间进行辩论以获得精确的输出。 - anatoly techtonik

0

我不确定我理解你所描述的问题在哪里。

不过,我使用的是稍微不同的方法:

parser = argparse.ArgumentParser(description='My description')
parser.add_argument('-i', '--input', type=str, required=True, help='Inputfile')
parser.add_argument('-o', '--output', type=str, required=False, help='Output file')
args = parser.parse_args()
input_filename = args.input
if not args.output:
    output_filename = input_filename
else:
    output_filename = args.output

你根本不使用子命令。尝试执行 hgsvn help 命令以查看差异。 - anatoly techtonik
1
哦,现在我明白你的意思了。好主意,一旦你得到了问题的好答案,我也会开始使用它们的 :-) - Hugues Fontenelle

0

你应该看一下这里。从文档中可以看出...

  • 没有限制地进行惰性组合
  • 完全遵循Unix命令行约定
  • 支持直接从环境变量中加载值
  • 支持提示自定义值
  • 完全可嵌套和组合
  • 在Python 2和3中的使用方式相同
  • 支持文件处理
  • 附带有用的常见辅助功能(获取终端尺寸、ANSI颜色、获取直接键盘输入、清除屏幕、查找配置路径、启动应用程序和编辑器等)

使用装饰器创建参数和选项非常直观。您可以通过创建组来创建子命令,如下所示。

import click                                                    


@click.command()                                                
@click.option('--count', default=1, help='number of greetings') 
@click.argument('name')                                         
def hello(count, name):                                         
    for i in range(count):                                      
        print(f"{i}. Hello {name}")                             

@click.group()                                                  
def cli():                                                      
    pass                                                        

@cli.command()                                                  
def initdb():                                                   
    click.echo('Initialized the database')                      

@cli.command()                                                  
def dropdb():                                                   
    click.echo('Dropped the database')                          

if __name__ == "__main__":                                      
    cli() 

这段代码的输出结果是:

$ python click-example.py --help
Usage: click-example.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  dropdb
  initdb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接