Skip to content

Commit f41632c

Browse files
serhiy-storchakaebonnal
authored andcommitted
pythongh-126374: Add support of options with optional arguments in the getopt module (pythonGH-126375)
1 parent 16d43a0 commit f41632c

File tree

5 files changed

+112
-25
lines changed

5 files changed

+112
-25
lines changed

Doc/library/getopt.rst

+23-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ exception:
3838
be parsed, without the leading reference to the running program. Typically, this
3939
means ``sys.argv[1:]``. *shortopts* is the string of option letters that the
4040
script wants to recognize, with options that require an argument followed by a
41-
colon (``':'``; i.e., the same format that Unix :c:func:`!getopt` uses).
41+
colon (``':'``) and options that accept an optional argument followed by
42+
two colons (``'::'``); i.e., the same format that Unix :c:func:`!getopt` uses.
4243

4344
.. note::
4445

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

73+
.. versionchanged:: 3.14
74+
Optional arguments are supported.
75+
7076

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

@@ -124,6 +130,20 @@ Using long option names is equally easy:
124130
>>> args
125131
['a1', 'a2']
126132

133+
Optional arguments should be specified explicitly:
134+
135+
.. doctest::
136+
137+
>>> s = '-Con -C --color=off --color a1 a2'
138+
>>> args = s.split()
139+
>>> args
140+
['-Con', '-C', '--color=off', '--color', 'a1', 'a2']
141+
>>> optlist, args = getopt.getopt(args, 'C::', ['color=?'])
142+
>>> optlist
143+
[('-C', 'on'), ('-C', ''), ('--color', 'off'), ('--color', '')]
144+
>>> args
145+
['a1', 'a2']
146+
127147
In a script, typical usage is something like this:
128148

129149
.. testcode::

Doc/whatsnew/3.14.rst

+5
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,11 @@ functools
314314
to reserve a place for positional arguments.
315315
(Contributed by Dominykas Grigonis in :gh:`119127`.)
316316

317+
getopt
318+
------
319+
320+
* Add support for options with optional arguments.
321+
(Contributed by Serhiy Storchaka in :gh:`126374`.)
317322

318323
http
319324
----

Lib/getopt.py

+17-7
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
# - allow the caller to specify ordering
2828
# - RETURN_IN_ORDER option
2929
# - GNU extension with '-' as first character of option string
30-
# - optional arguments, specified by double colons
3130
# - an option string with a W followed by semicolon should
3231
# treat "-W foo" as "--foo"
3332

@@ -58,12 +57,14 @@ def getopt(args, shortopts, longopts = []):
5857
running program. Typically, this means "sys.argv[1:]". shortopts
5958
is the string of option letters that the script wants to
6059
recognize, with options that require an argument followed by a
61-
colon (i.e., the same format that Unix getopt() uses). If
60+
colon and options that accept an optional argument followed by
61+
two colons (i.e., the same format that Unix getopt() uses). If
6262
specified, longopts is a list of strings with the names of the
6363
long options which should be supported. The leading '--'
6464
characters should not be included in the option name. Options
6565
which require an argument should be followed by an equal sign
66-
('=').
66+
('='). Options which acept an optional argument should be
67+
followed by an equal sign and question mark ('=?').
6768
6869
The return value consists of two elements: the first is a list of
6970
(option, value) pairs; the second is the list of program arguments
@@ -153,7 +154,7 @@ def do_longs(opts, opt, longopts, args):
153154

154155
has_arg, opt = long_has_args(opt, longopts)
155156
if has_arg:
156-
if optarg is None:
157+
if optarg is None and has_arg != '?':
157158
if not args:
158159
raise GetoptError(_('option --%s requires argument') % opt, opt)
159160
optarg, args = args[0], args[1:]
@@ -174,13 +175,17 @@ def long_has_args(opt, longopts):
174175
return False, opt
175176
elif opt + '=' in possibilities:
176177
return True, opt
178+
elif opt + '=?' in possibilities:
179+
return '?', opt
177180
# No exact match, so better be unique.
178181
if len(possibilities) > 1:
179182
# XXX since possibilities contains all valid continuations, might be
180183
# nice to work them into the error msg
181184
raise GetoptError(_('option --%s not a unique prefix') % opt, opt)
182185
assert len(possibilities) == 1
183186
unique_match = possibilities[0]
187+
if unique_match.endswith('=?'):
188+
return '?', unique_match[:-2]
184189
has_arg = unique_match.endswith('=')
185190
if has_arg:
186191
unique_match = unique_match[:-1]
@@ -189,8 +194,9 @@ def long_has_args(opt, longopts):
189194
def do_shorts(opts, optstring, shortopts, args):
190195
while optstring != '':
191196
opt, optstring = optstring[0], optstring[1:]
192-
if short_has_arg(opt, shortopts):
193-
if optstring == '':
197+
has_arg = short_has_arg(opt, shortopts)
198+
if has_arg:
199+
if optstring == '' and has_arg != '?':
194200
if not args:
195201
raise GetoptError(_('option -%s requires argument') % opt,
196202
opt)
@@ -204,7 +210,11 @@ def do_shorts(opts, optstring, shortopts, args):
204210
def short_has_arg(opt, shortopts):
205211
for i in range(len(shortopts)):
206212
if opt == shortopts[i] != ':':
207-
return shortopts.startswith(':', i+1)
213+
if not shortopts.startswith(':', i+1):
214+
return False
215+
if shortopts.startswith('::', i+1):
216+
return '?'
217+
return True
208218
raise GetoptError(_('option -%s not recognized') % opt, opt)
209219

210220
if __name__ == '__main__':

Lib/test/test_getopt.py

+66-15
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,34 @@ def assertError(self, *args, **kwargs):
1919
self.assertRaises(getopt.GetoptError, *args, **kwargs)
2020

2121
def test_short_has_arg(self):
22-
self.assertTrue(getopt.short_has_arg('a', 'a:'))
23-
self.assertFalse(getopt.short_has_arg('a', 'a'))
22+
self.assertIs(getopt.short_has_arg('a', 'a:'), True)
23+
self.assertIs(getopt.short_has_arg('a', 'a'), False)
24+
self.assertEqual(getopt.short_has_arg('a', 'a::'), '?')
2425
self.assertError(getopt.short_has_arg, 'a', 'b')
2526

2627
def test_long_has_args(self):
2728
has_arg, option = getopt.long_has_args('abc', ['abc='])
28-
self.assertTrue(has_arg)
29+
self.assertIs(has_arg, True)
2930
self.assertEqual(option, 'abc')
3031

3132
has_arg, option = getopt.long_has_args('abc', ['abc'])
32-
self.assertFalse(has_arg)
33+
self.assertIs(has_arg, False)
3334
self.assertEqual(option, 'abc')
3435

36+
has_arg, option = getopt.long_has_args('abc', ['abc=?'])
37+
self.assertEqual(has_arg, '?')
38+
self.assertEqual(option, 'abc')
39+
40+
has_arg, option = getopt.long_has_args('abc', ['abcd='])
41+
self.assertIs(has_arg, True)
42+
self.assertEqual(option, 'abcd')
43+
3544
has_arg, option = getopt.long_has_args('abc', ['abcd'])
36-
self.assertFalse(has_arg)
45+
self.assertIs(has_arg, False)
46+
self.assertEqual(option, 'abcd')
47+
48+
has_arg, option = getopt.long_has_args('abc', ['abcd=?'])
49+
self.assertEqual(has_arg, '?')
3750
self.assertEqual(option, 'abcd')
3851

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

52-
#opts, args = getopt.do_shorts([], 'a=1', 'a:', [])
53-
#self.assertEqual(opts, [('-a', '1')])
54-
#self.assertEqual(args, [])
65+
opts, args = getopt.do_shorts([], 'a=1', 'a:', [])
66+
self.assertEqual(opts, [('-a', '=1')])
67+
self.assertEqual(args, [])
5568

5669
opts, args = getopt.do_shorts([], 'a', 'a:', ['1'])
5770
self.assertEqual(opts, [('-a', '1')])
@@ -61,6 +74,14 @@ def test_do_shorts(self):
6174
self.assertEqual(opts, [('-a', '1')])
6275
self.assertEqual(args, ['2'])
6376

77+
opts, args = getopt.do_shorts([], 'a', 'a::', ['1'])
78+
self.assertEqual(opts, [('-a', '')])
79+
self.assertEqual(args, ['1'])
80+
81+
opts, args = getopt.do_shorts([], 'a1', 'a::', [])
82+
self.assertEqual(opts, [('-a', '1')])
83+
self.assertEqual(args, [])
84+
6485
self.assertError(getopt.do_shorts, [], 'a1', 'a', [])
6586
self.assertError(getopt.do_shorts, [], 'a', 'a:', [])
6687

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

101+
opts, args = getopt.do_longs([], 'abc', ['abc=?'], ['1'])
102+
self.assertEqual(opts, [('--abc', '')])
103+
self.assertEqual(args, ['1'])
104+
105+
opts, args = getopt.do_longs([], 'abc', ['abcd=?'], ['1'])
106+
self.assertEqual(opts, [('--abcd', '')])
107+
self.assertEqual(args, ['1'])
108+
109+
opts, args = getopt.do_longs([], 'abc=1', ['abc=?'], [])
110+
self.assertEqual(opts, [('--abc', '1')])
111+
self.assertEqual(args, [])
112+
113+
opts, args = getopt.do_longs([], 'abc=1', ['abcd=?'], [])
114+
self.assertEqual(opts, [('--abcd', '1')])
115+
self.assertEqual(args, [])
116+
80117
opts, args = getopt.do_longs([], 'abc', ['ab', 'abc', 'abcd'], [])
81118
self.assertEqual(opts, [('--abc', '')])
82119
self.assertEqual(args, [])
@@ -95,7 +132,7 @@ def test_getopt(self):
95132
# note: the empty string between '-a' and '--beta' is significant:
96133
# it simulates an empty string option argument ('-a ""') on the
97134
# command line.
98-
cmdline = ['-a', '1', '-b', '--alpha=2', '--beta', '-a', '3', '-a',
135+
cmdline = ['-a1', '-b', '--alpha=2', '--beta', '-a', '3', '-a',
99136
'', '--beta', 'arg1', 'arg2']
100137

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

146+
cmdline = ['-a1', '--alpha=2', '--alpha=', '-a', '--alpha', 'arg1', 'arg2']
147+
opts, args = getopt.getopt(cmdline, 'a::', ['alpha=?'])
148+
self.assertEqual(opts, [('-a', '1'), ('--alpha', '2'), ('--alpha', ''),
149+
('-a', ''), ('--alpha', '')])
150+
self.assertEqual(args, ['arg1', 'arg2'])
151+
109152
self.assertError(getopt.getopt, cmdline, 'a:b', ['alpha', 'beta'])
110153

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

115159
# GNU style
116160
opts, args = getopt.gnu_getopt(cmdline, 'ab:', ['alpha', 'beta='])
117-
self.assertEqual(args, ['arg1'])
118-
self.assertEqual(opts, [('-a', ''), ('-b', '1'),
119-
('--alpha', ''), ('--beta', '2')])
161+
self.assertEqual(args, ['arg1', 'arg2'])
162+
self.assertEqual(opts, [('-a', ''), ('-b', '1'), ('--alpha', ''),
163+
('--beta', '2'), ('--beta', '3')])
164+
165+
opts, args = getopt.gnu_getopt(cmdline, 'ab::', ['alpha', 'beta=?'])
166+
self.assertEqual(args, ['arg1', '1', '3', 'arg2'])
167+
self.assertEqual(opts, [('-a', ''), ('-b', ''), ('--alpha', ''),
168+
('--beta', '2'), ('--beta', '')])
120169

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

131181
# Posix style via POSIXLY_CORRECT
132182
self.env["POSIXLY_CORRECT"] = "1"
133183
opts, args = getopt.gnu_getopt(cmdline, 'ab:', ['alpha', 'beta='])
134184
self.assertEqual(opts, [('-a', '')])
135-
self.assertEqual(args, ['arg1', '-b', '1', '--alpha', '--beta=2'])
185+
self.assertEqual(args, ['arg1', '-b', '1', '--alpha', '--beta=2',
186+
'--beta', '3', 'arg2'])
136187

137188
def test_issue4629(self):
138189
longopts, shortopts = getopt.getopt(['--help='], '', ['help='])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for options with optional arguments in the :mod:`getopt` module.

0 commit comments

Comments
 (0)