17

I'd like to have an argument to my program that has some required parameters along with some optional parameters. Something like this:

[--print text [color [size]]

so you could pass it any of these:

mycommand --print hello
mycommand --print hello blue
mycommand --print hello red 12

There could be multiple of these so it has to be a single add_argument. For example:

[--print text [color]] [--output filename [overwrite]]

I can achieve arguments that are close to what I want:

>>> parser = argparse.ArgumentParser()
>>> act = parser.add_argument('--foo', nargs=3, metavar=('x','y','z'))
>>> act = parser.add_argument('--bar', nargs='?')
>>> act = parser.add_argument('--baz', nargs='*')
>>> parser.print_help()
usage: [-h] [--foo x y z] [--bar [BAR]] [--baz [BAZ [BAZ ...]]]

optional arguments:
  -h, --help            show this help message and exit
  --foo x y z
  --bar [BAR]
  --baz [BAZ [BAZ ...]]

but not quite. Is there any way to do this with argparse? I know I could make them all nargs="*" but then --help would not list the names of the optional arguments. If I pass nargs="*" and a tuple for metavar, argparse throws an exception.

jterrace
  • 64,866
  • 22
  • 157
  • 202

4 Answers4

14

How about

def printText(args):
  print args

parser = argparse.ArgumentParser()
subparser = parser.add_subparsers()
printer = subparser.add_parser('print')
printer.add_argument('text')
printer.add_argument('color', nargs='?')
printer.add_argument('size', type=int, nargs='?')
printer.set_defaults(func=printText)

cmd = parser.parse_args()
cmd.func(cmd)

Then you get something like this:

$ ./test.py -h
usage: test.py [-h] {print} ...

positional arguments:
  {print}

$ ./test.py print -h
usage: test.py print [-h] text [color] [size]

positional arguments:
  text
  color
  size

$ ./test.py print text
Namespace(color=None, func=<function printText at 0x2a96150b90>, size=None, text='text')

$ ./test.py print text red
Namespace(color='red', func=<function printText at 0x2a96150b90>, size=None, text='text')

$ ./test.py print text red 12
Namespace(color='red', func=<function printText at 0x2a96150b90>, size=12, text='text')
CNeo
  • 736
  • 1
  • 6
  • 10
  • But you can only specify one subparser at once, do this won't work for multiple. – jterrace Dec 14 '11 at 15:07
  • Can you give an example for multiple? My example comes down to optional positional arguments, both being nargs='?', so depending on what you need to do, it could be accomplished without subparsers. or multiple subparsers.. :) – CNeo Dec 16 '11 at 15:36
  • I have the example in my OP: mycommand --print hello red 12 --output filename overwrite – jterrace Dec 16 '11 at 16:55
  • Apologies, I should probably have read the question more clearly.. You can simulate what you're after with the `nargs='+'` or `nargs='*'`, which is already as close as you came. metavar can indeed take a tuple, but can only define two names (not three). Also you could overwrite help, for something like `parser.add_argument('--print', nargs='+', metavar=('text', 'color'), help='--print text [color [size]]')` – CNeo Dec 19 '11 at 13:51
8

Reading the source code (start in take_action), I believe what you want is impossible. All argument parsing and passing to actions is done based on nargs, and nargs is either a number, OPTIONAL ("?"), ZERO_OR_MORE ("*"), ONE_OR_MORE ("+"), PARSER, or REMAINDER. This must be determined before the Action object (which handles the input) even sees what it's getting, so it can't dynamically figure out nargs.

I think you'll need to live with a workaround. I would maybe have --foo-x x, --foo-y y, and --foo-z z, and perhaps also --foo x y z.

Devin Jeanpierre
  • 92,913
  • 4
  • 55
  • 79
1

that will work for single arg:

parser.add_argument('--write_google', nargs='?', const='Yes',
                    choices=['force', 'Yes'],
                    help="write local changes to google")
1

According to Devin Jeanpierre's answer, it seems that using '+' (one or more) instead of '*' would do what you are trying to achieve. (PS: I would've just commented in his answer if I had enough points)

Siddardha
  • 510
  • 1
  • 7
  • 17
  • Don't forget-- he also doesn't want to have more than three arguments. If you incorporate this into your post, then you can post an independent answer (if you, of course, credit Jean Pierre for his idea.) – Alex Aug 22 '17 at 16:15