I feel like there's a slight limitation with sub_parsers in argparse, if say, you have a suite of tools that might have similar options that might spread across different levels. It might be rare to have this situation, but if you're writing pluggable / modular code, it could happen.
I have the following example. It is far-fetched and not well explained at the moment because it is quite late, but here it goes:
Usage: tool [-y] {a, b}
a [-x] {create, delete}
create [-x]
delete [-y]
b [-y] {push, pull}
push [-x]
pull [-x]
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument('-x', action = 'store_true')
parser.add_argument('-y', action = 'store_true')
subparsers = parser.add_subparsers(dest = 'command')
parser_a = subparsers.add_parser('a')
parser_a.add_argument('-x', action = 'store_true')
subparsers_a = parser_a.add_subparsers(dest = 'sub_command')
parser_a_create = subparsers_a.add_parser('create')
parser_a_create.add_argument('-x', action = 'store_true')
parser_a_delete = subparsers_a.add_parser('delete')
parser_a_delete.add_argument('-y', action = 'store_true')
parser_b = subparsers.add_parser('b')
parser_b.add_argument('-y', action = 'store_true')
subparsers_b = parser_b.add_subparsers(dest = 'sub_command')
parser_b_create = subparsers_b.add_parser('push')
parser_b_create.add_argument('-x', action = 'store_true')
parser_b_delete = subparsers_b.add_parser('pull')
parser_b_delete.add_argument('-y', action = 'store_true')
print parser.parse_args(['-x', 'a', 'create'])
print parser.parse_args(['a', 'create', '-x'])
print parser.parse_args(['b', '-y', 'pull', '-y'])
print parser.parse_args(['-x', 'b', '-y', 'push', '-x'])
Output
Namespace(command='a', sub_command='create', x=True, y=False)
Namespace(command='a', sub_command='create', x=True, y=False)
Namespace(command='b', sub_command='pull', x=False, y=True)
Namespace(command='b', sub_command='push', x=True, y=True)
As you can see, it is hard to distinguish where along the chain each argument was set.
You could solve this by changing the name for each variable. For example, you could set 'dest' to 'x', 'a_x', 'a_create_x', 'b_push_x', etc., but that would be painful and hard to separate out.
An alternative would be to have the ArgumentParser stop once it reaches a subcommand and pass the remaining arguments off to another, independent parser, so it could generates separate objects.
You can try to achieve that by using 'parse_known_args()' and not defining arguments for each subcommand. However, that would not be good because any un-parsed arguments from before would still be there and might confuse the program.
I feel a slightly cheap, but useful workaround is to have argparse interpret the following arguments as strings in a list. This can be done by setting the prefix to a null-terminator '\0' (or some other 'hard-to-use' character) - if the prefix is empty, the code will throw an error, at least in Python 2.7.3.
Example:
parser = ArgumentParser()
parser.add_argument('-x', action = 'store_true')
parser.add_argument('-y', action = 'store_true')
subparsers = parser.add_subparsers(dest = 'command')
parser_a = subparsers.add_parser('a' prefix_chars = '\0')
parser_a.add_argument('args', type = str, nargs = '*')
print parser.parse_args(['-xy', 'a', '-y', '12'])
Output:
Namespace(args=['-y', '12'], command='a', x=True, y=True)
Note that it does not consume the second -y
option.
You can then pass the result 'args' to another ArgumentParser.
Drawbacks:
- Help might not be handled well. Would have to make some more workaround with this
- Encountering errors might be hard to trace and require some additional effort to make sure error messages are properly chained.
- A little bit more overhead associated with the multiple ArgumentParsers.
If anybody has more input on this, please let me know.