I also ran into this problem. I put together this script to reproduce the issue:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('app')
parser.add_argument('--config')
parser.add_argument('app_args', nargs=argparse.REMAINDER)
args = parser.parse_args(['app', '--config', 'bar'])
print vars(args)
# actual: {'app': 'app', 'app_args': ['--config', 'bar'], 'config': None}
# expected: {'app': 'app', 'app_args': [], 'config': 'bar'}
I'll try using parse_known_args instead.
|
An alternative to Jason's example:
parser = argparse.ArgumentParser()
parser.add_argument('app')
parser.add_argument('--config')
parser.add_argument('app_args', nargs=argparse.REMAINDER)
args = parser.parse_args(['--config', 'bar', 'app'])
print vars(args)
# as expected: {'app': 'app', 'app_args': [], 'config': 'bar'}
When you have several positionals, one or more of which may have 0 arguments (*,?,...), it is best to put all of the optional arguments first.
With 'app --config bar', parse_args identifies a 'AOA' pattern (argument, optional, argument). It then checks which positional arguments match. 'app' claims 1, 'app_args' claims 2 (REMAINDER means match everything that follows). That leaves nothing for '--config'.
What you expected was that 'app' would match with the 1st string, '--config' would match the next 2, leaving nothing for 'app_args'.
In http://bugs.python.org/issue14191 I wrote a patch that would give the results you want if 'app_args' uses '*'. That is makes it possible to interleave positional and optional argument strings. But it does not change the behavior of REMAINDER.
parser.add_argument('app_args', nargs='*')
--------------
Maybe the documentation example for REMAINDER needs to modified to show just how 'greedy' REMAINDER is. Adding a:
parser.add_argument('--arg1',action='store_true')
does not change the outcome. REMAINDER still grabs '--arg1' even though it is a defined argument.
Namespace(arg1=False, args=['--arg1', 'XX', 'ZZ'], command='cmd', foo='B')
|
Here's a possible solution to the problem (assuming there really is one):
- redefine REMAINDER so it matches a '((?:A[AO]*)?)' pattern (where O is a string that looks like an optional flag, A an argument string). I've added the condition that the first match (if any) must be an A. It ends up being closer to the pattern for PARSER.
I included a patch from issue 15112, which delays the consumption of a positional that matches with 0 strings.
In the sample case for this issue, results with this patch are:
args = parser.parse_args(['app', '--config', 'bar'])
# Namespace(app='app', app_args=[], config='bar')
args = parser.parse_args(['--config', 'bar', 'app'])
# Namespace(app='app', app_args=[], config='bar')
args = parser.parse_args(['app', 'args', '--config', 'bar'])
# Namespace(app='app', app_args=['args', '--config', 'bar'], config=None)
In the last case, 'app_args' gets the rest of the strings because the first is a plain 'args'. I believe this is consistent with the intuition expressed in this issue.
I've added one test case to test_argparse.TestNargsRemainder. This is a TestCase that is similar to the above example.
argument_signatures = [Sig('x'), Sig('y', nargs='...'), Sig('-z')]
failures = ['', '-z', '-z Z']
successes = [
('X', NS(x='X', y=[], z=None)),
('-z Z X', NS(x='X', y=[], z='Z')),
('X A B -z Z', NS(x='X', y=['A', 'B', '-z', 'Z'], z=None)),
('X Y --foo', NS(x='X', y=['Y', '--foo'], z=None)),
('X -z Z A B', NS(x='X', y=['A', 'B'], z='Z')), # new case
]
This patch runs test_argparse fine. But there is a slight possibility that this patch will cause backward compatibility problems. Some user might expect y=['-z','Z',...]. But that expectation has not been enshrined the test_argparse.
It may require a slight change to the documentation as well.
|