Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,53 @@ See also :ref:`specifying-ambiguous-arguments`. The supported values are:
usage: PROG [-h] foo [foo ...]
PROG: error: the following arguments are required: foo

.. _argparse.REMAINDER:

* ``argparse.REMAINDER``. Remaining command-line arguments are gathered into a
list, including strings that look like options. This is useful for forwarding
arguments to another command. A common pattern is a final positional argument
after a command name; options for the outer parser must precede that command::

>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo')
>>> parser.add_argument('command')
>>> parser.add_argument('args', nargs=argparse.REMAINDER)
>>> parser.parse_args('--foo B cmd --foo C'.split())
Namespace(foo='B', command='cmd', args=['--foo', 'C'])
>>> parser.parse_args(['cmd'])
Namespace(foo=None, command='cmd', args=[])

When the remainder is the first positional argument, leading options are still
parsed normally. It starts with the first positional value, or with ``--``.
In a subparser, the subcommand name has already been consumed by the parent
parser and does not start the remainder. Any ``--`` consumed by the remainder
is retained::

>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('args', nargs=argparse.REMAINDER)
>>> parser.parse_args(['--', '--foo', 'B'])
Namespace(args=['--', '--foo', 'B'])

For an optional argument, supply the option separately from its values::

>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--forward', nargs=argparse.REMAINDER)
>>> parser.parse_args('--forward --foo B --'.split())
Namespace(forward=['--foo', 'B', '--'])
>>> parser.parse_args(['--forward'])
Namespace(forward=[])

An attached value such as ``--forward=value`` supplies only that value;
subsequent arguments are parsed normally.

.. versionchanged:: 3.15
Optional arguments with ``nargs=argparse.REMAINDER`` consume ``--`` and
subsequent arguments instead of stopping at ``--``.

With the default ``'store'`` action, an empty positional remainder ignores
default_ and produces ``[]``. The choices_ restriction is not applied to the
remainder.

If the ``nargs`` keyword argument is not provided, the number of arguments consumed
is determined by the action_. Generally this means a single command-line argument
will be consumed and a single item (not a list) will be produced.
Expand Down Expand Up @@ -2334,8 +2381,9 @@ Intermixed parsing

These parsers do not support all the :mod:`!argparse` features, and will raise
exceptions if unsupported features are used. In particular, subparsers,
and mutually exclusive groups that include both
optionals and positionals are not supported.
positional arguments with
:ref:`nargs=argparse.REMAINDER <argparse.REMAINDER>`, and mutually exclusive
groups that include both optionals and positionals are not supported.

The following example shows the difference between
:meth:`~ArgumentParser.parse_known_args` and
Expand Down
79 changes: 78 additions & 1 deletion Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1838,6 +1838,75 @@ class TestNargsRemainder(ParserTestCase):
]


class TestNargsRemainderBoundaries(TestCase):

def test_first_positional(self):
parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('--foo')
parser.add_argument('args', nargs=argparse.REMAINDER)

for args in (['--unknown'], ['--foo', 'B', '--unknown']):
with self.subTest(args=args):
with self.assertRaisesRegex(
argparse.ArgumentError,
'unrecognized arguments: --unknown'):
parser.parse_args(args)
self.assertEqual(parser.parse_args(['--foo', 'B']),
NS(foo='B', args=[]))
self.assertEqual(parser.parse_args(['cmd', '--foo', 'C']),
NS(foo=None, args=['cmd', '--foo', 'C']))
self.assertEqual(parser.parse_args(['--', '--foo', 'C']),
NS(foo=None, args=['--', '--foo', 'C']))

def test_first_positional_in_subparser(self):
parser = argparse.ArgumentParser(exit_on_error=False)
subparser = parser.add_subparsers().add_parser('run')
subparser.add_argument('--foo', action='store_true')
subparser.add_argument('args', nargs=argparse.REMAINDER)

with self.assertRaisesRegex(argparse.ArgumentError,
'unrecognized arguments: --unknown'):
parser.parse_args(['run', '--unknown'])
self.assertEqual(parser.parse_args(['run', '--foo']),
NS(foo=True, args=[]))
self.assertEqual(parser.parse_args(['run', 'cmd', '--foo']),
NS(foo=False, args=['cmd', '--foo']))
self.assertEqual(parser.parse_args(['run', '--', '--foo']),
NS(foo=False, args=['--', '--foo']))

def test_optional_captures_options(self):
parser = argparse.ArgumentParser()
parser.add_argument('--foo', action='store_true')
parser.add_argument('--forward', nargs=argparse.REMAINDER,
default=['fallback'])

self.assertEqual(parser.parse_args(['--forward']),
NS(foo=False, forward=[]))
self.assertEqual(parser.parse_args(['--forward', '--foo']),
NS(foo=False, forward=['--foo']))

def test_optional_attached_value(self):
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--forward', nargs=argparse.REMAINDER)

for option in ('--forward=a', '-fa'):
with self.subTest(option=option):
self.assertEqual(parser.parse_known_args([option, 'b']),
(NS(forward=['a']), ['b']))

def test_empty_positional_ignores_default(self):
parser = argparse.ArgumentParser()
parser.add_argument('args', nargs=argparse.REMAINDER,
default=['fallback'])
self.assertEqual(parser.parse_args([]), NS(args=[]))

def test_type_conversion_without_choices_validation(self):
parser = argparse.ArgumentParser()
parser.add_argument('args', nargs=argparse.REMAINDER,
type=int, choices=[1])
self.assertEqual(parser.parse_args(['2']), NS(args=[2]))


class TestOptionLike(ParserTestCase):
"""Tests options that may or may not be arguments"""

Expand Down Expand Up @@ -6856,7 +6925,7 @@ def test_basic(self):
self.assertEqual(bar.required, True)

def test_remainder(self):
# Intermixed and remainder are incompatible
# Intermixed and positional remainder are incompatible
parser = ErrorRaisingArgumentParser(prog='PROG')
parser.add_argument('-z')
parser.add_argument('x')
Expand All @@ -6868,6 +6937,14 @@ def test_remainder(self):
parser.parse_intermixed_args(argv)
self.assertRegex(str(cm.exception), r'\.\.\.')

def test_optional_remainder(self):
parser = argparse.ArgumentParser()
parser.add_argument('command')
parser.add_argument('--forward', nargs=argparse.REMAINDER)
self.assertEqual(
parser.parse_intermixed_args(['cmd', '--forward', '--foo']),
NS(command='cmd', forward=['--foo']))

def test_required_exclusive(self):
# required mutually exclusive group; intermixed works fine
parser = argparse.ArgumentParser(prog='PROG', exit_on_error=False)
Expand Down
Loading