diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 334a488d0b6223..ff31f57d732b77 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -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. @@ -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 `, 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 diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index e1c4cf6561fc77..f43d93fc3df868 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -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""" @@ -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') @@ -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)