【问题标题】:How to parse multiple nested sub-commands using python argparse?如何使用 python argparse 解析多个嵌套的子命令?
【发布时间】:2012-05-13 23:03:48
【问题描述】:

我正在实现一个具有如下界面的命令行程序:

cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]

我已经通过argparse documentation。我可以在argparse 中使用add_argument 将GLOBAL_OPTIONS 实现为可选参数。而{command [COMMAND_OPTS]} 使用Sub-commands。

从文档看来我只能有一个子命令。但正如您所见,我必须实现一个或多个子命令。使用 argparse 解析此类命令行参数的最佳方法是什么?

【问题讨论】:

  • 我认为这不是子命令的用途。从文档中可以看出,这实质上是为了控制单独的不同子程序。你看过argument groups吗?
  • distutils ./setup.py 也有这种风格的 CLI 界面,看看他们的源代码会很有趣。

标签: python command-line-arguments argparse


【解决方案1】:

我想出了同样的问题,看来我得到了更好的答案。

解决方案是我们不能简单地将 subparser 与另一个 subparser 嵌套,而是我们可以在 subparser 之后添加一个 parser 跟在另一个 subparser 之后。

代码告诉你怎么做:

parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--user', '-u',
                    default=getpass.getuser(),
                    help='username')
parent_parser.add_argument('--debug', default=False, required=False,
                        action='store_true', dest="debug", help='debug flag')
main_parser = argparse.ArgumentParser()
service_subparsers = main_parser.add_subparsers(title="service",
                    dest="service_command")
service_parser = service_subparsers.add_parser("first", help="first",
                    parents=[parent_parser])
action_subparser = service_parser.add_subparsers(title="action",
                    dest="action_command")
action_parser = action_subparser.add_parser("second", help="second",
                    parents=[parent_parser])

args = main_parser.parse_args()

【讨论】:

  • 是的,argparse 确实允许嵌套子解析器。但我只在其他地方看到过它们——在 Python 问题的测试用例中,bugs.python.org/issue14365
  • 这假定命令具有嵌套结构。但问题是要求“并行”命令
  • 在这种情况下,我正在尝试创建多个子解析器,例如service,因为我对命令进行了划分。说我不能有重复的命令?我想要类似command --user us pass --debug false subcommandone subcommandoption 或command --user us pass --debug false subcommandtwo subcommandoption 或commandtwo --user us pass subcommandthree subcommandoption 的东西。我也打算相应地添加完整的帮助文档。代码将以python test.py command --user us pass --debug false subcommandtwo subcommandoption 运行
【解决方案2】:

@mgilson 对这个问题有一个很好的answer。但是我自己拆分 sys.argv 的问题是我丢失了 Argparse 为用户生成的所有很好的帮助消息。所以我最终这样做了:

import argparse

## This function takes the 'extra' attribute from global namespace and re-parses it to create separate namespaces for all other chained commands.
def parse_extra (parser, namespace):
  namespaces = []
  extra = namespace.extra
  while extra:
    n = parser.parse_args(extra)
    extra = n.extra
    namespaces.append(n)

  return namespaces

argparser=argparse.ArgumentParser()
subparsers = argparser.add_subparsers(help='sub-command help', dest='subparser_name')

parser_a = subparsers.add_parser('command_a', help = "command_a help")
## Setup options for parser_a

## Add nargs="*" for zero or more other commands
argparser.add_argument('extra', nargs = "*", help = 'Other commands')

## Do similar stuff for other sub-parsers

现在,在第一次解析后,所有链式命令都存储在 extra 中。我重新解析它,同时获取所有链式命令并为它们创建单独的命名空间。而且我得到了 argparse 生成的更好的使用字符串。

【讨论】:

  • @Flavius,在我通过调用namespace = argparser.parse_args() 从解析器获得namespace 后,我使用parser 和namespace 调用parse_extra。 extra_namespaces = parse_extra( argparser, namespace )
  • 我想我理解其中的逻辑,但你所拥有的代码中的parser 是什么。我只看到它被用来添加extra 参数。然后你在上面的评论中再次提到了它。应该是argparser?
  • @jmlopez 是的,应该是argparser。将编辑它。
  • 请注意,此解决方案对于特定于子命令的可选参数会失败。请参阅下面的解决方案 (stackoverflow.com/a/49977713/428542) 以获取替代解决方案。
  • 这是一个失败的例子。添加以下 3 行:parser_b = subparsers.add_parser('command_b', help='command_b help'); parser_b.add_argument('--baz', choices='XYZ', help='baz help'); options = argparser.parse_args(['--foo', 'command_a', 'command_b', '--baz', 'Z']);这将失败并出现错误PROG: error: unrecognized arguments: --baz Z。原因是在command_a的解析过程中,command_b的可选参数已经被解析(对于command_a的子解析器是未知的)。
【解决方案3】:

parse_known_args 返回一个命名空间和一个未知字符串列表。这类似于检查答案中的extra。

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo')
sub = parser.add_subparsers()
for i in range(1,4):
    sp = sub.add_parser('cmd%i'%i)
    sp.add_argument('--foo%i'%i) # optionals have to be distinct

rest = '--foo 0 cmd2 --foo2 2 cmd3 --foo3 3 cmd1 --foo1 1'.split() # or sys.argv
args = argparse.Namespace()
while rest:
    args,rest =  parser.parse_known_args(rest,namespace=args)
    print args, rest

产生:

Namespace(foo='0', foo2='2') ['cmd3', '--foo3', '3', 'cmd1', '--foo1', '1']
Namespace(foo='0', foo2='2', foo3='3') ['cmd1', '--foo1', '1']
Namespace(foo='0', foo1='1', foo2='2', foo3='3') []

另一种循环会给每个子解析器自己的命名空间。这允许位置名称重叠。

argslist = []
while rest:
    args,rest =  parser.parse_known_args(rest)
    argslist.append(args)

【讨论】:

  • 效果很好。但是有一个缺陷:如果某处有拼写错误的选项(例如rest = '--foo 0 cmd2 --foo2 2 --bar cmd3 --foo3 3 cmd1 --foo1 1'.split()),那么 argparse 将以error: too few arguments 结尾,而不是指出无效的选项。这是因为错误的选项将留在rest 中,直到我们用完命令参数。
  • 评论# or sys.argv应该是# or sys.argv[1:]。
【解决方案4】:

@Vikas 提供的解决方案对于特定于子命令的可选参数失败,但该方法是有效的。这是一个改进的版本:

import argparse

# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='foo help')
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')

# create the parser for the "command_a" command
parser_a = subparsers.add_parser('command_a', help='command_a help')
parser_a.add_argument('bar', type=int, help='bar help')

# create the parser for the "command_b" command
parser_b = subparsers.add_parser('command_b', help='command_b help')
parser_b.add_argument('--baz', choices='XYZ', help='baz help')

# parse some argument lists
argv = ['--foo', 'command_a', '12', 'command_b', '--baz', 'Z']
while argv:
    print(argv)
    options, argv = parser.parse_known_args(argv)
    print(options)
    if not options.subparser_name:
        break

这使用parse_known_args 而不是parse_args。一旦遇到当前子解析器未知的参数,parse_args 就会中止,parse_known_args 将它们作为返回元组中的第二个值返回。在这种方法中,剩余的参数被再次提供给解析器。因此,对于每个命令,都会创建一个新的命名空间。

请注意,在这个基本示例中,所有全局选项仅添加到第一个选项命名空间,而不是随后的命名空间。

这种方法适用于大多数情况,但有三个重要限制:

  • 不能对不同的子命令使用相同的可选参数,例如myprog.py command_a --foo=bar command_b --foo=bar。
  • 不能将任何可变长度位置参数与子命令(nargs='?' 或 nargs='+' 或 nargs='*')一起使用。
  • 解析任何已知参数,在新命令中没有“中断”。例如。在带有上述代码的PROG --foo command_b command_a --baz Z 12 中,--baz Z 将由command_b 使用,而不是由command_a 使用。

这些限制是 argparse 的直接限制。这是一个简单的例子,它显示了 argparse 的局限性——即使使用单个子命令——:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('spam', nargs='?')
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')

# create the parser for the "command_a" command
parser_a = subparsers.add_parser('command_a', help='command_a help')
parser_a.add_argument('bar', type=int, help='bar help')

# create the parser for the "command_b" command
parser_b = subparsers.add_parser('command_b', help='command_b help')

options = parser.parse_args('command_a 42'.split())
print(options)

这将引发error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b')。

原因是内部方法argparse.ArgParser._parse_known_args()太贪心了,假设command_a是可选spam参数的值。特别是,当“拆分”可选参数和位置参数时,_parse_known_args() 不会查看参数的名称(如command_a 或command_b),而只查看它们在参数列表中出现的位置。它还假设任何子命令都将使用所有剩余的参数。 argparse 的这种限制也妨碍了多命令子解析器的正确实现。不幸的是,这意味着正确的实现需要完全重写 argparse.ArgParser._parse_known_args() 方法,即 200 多行代码。

鉴于这些限制,可以选择简单地恢复为单个多选参数而不是子命令:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--bar', type=int, help='bar help')
parser.add_argument('commands', nargs='*', metavar='COMMAND',
                 choices=['command_a', 'command_b'])

options = parser.parse_args('--bar 2 command_a command_b'.split())
print(options)
#options = parser.parse_args(['--help'])

甚至可以在使用信息中列出不同的命令,见我的回答https://stackoverflow.com/a/49999185/428542

【讨论】:

    【解决方案5】:

    您始终可以自己拆分命令行(在您的命令名称上拆分sys.argv),然后只将与特定命令对应的部分传递给parse_args——您甚至可以使用相同的Namespace如果需要,可以使用命名空间关键字。

    使用itertools.groupby 对命令行进行分组很容易:

    import sys
    import itertools
    import argparse    
    
    mycommands=['cmd1','cmd2','cmd3']
    
    def groupargs(arg,currentarg=[None]):
        if(arg in mycommands):currentarg[0]=arg
        return currentarg[0]
    
    commandlines=[list(args) for cmd,args in intertools.groupby(sys.argv,groupargs)]
    
    #setup parser here...
    parser=argparse.ArgumentParser()
    #...
    
    namespace=argparse.Namespace()
    for cmdline in commandlines:
        parser.parse_args(cmdline,namespace=namespace)
    
    #Now do something with namespace...
    

    未经测试

    【讨论】:

    • 谢谢 mgilson。这是对我的问题的一个很好的解决方案,但我最终的做法略有不同。我添加了另一个answer。
    • 很好地使用itertools.groupby()! This 是我在知道 groupby() 之前所做的同样事情。
    【解决方案6】:

    改进@mgilson 的答案,我写了一个小的解析方法,它将 argv 拆分为多个部分,并将命令的参数值放入命名空间的层次结构中:

    import sys
    import argparse
    
    
    def parse_args(parser, commands):
        # Divide argv by commands
        split_argv = [[]]
        for c in sys.argv[1:]:
            if c in commands.choices:
                split_argv.append([c])
            else:
                split_argv[-1].append(c)
        # Initialize namespace
        args = argparse.Namespace()
        for c in commands.choices:
            setattr(args, c, None)
        # Parse each command
        parser.parse_args(split_argv[0], namespace=args)  # Without command
        for argv in split_argv[1:]:  # Commands
            n = argparse.Namespace()
            setattr(args, argv[0], n)
            parser.parse_args(argv, namespace=n)
        return args
    
    
    parser = argparse.ArgumentParser()
    commands = parser.add_subparsers(title='sub-commands')
    
    cmd1_parser = commands.add_parser('cmd1')
    cmd1_parser.add_argument('--foo')
    
    cmd2_parser = commands.add_parser('cmd2')
    cmd2_parser.add_argument('--foo')
    
    cmd2_parser = commands.add_parser('cmd3')
    cmd2_parser.add_argument('--foo')
    
    
    args = parse_args(parser, commands)
    print(args)
    

    它的行为正常,提供了很好的 argparse 帮助:

    对于./test.py --help:

    usage: test.py [-h] {cmd1,cmd2,cmd3} ...
    
    optional arguments:
      -h, --help        show this help message and exit
    
    sub-commands:
      {cmd1,cmd2,cmd3}
    

    对于./test.py cmd1 --help:

    usage: test.py cmd1 [-h] [--foo FOO]
    
    optional arguments:
      -h, --help  show this help message and exit
      --foo FOO
    

    并创建包含参数值的命名空间层次结构:

    ./test.py cmd1 --foo 3 cmd3 --foo 4
    Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))
    

    【讨论】:

    • 查看上面的代码,我遇到了一个问题。在第 18 行,您引用了split_argv[0],它在split_argv 中实际上是空的,因为您将[c] 附加到split_argv(最初设置为[[]])。如果您将第 7 行更改为 split_argv = [],则一切正常。
    • 我(再次)对您共享的代码进行了一些修复(修复了我遇到的一些问题)并最终得到了这个:gist.github.com/anonymous/f4be805fc3ff9e132eb1e1aa0b4f7d4b
    • 这个答案相当不错,你可以通过在add_subparsers方法stackoverflow.com/questions/8250010/…中添加dest来确定使用了哪个subparser
    【解决方案7】:

    你可以试试arghandler。这是对 argparse 的扩展,显式支持子命令。

    【讨论】:

    • arghandler 提供了一种声明子命令的好方法。但是我看不出这如何有助于解决 OP 的问题:解析 multiple 子命令。解析的第一个子命令将吃掉所有剩余的参数,因此永远不会解析更多的命令。请提供有关如何使用 arghandler 解决此问题的提示。谢谢。
    【解决方案8】:

    使用 subparsers、parse_known_args 和 parse_args (running on IDEone) 构建一个完整的 Python 2/3 示例:

    from __future__ import print_function
    
    from argparse import ArgumentParser
    from random import randint
    
    
    def main():
        parser = get_parser()
    
        input_sum_cmd = ['sum_cmd', '--sum']
        input_min_cmd = ['min_cmd', '--min']
    
        args, rest = parser.parse_known_args(
            # `sum`
            input_sum_cmd +
            ['-a', str(randint(21, 30)),
             '-b', str(randint(51, 80))] +
            # `min`
            input_min_cmd +
            ['-y', str(float(randint(64, 79))),
             '-z', str(float(randint(91, 120)) + .5)]
        )
    
        print('args:\t ', args,
              '\nrest:\t ', rest, '\n', sep='')
    
        sum_cmd_result = args.sm((args.a, args.b))
        print(
            'a:\t\t {:02d}\n'.format(args.a),
            'b:\t\t {:02d}\n'.format(args.b),
            'sum_cmd: {:02d}\n'.format(sum_cmd_result), sep='')
    
        assert rest[0] == 'min_cmd'
        args = parser.parse_args(rest)
        min_cmd_result = args.mn((args.y, args.z))
        print(
            'y:\t\t {:05.2f}\n'.format(args.y),
            'z:\t\t {:05.2f}\n'.format(args.z),
            'min_cmd: {:05.2f}'.format(min_cmd_result), sep='')
    
    def get_parser():
        # create the top-level parser
        parser = ArgumentParser(prog='PROG')
        subparsers = parser.add_subparsers(help='sub-command help')
    
        # create the parser for the "sum" command
        parser_a = subparsers.add_parser('sum_cmd', help='sum some integers')
        parser_a.add_argument('-a', type=int,
                              help='an integer for the accumulator')
        parser_a.add_argument('-b', type=int,
                              help='an integer for the accumulator')
        parser_a.add_argument('--sum', dest='sm', action='store_const',
                              const=sum, default=max,
                              help='sum the integers (default: find the max)')
    
        # create the parser for the "min" command
        parser_b = subparsers.add_parser('min_cmd', help='min some integers')
        parser_b.add_argument('-y', type=float,
                              help='an float for the accumulator')
        parser_b.add_argument('-z', type=float,
                              help='an float for the accumulator')
        parser_b.add_argument('--min', dest='mn', action='store_const',
                              const=min, default=0,
                              help='smallest integer (default: 0)')
        return parser
    
    
    if __name__ == '__main__':
        main()
    

    【讨论】:

      【解决方案9】:

      我有或多或少相同的要求:能够设置全局参数并能够链接命令并按命令行顺序执行它们。

      我最终得到了以下代码。我确实使用了这个线程和其他线程的部分代码。

      # argtest.py
      import sys
      import argparse
      
      def init_args():
      
          def parse_args_into_namespaces(parser, commands):
              '''
              Split all command arguments (without prefix, like --) in
              own namespaces. Each command accepts extra options for
              configuration.
              Example: `add 2 mul 5 --repeat 3` could be used to a sequencial
                       addition of 2, then multiply with 5 repeated 3 times.
              '''
              class OrderNamespace(argparse.Namespace):
                  '''
                  Add `command_order` attribute - a list of command
                  in order on the command line. This allows sequencial
                  processing of arguments.
                  '''
                  globals = None
                  def __init__(self, **kwargs):
                      self.command_order = []
                      super(OrderNamespace, self).__init__(**kwargs)
      
                  def __setattr__(self, attr, value):
                      attr = attr.replace('-', '_')
                      if value and attr not in self.command_order:
                          self.command_order.append(attr)
                      super(OrderNamespace, self).__setattr__(attr, value)
      
              # Divide argv by commands
              split_argv = [[]]
              for c in sys.argv[1:]:
                  if c in commands.choices:
                      split_argv.append([c])
                  else:
                      split_argv[-1].append(c)
      
              # Globals arguments without commands
              args = OrderNamespace()
              cmd, args_raw = 'globals', split_argv.pop(0)
              args_parsed = parser.parse_args(args_raw, namespace=OrderNamespace())
              setattr(args, cmd, args_parsed)
      
              # Split all commands to separate namespace
              pos = 0
              while len(split_argv):
                  pos += 1
                  cmd, *args_raw = split_argv.pop(0)
                  assert cmd[0].isalpha(), 'Command must start with a letter.'
                  args_parsed = commands.choices[cmd].parse_args(args_raw, namespace=OrderNamespace())
                  setattr(args, f'{cmd}~{pos}', args_parsed)
      
              return args
      
      
          #
          # Supported commands and options
          #
          parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
      
          parser.add_argument('--print', action='store_true')
      
          commands = parser.add_subparsers(title='Operation chain')
      
          cmd1_parser = commands.add_parser('add', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
          cmd1_parser.add_argument('add', help='Add this number.', type=float)
          cmd1_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.',
                                                     default=1, type=int)
      
          cmd2_parser = commands.add_parser('mult', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
          cmd2_parser.add_argument('mult', help='Multiply with this number.', type=float)
          cmd2_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.',
                                                     default=1, type=int)
      
          args = parse_args_into_namespaces(parser, commands)
          return args
      
      
      #
      # DEMO
      #
      
      args = init_args()
      
      # print('Parsed arguments:')
      # for cmd in args.command_order:
      #     namespace = getattr(args, cmd)
      #     for option_name in namespace.command_order:
      #         option_value = getattr(namespace, option_name)
      #         print((cmd, option_name, option_value))
      
      print('Execution:')
      result = 0
      for cmd in args.command_order:
          namespace = getattr(args, cmd)
          cmd_name, cmd_position = cmd.split('~') if cmd.find('~') > -1 else (cmd, 0)
          if cmd_name == 'globals':
              pass
          elif cmd_name == 'add':
              for r in range(namespace.repeat):
                  if args.globals.print:
                      print(f'+ {namespace.add}')
                  result = result + namespace.add
          elif cmd_name == 'mult':
              for r in range(namespace.repeat):
                  if args.globals.print:
                      print(f'* {namespace.mult}')
                  result = result * namespace.mult
          else:
              raise NotImplementedError(f'Namespace `{cmd}` is not implemented.')
      print(10*'-')
      print(result)
      

      下面是一个例子:

      $ python argstest.py --print add 1 -r 2 mult 5 add 3 mult -r 5 5
      
      Execution:
      + 1.0
      + 1.0
      * 5.0
      + 3.0
      * 5.0
      * 5.0
      * 5.0
      * 5.0
      * 5.0
      ----------
      40625.0
      

      【讨论】:

        【解决方案10】:

        另一个支持并行解析器的包是“declarative_parser”。

        import argparse
        from declarative_parser import Parser, Argument
        
        supported_formats = ['png', 'jpeg', 'gif']
        
        class InputParser(Parser):
            path = Argument(type=argparse.FileType('rb'), optional=False)
            format = Argument(default='png', choices=supported_formats)
        
        class OutputParser(Parser):
            format = Argument(default='jpeg', choices=supported_formats)
        
        class ImageConverter(Parser):
            description = 'This app converts images'
        
            verbose = Argument(action='store_true')
            input = InputParser()
            output = OutputParser()
        
        parser = ImageConverter()
        
        commands = '--verbose input image.jpeg --format jpeg output --format gif'.split()
        
        namespace = parser.parse_args(commands)
        

        命名空间变成:

        Namespace(
            input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>),
            output=Namespace(format='gif'),
            verbose=True
        )
        

        免责声明:我是作者。需要 Python 3.6。安装使用:

        pip3 install declarative_parser
        

        这里是documentation,这里是repo on GitHub。

        【讨论】:

          【解决方案11】:

          为了解析子命令,我使用了以下(参考 argparse.py 代码)。它解析子解析器参数并保留两者的帮助。没有额外的东西通过那里。

          args, _ = parser.parse_known_args()
          

          【讨论】:

            【解决方案12】:

            你可以使用包 optparse

            import optparse
            parser = optparse.OptionParser()
            parser.add_option("-f", dest="filename", help="corpus filename")
            parser.add_option("--alpha", dest="alpha", type="float", help="parameter alpha", default=0.5)
            (options, args) = parser.parse_args()
            fname = options.filename
            alpha = options.alpha
            

            【讨论】:

            • 这并不能真正回答问题。此外,不推荐使用 optparse(来自python docs“不推荐使用 optparse 模块,不会进一步开发;开发将继续使用 argparse 模块”)。
            • 抱歉投反对票,但这并没有解决我提出的问题。
            猜你喜欢
            • 2017-01-31
            • 2018-01-04
            • 2019-11-28
            • 2019-09-23
            • 2020-09-06
            • 2013-02-27
            • 1970-01-01
            • 2013-09-11
            相关资源
            最近更新 更多