Python argparse, provide different arguments based on parent argument value

Narthe picture Narthe · May 13, 2015 · Viewed 10k times · Source

here is what i would like to do : A command that looks like git command behavior. You don't get the same options whether you typed git commit or git checkout. But in my case i want to provide different arguments based on an argument value (a file name) like this :

>cmd file.a -h
usage: cmd filename [-opt1] [-opt2]
positional arguments:
filename         file to process
optional arguments:
-opt1            do something on files of type 'a'
-opt2            do something else on files of type 'a'

>cmd file.b -h
usage: cmd filename [-opt3] [-opt4] 
positional arguments:
filename         file to process
optional arguments:
-opt3            do something on files of type 'b'
-opt4            do something else on files of type 'b'

Is it possible to do this kind of thing using python and argparse ?
What i've tried so far is :

parser = argparse.Argument_parser(prog='cmd')
subparsers = parser.add_subparsers()

parser.add_argument('filename', 
                    help="file or sequence to process")

args = parser.parse_args(args=argv[1:])

sub_parser = subparsers.add_parser(args.filename, help="job type")
base, ext = os.path.splitext(args.filename)
if ext == 'a':
    sub_parser.add_argument("-opt1", action='store_true')
    sub_parser.add_argument("-opt2", action='store_true')
elif ext == 'b':
    sub_parser.add_argument("-opt3", action='store_true')
    sub_parser.add_argument("-opt4", action='store_true')

args = parser.parse_args(args=argv[1:])

I don't know if i should use subparsers or child parsers or groups, i'm kind of lost in all the possibilities provided by argparse

Answer

Vyktor picture Vyktor · May 13, 2015

When you take a look at parse_args() implementation you'll notice that it parses all arguments at once (it doesn't use yield to continuously generate state) so you have to prepare your structure before and not after half of the arguments would be parsed.

Taking from official example in the docs you should add subparser(s) before starting parsing like this:

import argparse

parser = argparse.ArgumentParser(prog='PROG')
subparsers = parser.add_subparsers(help='sub-command help')

# create the parser for the "a" command
parser_a = subparsers.add_parser('a', help='a help')
parser_a.add_argument("--opt1", action='store_true')
parser_a.add_argument("--opt2", action='store_true')

# create the parser for the "b" command
parser_b = subparsers.add_parser('b', help='b help')
parser_b.add_argument("--opt3", action='store_true')
parser_b.add_argument("--opt4", action='store_true')

# parse some argument lists
print(parser.parse_args())

And the output (in command line), help is nicely printed:

D:\tmp>s.py -h
usage: PROG [-h] {a,b} ...

positional arguments:
  {a,b}       sub-command help
    a         a help
    b         b help

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

A arguments are parsed

D:\tmp>s.py a --opt1
Namespace(opt1=True, opt2=False)

B arguments are parsed

D:\tmp>s.py b
Namespace(opt3=False, opt4=False)

Also with args:

D:\tmp>s.py b --opt3
Namespace(opt3=True, opt4=False)

Running A arguments in B causes error:

D:\tmp>s.py b --opt2
usage: PROG [-h] {a,b} ...
PROG: error: unrecognized arguments: --opt2

Also if you need to identify which subparser was used you may add dest=name to parser.add_subparsers() call (which I think isn't properly stressed in the docs):

subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')

With the result of:

D:\tmp>s.py b --opt3
Namespace(opt3=True, opt4=False, subparser_name='b')

If you needed to really create arguments dynamically (for example load some argument options from expensive resource) you could use parse_known_args():

Sometimes a script may only parse a few of the command-line arguments, passing the remaining arguments on to another script or program. In these cases, the parse_known_args() method can be useful. It works much like parse_args()except that it does not produce an error when extra arguments are present. Instead, it returns a two item tuple containing the populated namespace and the list of remaining argument strings.

After all, parse_args() just checks trailing aruments:

def parse_args(self, args=None, namespace=None):
    args, argv = self.parse_known_args(args, namespace)
    if argv:
        msg = _('unrecognized arguments: %s')
        self.error(msg % ' '.join(argv))
    return args

And then you can re-execute another parser on argv, but I can imagine few issues that can go with this and I wouldn't recommend it until really necessary.