Update get{opt,pass}.py from 3.14.3

This commit is contained in:
ShaharNaveh
2026-02-07 22:38:57 +02:00
committed by Jeong, YunWon
parent bbe5412aea
commit c29d2d9a1c
4 changed files with 258 additions and 45 deletions

63
Lib/getopt.py vendored
View File

@@ -2,8 +2,8 @@
This module helps scripts to parse the command line arguments in
sys.argv. It supports the same conventions as the Unix getopt()
function (including the special meanings of arguments of the form `-'
and `--'). Long options similar to those supported by GNU software
function (including the special meanings of arguments of the form '-'
and '--'). Long options similar to those supported by GNU software
may be used as well via an optional third argument. This module
provides two functions and an exception:
@@ -24,21 +24,14 @@ option involved with the exception.
# TODO for gnu_getopt():
#
# - GNU getopt_long_only mechanism
# - 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"
__all__ = ["GetoptError","error","getopt","gnu_getopt"]
import os
try:
from gettext import gettext as _
except ImportError:
# Bootstrapping Python: gettext's dependencies not built yet
def _(s): return s
from gettext import gettext as _
class GetoptError(Exception):
opt = ''
@@ -61,12 +54,14 @@ def getopt(args, shortopts, longopts = []):
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 getopt() uses). If
colon and options that accept an optional argument followed by
two colons (i.e., the same format that Unix getopt() uses). If
specified, longopts is 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. Options
which require an argument should be followed by an equal sign
('=').
('='). Options which accept an optional argument should be
followed by an equal sign and question mark ('=?').
The return value consists of two elements: the first is a list of
(option, value) pairs; the second is the list of program arguments
@@ -105,7 +100,7 @@ def gnu_getopt(args, shortopts, longopts = []):
processing options as soon as a non-option argument is
encountered.
If the first character of the option string is `+', or if the
If the first character of the option string is '+', or if the
environment variable POSIXLY_CORRECT is set, then option
processing stops as soon as a non-option argument is encountered.
@@ -118,8 +113,13 @@ def gnu_getopt(args, shortopts, longopts = []):
else:
longopts = list(longopts)
return_in_order = False
if shortopts.startswith('-'):
shortopts = shortopts[1:]
all_options_first = False
return_in_order = True
# Allow options after non-option arguments?
if shortopts.startswith('+'):
elif shortopts.startswith('+'):
shortopts = shortopts[1:]
all_options_first = True
elif os.environ.get("POSIXLY_CORRECT"):
@@ -133,8 +133,14 @@ def gnu_getopt(args, shortopts, longopts = []):
break
if args[0][:2] == '--':
if return_in_order and prog_args:
opts.append((None, prog_args))
prog_args = []
opts, args = do_longs(opts, args[0][2:], longopts, args[1:])
elif args[0][:1] == '-' and args[0] != '-':
if return_in_order and prog_args:
opts.append((None, prog_args))
prog_args = []
opts, args = do_shorts(opts, args[0][1:], shortopts, args[1:])
else:
if all_options_first:
@@ -156,7 +162,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:]
@@ -177,13 +183,19 @@ def long_has_args(opt, longopts):
return False, opt
elif opt + '=' in possibilities:
return True, opt
# No exact match, so better be unique.
elif opt + '=?' in possibilities:
return '?', opt
# Possibilities must be unique to be accepted
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)
raise GetoptError(
_("option --%s not a unique prefix; possible options: %s")
% (opt, ", ".join(possibilities)),
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]
@@ -192,8 +204,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)
@@ -207,7 +220,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__':

78
Lib/getpass.py vendored
View File

@@ -1,6 +1,7 @@
"""Utilities to get a password and/or the current user name.
getpass(prompt[, stream]) - Prompt for a password, with echo turned off.
getpass(prompt[, stream[, echo_char]]) - Prompt for a password, with echo
turned off and optional keyboard feedback.
getuser() - Get the user name from the environment or password database.
GetPassWarning - This UserWarning is issued when getpass() cannot prevent
@@ -25,13 +26,15 @@ __all__ = ["getpass","getuser","GetPassWarning"]
class GetPassWarning(UserWarning): pass
def unix_getpass(prompt='Password: ', stream=None):
def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
"""Prompt for a password, with echo turned off.
Args:
prompt: Written on stream to ask for the input. Default: 'Password: '
stream: A writable file object to display the prompt. Defaults to
the tty. If no tty is available defaults to sys.stderr.
echo_char: A single ASCII character to mask input (e.g., '*').
If None, input is hidden.
Returns:
The seKr3t input.
Raises:
@@ -40,6 +43,8 @@ def unix_getpass(prompt='Password: ', stream=None):
Always restores terminal settings before returning.
"""
_check_echo_char(echo_char)
passwd = None
with contextlib.ExitStack() as stack:
try:
@@ -68,12 +73,16 @@ def unix_getpass(prompt='Password: ', stream=None):
old = termios.tcgetattr(fd) # a copy to save
new = old[:]
new[3] &= ~termios.ECHO # 3 == 'lflags'
if echo_char:
new[3] &= ~termios.ICANON
tcsetattr_flags = termios.TCSAFLUSH
if hasattr(termios, 'TCSASOFT'):
tcsetattr_flags |= termios.TCSASOFT
try:
termios.tcsetattr(fd, tcsetattr_flags, new)
passwd = _raw_input(prompt, stream, input=input)
passwd = _raw_input(prompt, stream, input=input,
echo_char=echo_char)
finally:
termios.tcsetattr(fd, tcsetattr_flags, old)
stream.flush() # issue7208
@@ -93,10 +102,11 @@ def unix_getpass(prompt='Password: ', stream=None):
return passwd
def win_getpass(prompt='Password: ', stream=None):
def win_getpass(prompt='Password: ', stream=None, *, echo_char=None):
"""Prompt for password with echo off, using Windows getwch()."""
if sys.stdin is not sys.__stdin__:
return fallback_getpass(prompt, stream)
_check_echo_char(echo_char)
for c in prompt:
msvcrt.putwch(c)
@@ -108,25 +118,48 @@ def win_getpass(prompt='Password: ', stream=None):
if c == '\003':
raise KeyboardInterrupt
if c == '\b':
if echo_char and pw:
msvcrt.putwch('\b')
msvcrt.putwch(' ')
msvcrt.putwch('\b')
pw = pw[:-1]
else:
pw = pw + c
if echo_char:
msvcrt.putwch(echo_char)
msvcrt.putwch('\r')
msvcrt.putwch('\n')
return pw
def fallback_getpass(prompt='Password: ', stream=None):
def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None):
_check_echo_char(echo_char)
import warnings
warnings.warn("Can not control echo on the terminal.", GetPassWarning,
stacklevel=2)
if not stream:
stream = sys.stderr
print("Warning: Password input may be echoed.", file=stream)
return _raw_input(prompt, stream)
return _raw_input(prompt, stream, echo_char=echo_char)
def _raw_input(prompt="", stream=None, input=None):
def _check_echo_char(echo_char):
# Single-character ASCII excluding control characters
if echo_char is None:
return
if not isinstance(echo_char, str):
raise TypeError("'echo_char' must be a str or None, not "
f"{type(echo_char).__name__}")
if not (
len(echo_char) == 1
and echo_char.isprintable()
and echo_char.isascii()
):
raise ValueError("'echo_char' must be a single printable ASCII "
f"character, got: {echo_char!r}")
def _raw_input(prompt="", stream=None, input=None, echo_char=None):
# This doesn't save the string in the GNU readline history.
if not stream:
stream = sys.stderr
@@ -143,6 +176,8 @@ def _raw_input(prompt="", stream=None, input=None):
stream.write(prompt)
stream.flush()
# NOTE: The Python C API calls flockfile() (and unlock) during readline.
if echo_char:
return _readline_with_echo_char(stream, input, echo_char)
line = input.readline()
if not line:
raise EOFError
@@ -151,6 +186,35 @@ def _raw_input(prompt="", stream=None, input=None):
return line
def _readline_with_echo_char(stream, input, echo_char):
passwd = ""
eof_pressed = False
while True:
char = input.read(1)
if char == '\n' or char == '\r':
break
elif char == '\x03':
raise KeyboardInterrupt
elif char == '\x7f' or char == '\b':
if passwd:
stream.write("\b \b")
stream.flush()
passwd = passwd[:-1]
elif char == '\x04':
if eof_pressed:
break
else:
eof_pressed = True
elif char == '\x00':
continue
else:
passwd += char
stream.write(echo_char)
stream.flush()
eof_pressed = False
return passwd
def getuser():
"""Get the username from the environment or password database.

View File

@@ -19,21 +19,34 @@ class GetoptTests(unittest.TestCase):
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'])
@@ -49,9 +62,9 @@ class GetoptTests(unittest.TestCase):
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')])
@@ -61,6 +74,14 @@ class GetoptTests(unittest.TestCase):
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:', [])
@@ -77,6 +98,22 @@ class GetoptTests(unittest.TestCase):
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, [])
@@ -95,7 +132,7 @@ class GetoptTests(unittest.TestCase):
# 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'])
@@ -106,33 +143,53 @@ class GetoptTests(unittest.TestCase):
# 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:', [])
self.assertEqual(args, ['-'])
self.assertEqual(opts, [('-a', ''), ('-b', '-')])
# Return positional arguments intermixed with options.
opts, args = getopt.gnu_getopt(cmdline, '-ab:', ['alpha', 'beta='])
self.assertEqual(args, ['arg2'])
self.assertEqual(opts, [('-a', ''), (None, ['arg1']), ('-b', '1'), ('--alpha', ''),
('--beta', '2'), ('--beta', '3')])
# 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='])

View File

@@ -161,6 +161,81 @@ class UnixGetpassTest(unittest.TestCase):
self.assertIn('Warning', stderr.getvalue())
self.assertIn('Password:', stderr.getvalue())
def test_echo_char_replaces_input_with_asterisks(self):
mock_result = '*************'
with mock.patch('os.open') as os_open, \
mock.patch('io.FileIO'), \
mock.patch('io.TextIOWrapper') as textio, \
mock.patch('termios.tcgetattr'), \
mock.patch('termios.tcsetattr'), \
mock.patch('getpass._raw_input') as mock_input:
os_open.return_value = 3
mock_input.return_value = mock_result
result = getpass.unix_getpass(echo_char='*')
mock_input.assert_called_once_with('Password: ', textio(),
input=textio(), echo_char='*')
self.assertEqual(result, mock_result)
def test_raw_input_with_echo_char(self):
passwd = 'my1pa$$word!'
mock_input = StringIO(f'{passwd}\n')
mock_output = StringIO()
with mock.patch('sys.stdin', mock_input), \
mock.patch('sys.stdout', mock_output):
result = getpass._raw_input('Password: ', mock_output, mock_input,
'*')
self.assertEqual(result, passwd)
self.assertEqual('Password: ************', mock_output.getvalue())
def test_control_chars_with_echo_char(self):
passwd = 'pass\twd\b'
expect_result = 'pass\tw'
mock_input = StringIO(f'{passwd}\n')
mock_output = StringIO()
with mock.patch('sys.stdin', mock_input), \
mock.patch('sys.stdout', mock_output):
result = getpass._raw_input('Password: ', mock_output, mock_input,
'*')
self.assertEqual(result, expect_result)
self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue())
class GetpassEchoCharTest(unittest.TestCase):
def test_accept_none(self):
getpass._check_echo_char(None)
@support.subTests('echo_char', ["*", "A", " "])
def test_accept_single_printable_ascii(self, echo_char):
getpass._check_echo_char(echo_char)
def test_reject_empty_string(self):
self.assertRaises(ValueError, getpass.getpass, echo_char="")
@support.subTests('echo_char', ["***", "AA", "aA*!"])
def test_reject_multi_character_strings(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
@support.subTests('echo_char', [
'\N{LATIN CAPITAL LETTER AE}', # non-ASCII single character
'\N{HEAVY BLACK HEART}', # non-ASCII multibyte character
])
def test_reject_non_ascii(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
@support.subTests('echo_char', [
ch for ch in map(chr, range(0, 128))
if not ch.isprintable()
])
def test_reject_non_printable_characters(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
# TypeError Rejection
@support.subTests('echo_char', [b"*", 0, 0.0, [], {}])
def test_reject_non_string(self, echo_char):
self.assertRaises(TypeError, getpass.getpass, echo_char=echo_char)
if __name__ == "__main__":
unittest.main()