How to parse multiple nested sub-commands using python argparse?

后端 未结 11 1065
执念已碎
执念已碎 2020-11-28 20:52

I am implementing a command line program which has interface like this:

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


        
相关标签:
11条回答
  • 2020-11-28 21:24

    Built a full Python 2/3 example with subparsers, parse_known_args and parse_args (running on IDEone):

    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()
    
    0 讨论(0)
  • 2020-11-28 21:29

    You can always split up the command-line yourself (split sys.argv on your command names), and then only pass the portion corresponding to the particular command to parse_args -- You can even use the same Namespace using the namespace keyword if you want.

    Grouping the commandline is easy with 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...
    

    untested

    0 讨论(0)
  • 2020-11-28 21:35

    The solution provide by @Vikas fails for subcommand-specific optional arguments, but the approach is valid. Here is an improved version:

    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
    

    This uses parse_known_args instead of parse_args. parse_args aborts as soon as a argument unknown to the current subparser is encountered, parse_known_args returns them as a second value in the returned tuple. In this approach, the remaining arguments are fed again to the parser. So for each command, a new Namespace is created.

    Note that in this basic example, all global options are added to the first options Namespace only, not to the subsequent Namespaces.

    This approach works fine for most situations, but has three important limitations:

    • It is not possible to use the same optional argument for different subcommands, like myprog.py command_a --foo=bar command_b --foo=bar.
    • It is not possible to use any variable length positional arguments with subcommands (nargs='?' or nargs='+' or nargs='*').
    • Any known argument is parsed, without 'breaking' at the new command. E.g. in PROG --foo command_b command_a --baz Z 12 with the above code, --baz Z will be consumed by command_b, not by command_a.

    These limitations are a direct limitation of argparse. Here is a simple example that shows the limitations of argparse -even when using a single subcommand-:

    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)
    

    This will raise the error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b').

    The cause is that the internal method argparse.ArgParser._parse_known_args() it is too greedy and assumes that command_a is the value of the optional spam argument. In particular, when 'splitting' up optional and positional arguments, _parse_known_args() does not look at the names of the arugments (like command_a or command_b), but merely where they occur in the argument list. It also assumes that any subcommand will consume all remaining arguments. This limitation of argparse also prevents a proper implementation of multi-command subparsers. This unfortunately means that a proper implementation requires a full rewrite of the argparse.ArgParser._parse_known_args() method, which is 200+ lines of code.

    Given these limitation, it may be an options to simply revert to a single multiple-choice argument instead of subcommands:

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

    It is even possible to list the different commands in the usage information, see my answer https://stackoverflow.com/a/49999185/428542

    0 讨论(0)
  • 2020-11-28 21:35

    You could try arghandler. This is an extension to argparse with explicit support for subcommands.

    0 讨论(0)
  • 2020-11-28 21:35

    I had more or less the same requirements: Being able to set global arguments and being able to chain commands and execute them in order of command line.

    I ended up with the following code. I did use some parts of the code from this and other threads.

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

    Below an example:

    $ 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
    
    0 讨论(0)
  • 2020-11-28 21:38

    you can use the package 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
    
    0 讨论(0)
提交回复
热议问题