5

I would like to implement the following command line arguments parsing scenario:

I have 4 arguments: -g, -wid, -w1, and -w2.

-w1 and -w2 always appear together

-wid and (-w1 -w2) are mutually exclusive, but one or the other is required

-g is optional; if it is not specified only (-w1 -w2) can appear, but not -wid

Is there an elegant way to implement this using argparse, but without sub-commands (I am already in a sub-command)?

I was thinking of custom actions, but then in the body of the action I'd need to know when it is called for the last time (i.e. when the last argument is being parsed), which I do not know how to do since the number and the order of the arguments may vary.

Some more explanation just in case: the tool I am writing creates a gadget using a widget and a gadget parameter -g. The widget is either an existing widget - and then it is referred to by its id -wid, or a new widget that is created using parameters -w1 and -w2. If -g is not specified then the tool will just create and store a new widget using (-w1 -w2) without creating the gadget.

Thank you in advance.

7
  • if -w1 and -w2 always appear together, what reason do you have for having 2 arguments? Commented Apr 8, 2014 at 6:32
  • @mgilson: Actually, I've just being thinking about it: specify just one argument with nargs=2. Then create a mutually exclusive required group out of (-w1 -w2) and -wid. But I still have the last problem to contend with. Commented Apr 8, 2014 at 6:40
  • Have you looked at the docopt library? Commented Apr 8, 2014 at 7:55
  • @JohnSmithOptional, no, I have not. It does not seem to come packaged with Python 2.7 Commented Apr 8, 2014 at 12:40
  • It's not in the standard library but you can install it with pip (pip install docopt). I always use docopt for building command lines applications. Commented Apr 8, 2014 at 12:49

1 Answer 1

2

If you choose your defaults correctly, you can easily test for logical combinations of the argument values in the 'namespace' after 'parse_args'. You can also issue a parser error at that time. There is a bug report asking for mutually inclusive groups. There I suggest a mechanism for adding generalized combination testing. But it still requires your own logical tests.

http://bugs.python.org/issue11588 Add "necessarily inclusive" groups to argparse

The key to the solution I propose in that issue is making a seen_non_default_actions variable available to the programmer. This is a list (set actually) of actions which have been seen (taking into account that optional-positionals are always 'seen'). I would like to see more discussion of how implement a mixture of inclusive and exclusive groupings.

You specify:

I have 4 arguments: -g, -wid, -w1, and -w2. -w1 and -w2 always appear together -wid and (-w1 -w2) are mutually exclusive, but one or the other is required -g is optional; if it is not specified only (-w1 -w2) can appear, but not -wid

Which I would attempt to summarize as:

complex_group('g', required_next_exclusive_group('wid', inclusive_group('w1','w2)))

w1,w2 could be replaced with a '--w1w2',nargs=2. A simple 'mutually_inclusive_group' would work here. But argparse cannot handle nested groups.

wid and w1w2 could be put in a required mutually_exclusive_group.

-g requires a test like if args.g is None and args.wid is None: error()


Here's a script which behaves as you require (I think), using my latest patch in Issue11588. act_w1 etc are the actual action objects, which may appear in the seen_actions list. The test functions are registered with the subparser, and performed near the end of its parse_know_args().

parser = argparse.ArgumentParser(usage='custom usage')
sp = parser.add_subparsers(dest='cmd')
sp.required = True
spp = sp.add_parser('cmd1')
act_g = spp.add_argument('-g')
act_wid = spp.add_argument('--wid')
act_w1 = spp.add_argument('--w1')
act_w2 = spp.add_argument('--w2')

@spp.crosstest # decorator to register this function with spp
def test1(spp, seen_actions, *args):
    # seen_actions - list of actions that were seen by parser
    if 1==len({act_w1, act_w2}.intersection(seen_actions)):
        # error if only one of these was seen
        parser.error('-w1 and -w2 always appear together')
@spp.crosstest
def test2(spp, seen_actions, *args):
    # required mutually exclusive wid and (w1,w2 group)
    if act_wid in seen_actions:
        if act_w1 in seen_actions or act_w2 in seen_actions:
            parser.error('-wid and (-w1 -w2) are mutually exclusive')
    elif act_w1 not in seen_actions:
        parser.error('wid or (w1 and w2) required')
@spp.crosstest
def test3(spp, seen_actions, *args):
    # is this the simplest logical way of expressing this?
    if act_g not in seen_actions and act_wid in seen_actions:
        parser.error('not g, so not wid')
args = parser.parse_args()

In this example I save and test for the presence of the action objects. Testing could also be done using the dest strings. I'm exploring ways of making this testing more intuitive and user friendly. An expanded set of decorators seems most promising.

Sign up to request clarification or add additional context in comments.

2 Comments

The main point of checking arguments in argparse is to have a nice, helpful, and uniform automated error reporting (otherwise just follow EAFP). Doing it by hand sort of defeats the purpose. Also, as I mentioned, I am talking about a sub-command here, so parse_args() is called in a totally different module. Of course this is surmountable, but I find it cludgy.
I've used custom actions to solve my particular problem, but the solution is relatively complex (for example, it involves setting/clearing .required in actions returned by .add_argument()) and I do not like it much. In most cases what you are proposing is the most sensible approach, so I am accepting your answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.