16

When using argparse, some subcommands need the same options and I'm using parents to avoid repeatedly defining them in every sub-command.

script filename: testarg.py

import argparse                                                                  

parser = argparse.ArgumentParser(add_help=False)                                 
parser.add_argument('-H', '--host', default='192.168.122.1')                     
parser.add_argument('-P', '--port', default='12345')                             
subparsers = parser.add_subparsers()                                             

# subcommand a                                                                   
parser_a = subparsers.add_parser('a', parents=[parser])                          
parser_a.add_argument('-D', '--daemon', action='store_true')                     

parser_a.add_argument('-L', '--log', default='/tmp/test.log')                    

# subcommand b                                                                   
parser_b = subparsers.add_parser('b', parents=[parser])                          
parser_b.add_argument('-D', '--daemon', action='store_true')                     

# subcommand c                                                                   
parser_c = subparsers.add_parser('c', parents=[parser])                          
args = parser.parse_args()                                                       

print args   

But when I run command:

>>>./testarg.py a
usage: testarg.py a [-h] [-H HOST] [-P PORT] [-D] [-L LOG] {a,b,c} ...
testarg.py a: error: too few arguments

expecting output:

>>>./testarg.py a
Namespace(daemon=False, host='192.168.122.1', log='/tmp/test.log', port='12345')

>>>./testarg.py b -H 127.0.0.1 -P 11111
Namespace(daemon=False, host='127.0.0.1', port='11111')

>>>./testarg.py c
Namespace(host='192.168.122.1', port='12345')

also, 

>>>./testarg.py c -H 127.0.0.1 -P 12222
Namespace(host='127.0.0.1', port='12222')

What am I missing?

2
  • Could you please explain what you are trying to achieve? Commented Nov 11, 2015 at 7:39
  • @Alik, answer updated. I want to run some sub-commands with the same option (-H, -P), but I don't want to add them in every sub-commands. Commented Nov 11, 2015 at 7:52

2 Answers 2

42

Make a separate parent parser and pass it to subparsers

import argparse                                                                  

parent_parser = argparse.ArgumentParser(add_help=False)                                 
parent_parser.add_argument('-H', '--host', default='192.168.122.1')                     
parent_parser.add_argument('-P', '--port', default='12345')                             

parser = argparse.ArgumentParser(add_help=False) 
subparsers = parser.add_subparsers()                                             

# subcommand a                                                                   
parser_a = subparsers.add_parser('a', parents = [parent_parser])                          
parser_a.add_argument('-D', '--daemon', action='store_true')                     

parser_a.add_argument('-L', '--log', default='/tmp/test.log')                    

# subcommand b                                                                   
parser_b = subparsers.add_parser('b', parents = [parent_parser])                          
parser_b.add_argument('-D', '--daemon', action='store_true')                     

# subcommand c                                                                   
parser_c = subparsers.add_parser('c', parents = [parent_parser])                          
args = parser.parse_args()                                                       

print args   

This gives desired result

$ python arg.py a
Namespace(daemon=False, host='192.168.122.1', log='/tmp/test.log', port='12345')
$ python arg.py b -H 127.0.0.1 -P 11111
Namespace(daemon=False, host='127.0.0.1', port='11111')
$ python arg.py c
Namespace(host='192.168.122.1', port='12345')
Sign up to request clarification or add additional context in comments.

5 Comments

thanks, it works. I read official argparse doc but did not figure out the method, how do you know this? any doc?
The docs have a parents section, and a subparsers section, which you've read. You are putting those together in an unexpected way. It's hard to anticipate all applications.
The add_help here parser = argparse.ArgumentParser(add_help=False) is not needed.
I needed add_help=False for the parent_parser, but not for parser. Without this on the parent parser it throws an error. Setting add_help=False on parent_parser does not prevent the parent parser options from being listed in --help for parser.
This is a nice solution! BEWARE however: a bug currently exists in CPython that may silently skip options provided before subcommands: github.com/python/cpython/pull/30146
10

When you use parser itself as a parents of the subparsers, you recursively add subparsers to each subparser. The add_subparsers command actually defines a positional argument, one that gets choices, {'a','b','c'}. It ends up expecting prog.py a a a ..., each subparser expects another subparser command etc.

I've never seen anyone try this kind of definition, and it took a bit of thinking to realize what was happening.

@Konstantin's approach is a correct one. Define the parent parser separately, and don't use it directly. It is just a source for those -H and -P Actions that you want added to each subparser. That's all you want to add to the subparsers.

Another approach is to simply define -H and -P in the main parser.

parser = argparse.ArgumentParser()
parser.add_argument('-H', '--host', default='192.168.122.1')
parser.add_argument('-P', '--port', default='12345')
subparsers = parser.add_subparsers()

# subcommand a
parser_a = subparsers.add_parser('a')
parser_a.add_argument('-D', '--daemon', action='store_true')
....

It will function in the same way, except that -H and -P will have to be specified before the subparser command.

0015:~/mypy$ python stack33645859.py -H 127.0.0.1 -P 1111 b
Namespace(daemon=False, host='127.0.0.1', port='1111')

They still appear in the namespace in the same way, it's just that order in the commandline is different. help will also be different.

A third option is to add the common arguments programmatically, with a loop or function. A crude example is:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
splist = []
for cmd in ['a','b','c']:
    p = subparsers.add_parser(cmd)
    p.add_argument('-H', '--host', default='192.168.122.1')
    p.add_argument('-P', '--port', default='12345')
    splist.append(p)
splist[0].add_argument('-D', '--daemon', action='store_true')

Functionally it will be similar to @Alik's approach, with a subtle difference. With the parent, only one pair of H and P Action objects is created. References are added to each subparser.

With mine, each subparser gets its own H and P Action object. Each subparser could define different defaults for those arguments. I remember this being an issue in one other SO question.

Coding work is similar in all cases.

2 Comments

Thanks for your great explanation. many thanks. Actually, I tried both methods you suggested. I ask the questions because I think the -P and -H are a part of every sub-commands but not the root parser, the -H and -P have to specified before sub-command is not what i want. do you have any suggestion about how to do sub-commands with common options pythonic?
My last suggestion looks quite pythonic to me.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.