Skip to content

gh-126374: Add support of options with optional arguments in the getopt module #126375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 21 additions & 3 deletions Doc/library/getopt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ exception:
be parsed, without the leading reference to the running program. Typically, this
means ``sys.argv[1:]``. *shortopts* is the string of option letters that the
script wants to recognize, with options that require an argument followed by a
colon (``':'``; i.e., the same format that Unix :c:func:`!getopt` uses).
colon (``':'``) and options that takes an optional argument followed by
two colons (``'::'``); i.e., the same format that Unix :c:func:`!getopt` uses.

.. note::

Expand All @@ -49,8 +50,10 @@ exception:
*longopts*, if specified, must be a list of strings with the names of the
long options which should be supported. The leading ``'--'`` characters
should not be included in the option name. Long options which require an
argument should be followed by an equal sign (``'='``). Optional arguments
are not supported. To accept only long options, *shortopts* should be an
argument should be followed by an equal sign (``'='``).
Long options which take an optional argument should be followed by
an equal sign and question mark (``'=?'``).
To accept only long options, *shortopts* should be an
empty string. Long options on the command line can be recognized so long as
they provide a prefix of the option name that matches exactly one of the
accepted options. For example, if *longopts* is ``['foo', 'frob']``, the
Expand All @@ -67,6 +70,9 @@ exception:
options occur in the list in the same order in which they were found, thus
allowing multiple occurrences. Long and short options may be mixed.

.. versionchanged:: 3.14
Optional arguments are supported.


.. function:: gnu_getopt(args, shortopts, longopts=[])

Expand Down Expand Up @@ -120,6 +126,18 @@ Using long option names is equally easy:
>>> args
['a1', 'a2']

Optional arguments should be specified explicitly:

>>> s = '-Con -C --color=off --color a1 a2'
>>> args = s.split()
>>> args
['-Con', '-C', '--color=off', '--color', 'a1', 'a2']
>>> optlist, args = getopt.getopt(args, 'C::', ['color=?'])
>>> optlist
[('-C', 'on'), ('-C', ''), ('--color', 'off'), ('--color', '')]
>>> args
['a1', 'a2']

In a script, typical usage is something like this::

import getopt, sys
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ functools
to reserve a place for positional arguments.
(Contributed by Dominykas Grigonis in :gh:`119127`.)

getopt
------

* Add support of options with optional arguments.
(Contributed by Serhiy Storchaka in :gh:`126374`.)

http
----
Expand Down
18 changes: 13 additions & 5 deletions Lib/getopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
# - allow the caller to specify ordering
# - RETURN_IN_ORDER option
# - GNU extension with '-' as first character of option string
# - optional arguments, specified by double colons
# - an option string with a W followed by semicolon should
# treat "-W foo" as "--foo"

Expand Down Expand Up @@ -153,7 +152,7 @@ def do_longs(opts, opt, longopts, args):

has_arg, opt = long_has_args(opt, longopts)
if has_arg:
if optarg is None:
if optarg is None and has_arg != '?':
if not args:
raise GetoptError(_('option --%s requires argument') % opt, opt)
optarg, args = args[0], args[1:]
Expand All @@ -174,13 +173,17 @@ def long_has_args(opt, longopts):
return False, opt
elif opt + '=' in possibilities:
return True, opt
elif opt + '=?' in possibilities:
return '?', opt
# No exact match, so better be unique.
if len(possibilities) > 1:
# XXX since possibilities contains all valid continuations, might be
# nice to work them into the error msg
raise GetoptError(_('option --%s not a unique prefix') % opt, opt)
assert len(possibilities) == 1
unique_match = possibilities[0]
if unique_match.endswith('=?'):
return '?', unique_match[:-2]
has_arg = unique_match.endswith('=')
if has_arg:
unique_match = unique_match[:-1]
Expand All @@ -189,8 +192,9 @@ def long_has_args(opt, longopts):
def do_shorts(opts, optstring, shortopts, args):
while optstring != '':
opt, optstring = optstring[0], optstring[1:]
if short_has_arg(opt, shortopts):
if optstring == '':
has_arg = short_has_arg(opt, shortopts)
if has_arg:
if optstring == '' and has_arg != '?':
if not args:
raise GetoptError(_('option -%s requires argument') % opt,
opt)
Expand All @@ -204,7 +208,11 @@ def do_shorts(opts, optstring, shortopts, args):
def short_has_arg(opt, shortopts):
for i in range(len(shortopts)):
if opt == shortopts[i] != ':':
return shortopts.startswith(':', i+1)
if not shortopts.startswith(':', i+1):
return False
if shortopts.startswith('::', i+1):
return '?'
return True
raise GetoptError(_('option -%s not recognized') % opt, opt)

if __name__ == '__main__':
Expand Down
81 changes: 66 additions & 15 deletions Lib/test/test_getopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,34 @@ def assertError(self, *args, **kwargs):
self.assertRaises(getopt.GetoptError, *args, **kwargs)

def test_short_has_arg(self):
self.assertTrue(getopt.short_has_arg('a', 'a:'))
self.assertFalse(getopt.short_has_arg('a', 'a'))
self.assertIs(getopt.short_has_arg('a', 'a:'), True)
self.assertIs(getopt.short_has_arg('a', 'a'), False)
self.assertEqual(getopt.short_has_arg('a', 'a::'), '?')
self.assertError(getopt.short_has_arg, 'a', 'b')

def test_long_has_args(self):
has_arg, option = getopt.long_has_args('abc', ['abc='])
self.assertTrue(has_arg)
self.assertIs(has_arg, True)
self.assertEqual(option, 'abc')

has_arg, option = getopt.long_has_args('abc', ['abc'])
self.assertFalse(has_arg)
self.assertIs(has_arg, False)
self.assertEqual(option, 'abc')

has_arg, option = getopt.long_has_args('abc', ['abc=?'])
self.assertEqual(has_arg, '?')
self.assertEqual(option, 'abc')

has_arg, option = getopt.long_has_args('abc', ['abcd='])
self.assertIs(has_arg, True)
self.assertEqual(option, 'abcd')

has_arg, option = getopt.long_has_args('abc', ['abcd'])
self.assertFalse(has_arg)
self.assertIs(has_arg, False)
self.assertEqual(option, 'abcd')

has_arg, option = getopt.long_has_args('abc', ['abcd=?'])
self.assertEqual(has_arg, '?')
self.assertEqual(option, 'abcd')

self.assertError(getopt.long_has_args, 'abc', ['def'])
Expand All @@ -49,9 +62,9 @@ def test_do_shorts(self):
self.assertEqual(opts, [('-a', '1')])
self.assertEqual(args, [])

#opts, args = getopt.do_shorts([], 'a=1', 'a:', [])
#self.assertEqual(opts, [('-a', '1')])
#self.assertEqual(args, [])
opts, args = getopt.do_shorts([], 'a=1', 'a:', [])
self.assertEqual(opts, [('-a', '=1')])
self.assertEqual(args, [])

opts, args = getopt.do_shorts([], 'a', 'a:', ['1'])
self.assertEqual(opts, [('-a', '1')])
Expand All @@ -61,6 +74,14 @@ def test_do_shorts(self):
self.assertEqual(opts, [('-a', '1')])
self.assertEqual(args, ['2'])

opts, args = getopt.do_shorts([], 'a', 'a::', ['1'])
self.assertEqual(opts, [('-a', '')])
self.assertEqual(args, ['1'])

opts, args = getopt.do_shorts([], 'a1', 'a::', [])
self.assertEqual(opts, [('-a', '1')])
self.assertEqual(args, [])

self.assertError(getopt.do_shorts, [], 'a1', 'a', [])
self.assertError(getopt.do_shorts, [], 'a', 'a:', [])

Expand All @@ -77,6 +98,22 @@ def test_do_longs(self):
self.assertEqual(opts, [('--abcd', '1')])
self.assertEqual(args, [])

opts, args = getopt.do_longs([], 'abc', ['abc=?'], ['1'])
self.assertEqual(opts, [('--abc', '')])
self.assertEqual(args, ['1'])

opts, args = getopt.do_longs([], 'abc', ['abcd=?'], ['1'])
self.assertEqual(opts, [('--abcd', '')])
self.assertEqual(args, ['1'])

opts, args = getopt.do_longs([], 'abc=1', ['abc=?'], [])
self.assertEqual(opts, [('--abc', '1')])
self.assertEqual(args, [])

opts, args = getopt.do_longs([], 'abc=1', ['abcd=?'], [])
self.assertEqual(opts, [('--abcd', '1')])
self.assertEqual(args, [])

opts, args = getopt.do_longs([], 'abc', ['ab', 'abc', 'abcd'], [])
self.assertEqual(opts, [('--abc', '')])
self.assertEqual(args, [])
Expand All @@ -95,7 +132,7 @@ def test_getopt(self):
# note: the empty string between '-a' and '--beta' is significant:
# it simulates an empty string option argument ('-a ""') on the
# command line.
cmdline = ['-a', '1', '-b', '--alpha=2', '--beta', '-a', '3', '-a',
cmdline = ['-a1', '-b', '--alpha=2', '--beta', '-a', '3', '-a',
'', '--beta', 'arg1', 'arg2']

opts, args = getopt.getopt(cmdline, 'a:b', ['alpha=', 'beta'])
Expand All @@ -106,17 +143,29 @@ def test_getopt(self):
# accounted for in the code that calls getopt().
self.assertEqual(args, ['arg1', 'arg2'])

cmdline = ['-a1', '--alpha=2', '--alpha=', '-a', '--alpha', 'arg1', 'arg2']
opts, args = getopt.getopt(cmdline, 'a::', ['alpha=?'])
self.assertEqual(opts, [('-a', '1'), ('--alpha', '2'), ('--alpha', ''),
('-a', ''), ('--alpha', '')])
self.assertEqual(args, ['arg1', 'arg2'])

self.assertError(getopt.getopt, cmdline, 'a:b', ['alpha', 'beta'])

def test_gnu_getopt(self):
# Test handling of GNU style scanning mode.
cmdline = ['-a', 'arg1', '-b', '1', '--alpha', '--beta=2']
cmdline = ['-a', 'arg1', '-b', '1', '--alpha', '--beta=2', '--beta',
'3', 'arg2']

# GNU style
opts, args = getopt.gnu_getopt(cmdline, 'ab:', ['alpha', 'beta='])
self.assertEqual(args, ['arg1'])
self.assertEqual(opts, [('-a', ''), ('-b', '1'),
('--alpha', ''), ('--beta', '2')])
self.assertEqual(args, ['arg1', 'arg2'])
self.assertEqual(opts, [('-a', ''), ('-b', '1'), ('--alpha', ''),
('--beta', '2'), ('--beta', '3')])

opts, args = getopt.gnu_getopt(cmdline, 'ab::', ['alpha', 'beta=?'])
self.assertEqual(args, ['arg1', '1', '3', 'arg2'])
self.assertEqual(opts, [('-a', ''), ('-b', ''), ('--alpha', ''),
('--beta', '2'), ('--beta', '')])

# recognize "-" as an argument
opts, args = getopt.gnu_getopt(['-a', '-', '-b', '-'], 'ab:', [])
Expand All @@ -126,13 +175,15 @@ def test_gnu_getopt(self):
# Posix style via +
opts, args = getopt.gnu_getopt(cmdline, '+ab:', ['alpha', 'beta='])
self.assertEqual(opts, [('-a', '')])
self.assertEqual(args, ['arg1', '-b', '1', '--alpha', '--beta=2'])
self.assertEqual(args, ['arg1', '-b', '1', '--alpha', '--beta=2',
'--beta', '3', 'arg2'])

# Posix style via POSIXLY_CORRECT
self.env["POSIXLY_CORRECT"] = "1"
opts, args = getopt.gnu_getopt(cmdline, 'ab:', ['alpha', 'beta='])
self.assertEqual(opts, [('-a', '')])
self.assertEqual(args, ['arg1', '-b', '1', '--alpha', '--beta=2'])
self.assertEqual(args, ['arg1', '-b', '1', '--alpha', '--beta=2',
'--beta', '3', 'arg2'])

def test_issue4629(self):
longopts, shortopts = getopt.getopt(['--help='], '', ['help='])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support of options with optional arguments in the :mod:`getopt` module.
Loading