Skip to content

Commit 82fcaf8

Browse files
authored
Cleanup Python/getopt.c: add _PyOS_GetOpt structure (#158362)
Replace global variables with a new _PyOS_GetOpt structure: * _PyOS_opterr => getopt->error * _PyOS_optind => getopt->index * _PyOS_optarg => getopt->arg Changes: * Add tests to test_cmd_line. * Rename _PyOS_ResetGetOpt() to _PyOS_GetOpt_Init(). Add argc and argv parameters. * Don't expose longindex in the structure, it's only used internally. * Move _PyOS_LongOption structure from the internal C API to getopt.c.
1 parent eb30e3d commit 82fcaf8

5 files changed

Lines changed: 234 additions & 104 deletions

File tree

‎Include/internal/pycore_getopt.h‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@
55
# error "this header requires Py_BUILD_CORE define"
66
#endif
77

8-
extern int _PyOS_opterr;
9-
extern Py_ssize_t _PyOS_optind;
10-
extern const wchar_t *_PyOS_optarg;
8+
struct _PyOS_GetOpt {
9+
int error; // generate error messages
10+
Py_ssize_t index; // index into argv array
11+
const wchar_t *arg; // optional argument
12+
const wchar_t *ptr;
13+
Py_ssize_t argc;
14+
wchar_t * const *argv;
15+
};
1116

12-
extern void _PyOS_ResetGetOpt(void);
17+
extern void _PyOS_GetOpt_Init(
18+
struct _PyOS_GetOpt *getopt,
19+
Py_ssize_t argc,
20+
wchar_t * const *argv);
1321

14-
typedef struct {
15-
const wchar_t *name;
16-
int has_arg;
17-
int val;
18-
} _PyOS_LongOption;
19-
20-
extern int _PyOS_GetOpt(Py_ssize_t argc, wchar_t * const *argv, int *longindex);
22+
extern int _PyOS_GetOpt(
23+
struct _PyOS_GetOpt *getopt);
2124

2225
#endif /* !Py_INTERNAL_PYGETOPT_H */

‎Lib/test/test_cmd_line.py‎

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from test import support
1515
from test.support import os_helper
1616
from test.support import force_not_colorized
17+
from test.support import import_helper
1718
from test.support import threading_helper
1819
from test.support.script_helper import (
1920
spawn_python, kill_python, assert_python_ok, assert_python_failure,
@@ -60,14 +61,17 @@ def verify_valid_flag(self, cmd_line):
6061
@support.cpython_only
6162
@support.force_not_colorized
6263
def test_help(self):
63-
self.verify_valid_flag('-h')
64-
self.verify_valid_flag('-?')
65-
out = self.verify_valid_flag('--help')
66-
lines = out.splitlines()
67-
self.assertIn(b'usage', lines[0])
68-
self.assertNotIn(b'PYTHONHOME', out)
69-
self.assertNotIn(b'-X dev', out)
70-
self.assertLess(len(lines), 50)
64+
options = ['-h', '-?', '--help']
65+
if support.MS_WINDOWS:
66+
options.append('/?')
67+
for opt in options:
68+
with self.subTest(opt=opt):
69+
out = self.verify_valid_flag(opt)
70+
lines = out.splitlines()
71+
self.assertIn(b'usage', lines[0])
72+
self.assertNotIn(b'PYTHONHOME', out)
73+
self.assertNotIn(b'-X dev', out)
74+
self.assertLess(len(lines), 50)
7175

7276
@support.cpython_only
7377
@support.force_not_colorized
@@ -680,16 +684,22 @@ def test_del___main__(self):
680684

681685
@support.cpython_only
682686
def test_unknown_options(self):
683-
rc, out, err = assert_python_failure('-E', '-z')
684-
self.assertIn(b'Unknown option: -z', err)
685-
self.assertEqual(err.splitlines().count(b'Unknown option: -z'), 1)
686-
self.assertEqual(b'', out)
687+
# Test unknown option -a
688+
for option in ('-z', '--long-option', '---'):
689+
with self.subTest(option=option):
690+
rc, out, err = assert_python_failure('-E', option)
691+
errmsg = f'Unknown option: {option}'.encode()
692+
self.assertIn(errmsg, err)
693+
self.assertEqual(err.splitlines().count(errmsg), 1)
694+
self.assertEqual(b'', out)
695+
687696
# Add "without='-E'" to prevent _assert_python to append -E
688697
# to env_vars and change the output of stderr
689698
rc, out, err = assert_python_failure('-z', without='-E')
690699
self.assertIn(b'Unknown option: -z', err)
691700
self.assertEqual(err.splitlines().count(b'Unknown option: -z'), 1)
692701
self.assertEqual(b'', out)
702+
693703
rc, out, err = assert_python_failure('-a', '-z', without='-E')
694704
self.assertIn(b'Unknown option: -a', err)
695705
# only the first unknown option is reported
@@ -1403,6 +1413,97 @@ def test_dump_path_config(self):
14031413
self.assertIn(b'Python path configuration:', proc.err)
14041414
self.assertIn(f"PYTHONHOME = '{nonexistent}'".encode(), proc.err)
14051415

1416+
def test_short_options(self):
1417+
# Skip the test if _testcapi cannot be imported:
1418+
# the test uses _testcapi.config_get().
1419+
import_helper.import_module('_testcapi')
1420+
1421+
# Test short command line options
1422+
def check(options, config_names, expected):
1423+
if isinstance(config_names, str):
1424+
config_names = (config_names,)
1425+
if isinstance(options, str):
1426+
options = (options,)
1427+
expr = ', '.join(f'config_get({name!a})' for name in config_names)
1428+
code = f'from _testcapi import config_get; print({expr})'
1429+
args = options + ("-c", code)
1430+
proc = assert_python_ok(*args)
1431+
self.assertEqual(proc.out.rstrip(), expected.encode())
1432+
1433+
def check_ignored(option):
1434+
# Just test that passing the option doesn't fail
1435+
assert_python_ok(option, "-c", "pass")
1436+
1437+
check('-b', 'bytes_warning', '1')
1438+
check('-bb', 'bytes_warning', '2')
1439+
check('-B', 'write_bytecode', 'False')
1440+
check('-d', 'parser_debug', 'True')
1441+
check('-E', 'use_environment', 'False')
1442+
check('-i', ('inspect', 'interactive'), 'True True')
1443+
check('-I', 'isolated', 'True')
1444+
check('-O', 'optimization_level', '1')
1445+
check('-OO', 'optimization_level', '2')
1446+
check('-P', 'safe_path', 'True')
1447+
check('-q', 'quiet', 'True')
1448+
check('-R', 'use_hash_seed', 'False')
1449+
check('-s', 'user_site_directory', 'False')
1450+
check('-S', 'site_import', 'False')
1451+
check_ignored('-t')
1452+
check('-u', 'buffered_stdio', 'False')
1453+
check('-v', 'verbose', '1')
1454+
check('-Wignore', 'warnoptions', "['ignore']")
1455+
check('-x', 'skip_source_first_line', 'True')
1456+
check(('-X', 'xoption=value'), 'xoptions',
1457+
# assert_python_ok() adds -X faulthandler
1458+
"{'faulthandler': True, 'xoption': 'value'}")
1459+
1460+
# Short options can be combined
1461+
check('-bIs', ('bytes_warning', 'isolated', 'user_site_directory'),
1462+
'1 True False')
1463+
1464+
# -c, -h, -m, -V and -? are tested elsewhere
1465+
1466+
def test_missing_argument(self):
1467+
def check_missing_arg(option):
1468+
proc = assert_python_failure(option)
1469+
self.assertEqual(proc.rc, 2)
1470+
errmsg = f"Argument expected for the {option} option"
1471+
self.assertStartsWith(proc.err.rstrip(), errmsg.encode())
1472+
1473+
check_missing_arg('-c')
1474+
check_missing_arg('-m')
1475+
check_missing_arg('-W')
1476+
check_missing_arg('-X')
1477+
check_missing_arg('--check-hash-based-pycs')
1478+
1479+
def test_long_options(self):
1480+
# Test long command line options
1481+
1482+
# Test --check-hash-based-pycs option
1483+
code = 'import _imp; print(_imp.check_hash_based_pycs)'
1484+
opt = f"--check-hash-based-pycs"
1485+
for value in ('always', 'never', 'default'):
1486+
with self.subTest(value=value):
1487+
proc = assert_python_ok(opt, value, "-c", code)
1488+
self.assertEqual(proc.out.rstrip(), value.encode())
1489+
1490+
# "Expected long option" error
1491+
proc = assert_python_ok("-b-")
1492+
self.assertEqual(proc.out, b'')
1493+
self.assertEqual(proc.err.rstrip(), b"Expected long option")
1494+
1495+
# Other long options --help-all, --help-env, --help-xoptions
1496+
# and --version are tested elsewhere
1497+
1498+
def test_dash_option(self):
1499+
# Test -- in the command line
1500+
code = (
1501+
'import sys; '
1502+
'print(sys.flags.isolated, sys.flags.optimize, sys.argv)'
1503+
)
1504+
proc = assert_python_ok('-I', '-c', code, '--', '-O')
1505+
self.assertEqual(proc.out.rstrip(), b"1 0 ['-c', '--', '-O']")
1506+
14061507

14071508
@unittest.skipIf(interpreter_requires_environment(),
14081509
'Cannot run -I tests when PYTHON env vars are required.')

0 commit comments

Comments
 (0)