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

后端 未结 11 1060
执念已碎
执念已碎 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:39

    parse_known_args returns a Namespace and a list of unknown strings. This is similar to the extra in the checked answer.

    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
    

    produces:

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

    An alternative loop would give each subparser its own namespace. This allows overlap in positionals names.

    argslist = []
    while rest:
        args,rest =  parser.parse_known_args(rest)
        argslist.append(args)
    
    0 讨论(0)
  • 2020-11-28 21:42

    I came up with the same qustion, and it seems i have got a better answer.

    The solution is we shall not simply nest subparser with another subparser, but we can add subparser following with a parser following another subparser.

    Code tell you how:

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

    Another package which supports parallel parsers is "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)
    

    and namespace becomes:

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

    Disclaimer: I am the author. Requires Python 3.6. To install use:

    pip3 install declarative_parser
    

    Here is the documentation and here is the repo on GitHub.

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

    Improving on the answer by @mgilson, I wrote a small parsing method which splits argv into parts and puts values of arguments of commands into hierarchy of namespaces:

    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)
    

    It behaves properly, providing nice argparse help:

    For ./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}
    

    For ./test.py cmd1 --help:

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

    And creates a hierarchy of namespaces containing the argument values:

    ./test.py cmd1 --foo 3 cmd3 --foo 4
    Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))
    
    0 讨论(0)
  • 2020-11-28 21:46

    @mgilson has a nice answer to this question. But problem with splitting sys.argv myself is that i lose all the nice help message Argparse generates for the user. So i ended up doing this:

    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
    

    Now after first parse all chained commands are stored in extra. I reparse it while it is not empty to get all the chained commands and create separate namespaces for them. And i get nicer usage string that argparse generates.

    0 讨论(0)
提交回复
热议问题