https://github.com/python/cpython/commit/82fcaf881cddc8c735ed036ba5c1db81c5864276
commit: 82fcaf881cddc8c735ed036ba5c1db81c5864276
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-01T14:58:55Z
summary:
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.
files:
M Include/internal/pycore_getopt.h
M Lib/test/test_cmd_line.py
M Python/getopt.c
M Python/initconfig.c
M Python/preconfig.c
diff --git a/Include/internal/pycore_getopt.h b/Include/internal/pycore_getopt.h
index 7f0dd13ae577f7..e767cb1049d079 100644
--- a/Include/internal/pycore_getopt.h
+++ b/Include/internal/pycore_getopt.h
@@ -5,18 +5,21 @@
# error "this header requires Py_BUILD_CORE define"
#endif
-extern int _PyOS_opterr;
-extern Py_ssize_t _PyOS_optind;
-extern const wchar_t *_PyOS_optarg;
+struct _PyOS_GetOpt {
+ int error; // generate error messages
+ Py_ssize_t index; // index into argv array
+ const wchar_t *arg; // optional argument
+ const wchar_t *ptr;
+ Py_ssize_t argc;
+ wchar_t * const *argv;
+};
-extern void _PyOS_ResetGetOpt(void);
+extern void _PyOS_GetOpt_Init(
+ struct _PyOS_GetOpt *getopt,
+ Py_ssize_t argc,
+ wchar_t * const *argv);
-typedef struct {
- const wchar_t *name;
- int has_arg;
- int val;
-} _PyOS_LongOption;
-
-extern int _PyOS_GetOpt(Py_ssize_t argc, wchar_t * const *argv, int
*longindex);
+extern int _PyOS_GetOpt(
+ struct _PyOS_GetOpt *getopt);
#endif /* !Py_INTERNAL_PYGETOPT_H */
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
index 476a481a0ba54b..6d07238a9d08ab 100644
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -14,6 +14,7 @@
from test import support
from test.support import os_helper
from test.support import force_not_colorized
+from test.support import import_helper
from test.support import threading_helper
from test.support.script_helper import (
spawn_python, kill_python, assert_python_ok, assert_python_failure,
@@ -60,14 +61,17 @@ def verify_valid_flag(self, cmd_line):
@support.cpython_only
@support.force_not_colorized
def test_help(self):
- self.verify_valid_flag('-h')
- self.verify_valid_flag('-?')
- out = self.verify_valid_flag('--help')
- lines = out.splitlines()
- self.assertIn(b'usage', lines[0])
- self.assertNotIn(b'PYTHONHOME', out)
- self.assertNotIn(b'-X dev', out)
- self.assertLess(len(lines), 50)
+ options = ['-h', '-?', '--help']
+ if support.MS_WINDOWS:
+ options.append('/?')
+ for opt in options:
+ with self.subTest(opt=opt):
+ out = self.verify_valid_flag(opt)
+ lines = out.splitlines()
+ self.assertIn(b'usage', lines[0])
+ self.assertNotIn(b'PYTHONHOME', out)
+ self.assertNotIn(b'-X dev', out)
+ self.assertLess(len(lines), 50)
@support.cpython_only
@support.force_not_colorized
@@ -680,16 +684,22 @@ def test_del___main__(self):
@support.cpython_only
def test_unknown_options(self):
- rc, out, err = assert_python_failure('-E', '-z')
- self.assertIn(b'Unknown option: -z', err)
- self.assertEqual(err.splitlines().count(b'Unknown option: -z'), 1)
- self.assertEqual(b'', out)
+ # Test unknown option -a
+ for option in ('-z', '--long-option', '---'):
+ with self.subTest(option=option):
+ rc, out, err = assert_python_failure('-E', option)
+ errmsg = f'Unknown option: {option}'.encode()
+ self.assertIn(errmsg, err)
+ self.assertEqual(err.splitlines().count(errmsg), 1)
+ self.assertEqual(b'', out)
+
# Add "without='-E'" to prevent _assert_python to append -E
# to env_vars and change the output of stderr
rc, out, err = assert_python_failure('-z', without='-E')
self.assertIn(b'Unknown option: -z', err)
self.assertEqual(err.splitlines().count(b'Unknown option: -z'), 1)
self.assertEqual(b'', out)
+
rc, out, err = assert_python_failure('-a', '-z', without='-E')
self.assertIn(b'Unknown option: -a', err)
# only the first unknown option is reported
@@ -1403,6 +1413,97 @@ def test_dump_path_config(self):
self.assertIn(b'Python path configuration:', proc.err)
self.assertIn(f"PYTHONHOME = '{nonexistent}'".encode(), proc.err)
+ def test_short_options(self):
+ # Skip the test if _testcapi cannot be imported:
+ # the test uses _testcapi.config_get().
+ import_helper.import_module('_testcapi')
+
+ # Test short command line options
+ def check(options, config_names, expected):
+ if isinstance(config_names, str):
+ config_names = (config_names,)
+ if isinstance(options, str):
+ options = (options,)
+ expr = ', '.join(f'config_get({name!a})' for name in config_names)
+ code = f'from _testcapi import config_get; print({expr})'
+ args = options + ("-c", code)
+ proc = assert_python_ok(*args)
+ self.assertEqual(proc.out.rstrip(), expected.encode())
+
+ def check_ignored(option):
+ # Just test that passing the option doesn't fail
+ assert_python_ok(option, "-c", "pass")
+
+ check('-b', 'bytes_warning', '1')
+ check('-bb', 'bytes_warning', '2')
+ check('-B', 'write_bytecode', 'False')
+ check('-d', 'parser_debug', 'True')
+ check('-E', 'use_environment', 'False')
+ check('-i', ('inspect', 'interactive'), 'True True')
+ check('-I', 'isolated', 'True')
+ check('-O', 'optimization_level', '1')
+ check('-OO', 'optimization_level', '2')
+ check('-P', 'safe_path', 'True')
+ check('-q', 'quiet', 'True')
+ check('-R', 'use_hash_seed', 'False')
+ check('-s', 'user_site_directory', 'False')
+ check('-S', 'site_import', 'False')
+ check_ignored('-t')
+ check('-u', 'buffered_stdio', 'False')
+ check('-v', 'verbose', '1')
+ check('-Wignore', 'warnoptions', "['ignore']")
+ check('-x', 'skip_source_first_line', 'True')
+ check(('-X', 'xoption=value'), 'xoptions',
+ # assert_python_ok() adds -X faulthandler
+ "{'faulthandler': True, 'xoption': 'value'}")
+
+ # Short options can be combined
+ check('-bIs', ('bytes_warning', 'isolated', 'user_site_directory'),
+ '1 True False')
+
+ # -c, -h, -m, -V and -? are tested elsewhere
+
+ def test_missing_argument(self):
+ def check_missing_arg(option):
+ proc = assert_python_failure(option)
+ self.assertEqual(proc.rc, 2)
+ errmsg = f"Argument expected for the {option} option"
+ self.assertStartsWith(proc.err.rstrip(), errmsg.encode())
+
+ check_missing_arg('-c')
+ check_missing_arg('-m')
+ check_missing_arg('-W')
+ check_missing_arg('-X')
+ check_missing_arg('--check-hash-based-pycs')
+
+ def test_long_options(self):
+ # Test long command line options
+
+ # Test --check-hash-based-pycs option
+ code = 'import _imp; print(_imp.check_hash_based_pycs)'
+ opt = f"--check-hash-based-pycs"
+ for value in ('always', 'never', 'default'):
+ with self.subTest(value=value):
+ proc = assert_python_ok(opt, value, "-c", code)
+ self.assertEqual(proc.out.rstrip(), value.encode())
+
+ # "Expected long option" error
+ proc = assert_python_ok("-b-")
+ self.assertEqual(proc.out, b'')
+ self.assertEqual(proc.err.rstrip(), b"Expected long option")
+
+ # Other long options --help-all, --help-env, --help-xoptions
+ # and --version are tested elsewhere
+
+ def test_dash_option(self):
+ # Test -- in the command line
+ code = (
+ 'import sys; '
+ 'print(sys.flags.isolated, sys.flags.optimize, sys.argv)'
+ )
+ proc = assert_python_ok('-I', '-c', code, '--', '-O')
+ self.assertEqual(proc.out.rstrip(), b"1 0 ['-c', '--', '-O']")
+
@unittest.skipIf(interpreter_requires_environment(),
'Cannot run -I tests when PYTHON env vars are required.')
diff --git a/Python/getopt.c b/Python/getopt.c
index 7e918189c716a9..45454d39fc6779 100644
--- a/Python/getopt.c
+++ b/Python/getopt.c
@@ -27,18 +27,19 @@
#include <stdio.h>
#include <string.h>
#include <wchar.h>
-#include "pycore_getopt.h"
+#include "pycore_getopt.h" // struct _PyOS_GetOpt
-int _PyOS_opterr = 1; /* generate error messages */
-Py_ssize_t _PyOS_optind = 1; /* index into argv array */
-const wchar_t *_PyOS_optarg = NULL; /* optional argument */
-
-static const wchar_t *opt_ptr = L"";
/* Python command line short and long options */
#define SHORT_OPTS L"bBc:dEhiIm:OPqRsStuvVW:xX:?"
+typedef struct {
+ const wchar_t *name;
+ int has_arg;
+ int val;
+} _PyOS_LongOption;
+
static const _PyOS_LongOption longopts[] = {
/* name, has_arg, val (used in switch in initconfig.c) */
{L"check-hash-based-pycs", 1, 1},
@@ -49,113 +50,136 @@ static const _PyOS_LongOption longopts[] = {
};
-void _PyOS_ResetGetOpt(void)
+void
+_PyOS_GetOpt_Init(struct _PyOS_GetOpt *getopt,
+ Py_ssize_t argc, wchar_t * const *argv)
{
- _PyOS_opterr = 1;
- _PyOS_optind = 1;
- _PyOS_optarg = NULL;
- opt_ptr = L"";
+ getopt->error = 1;
+ getopt->index = 1;
+ getopt->arg = NULL;
+ getopt->ptr = L"";
+ getopt->argc = argc;
+ getopt->argv = argv;
}
-int _PyOS_GetOpt(Py_ssize_t argc, wchar_t * const *argv, int *longindex)
+// Parse a command line option.
+//
+// Return a character for short option (ex: return 'h' for -h).
+// Return a number for long options (see 'longopts' array).
+// Return '_' on unknown option or missing argument.
+// Return -1 when done.
+//
+// Return 'h' for --help and return 'V' for --version.
+//
+// If an option has an argument, set getopt->arg to the argument.
+// If getopt->error is non-error, write error messages to stderr.
+int
+_PyOS_GetOpt(struct _PyOS_GetOpt *getopt)
{
- wchar_t *ptr;
- wchar_t option;
+ // Local copy of read-only members to omit "getopt->"
+ const Py_ssize_t argc = getopt->argc;
+ wchar_t * const *argv = getopt->argv;
+ const int error = getopt->error;
- if (*opt_ptr == '\0') {
-
- if (_PyOS_optind >= argc)
+ if (*getopt->ptr == '\0') {
+ if (getopt->index >= argc) {
return -1;
+ }
+
+ const wchar_t *arg = argv[getopt->index];
#ifdef MS_WINDOWS
- else if (wcscmp(argv[_PyOS_optind], L"/?") == 0) {
- ++_PyOS_optind;
+ if (wcscmp(arg, L"/?") == 0) {
+ ++getopt->index;
return 'h';
}
#endif
- else if (argv[_PyOS_optind][0] != L'-' ||
- argv[_PyOS_optind][1] == L'\0' /* lone dash */ )
+ if (arg[0] != L'-' || arg[1] == L'\0' /* lone dash */ ) {
return -1;
+ }
- else if (wcscmp(argv[_PyOS_optind], L"--") == 0) {
- ++_PyOS_optind;
+ if (wcscmp(arg, L"--") == 0) {
+ ++getopt->index;
return -1;
}
-
- else if (wcscmp(argv[_PyOS_optind], L"--help") == 0) {
- ++_PyOS_optind;
+ if (wcscmp(arg, L"--help") == 0) {
+ ++getopt->index;
return 'h';
}
-
- else if (wcscmp(argv[_PyOS_optind], L"--version") == 0) {
- ++_PyOS_optind;
+ if (wcscmp(arg, L"--version") == 0) {
+ ++getopt->index;
return 'V';
}
- opt_ptr = &argv[_PyOS_optind++][1];
+ getopt->ptr = &argv[getopt->index++][1];
}
- if ((option = *opt_ptr++) == L'\0')
+ wchar_t option = *getopt->ptr++;
+ if (option == L'\0') {
return -1;
+ }
if (option == L'-') {
// Parse long option.
- if (*opt_ptr == L'\0') {
- if (_PyOS_opterr) {
+ if (*getopt->ptr == L'\0') {
+ if (error) {
fprintf(stderr, "Expected long option\n");
}
return -1;
}
- *longindex = 0;
+ int longindex = 0;
const _PyOS_LongOption *opt;
- for (opt = &longopts[*longindex]; opt->name; opt =
&longopts[++(*longindex)]) {
- if (!wcscmp(opt->name, opt_ptr))
+ for (opt = &longopts[longindex]; opt->name; opt =
&longopts[++longindex]) {
+ if (wcscmp(opt->name, getopt->ptr) == 0) {
break;
+ }
}
+
if (!opt->name) {
- if (_PyOS_opterr) {
- fprintf(stderr, "Unknown option: %ls\n", argv[_PyOS_optind -
1]);
+ if (error) {
+ fprintf(stderr, "Unknown option: %ls\n", argv[getopt->index -
1]);
}
return '_';
}
- opt_ptr = L"";
+
+ getopt->ptr = L"";
if (!opt->has_arg) {
return opt->val;
}
- if (_PyOS_optind >= argc) {
- if (_PyOS_opterr) {
+ if (getopt->index >= argc) {
+ if (error) {
fprintf(stderr, "Argument expected for the %ls options\n",
- argv[_PyOS_optind - 1]);
+ argv[getopt->index - 1]);
}
return '_';
}
- _PyOS_optarg = argv[_PyOS_optind++];
+ getopt->arg = argv[getopt->index++];
return opt->val;
}
- if ((ptr = wcschr(SHORT_OPTS, option)) == NULL) {
- if (_PyOS_opterr) {
+ wchar_t *ptr = wcschr(SHORT_OPTS, option);
+ if (ptr == NULL) {
+ if (error) {
fprintf(stderr, "Unknown option: -%c\n", (char)option);
}
return '_';
}
if (*(ptr + 1) == L':') {
- if (*opt_ptr != L'\0') {
- _PyOS_optarg = opt_ptr;
- opt_ptr = L"";
+ if (*getopt->ptr != L'\0') {
+ getopt->arg = getopt->ptr;
+ getopt->ptr = L"";
}
-
else {
- if (_PyOS_optind >= argc) {
- if (_PyOS_opterr) {
+ if (getopt->index >= argc) {
+ if (error) {
fprintf(stderr,
"Argument expected for the -%c option\n",
(char)option);
}
return '_';
}
- _PyOS_optarg = argv[_PyOS_optind++];
+ getopt->arg = argv[getopt->index++];
}
}
diff --git a/Python/initconfig.c b/Python/initconfig.c
index 6de12db9d600ec..178e6f992fa11f 100644
--- a/Python/initconfig.c
+++ b/Python/initconfig.c
@@ -3009,11 +3009,11 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
const PyWideStringList *argv = &config->argv;
int print_version = 0;
- _PyOS_ResetGetOpt();
+ struct _PyOS_GetOpt getopt;
+ _PyOS_GetOpt_Init(&getopt, argv->length, argv->items);
do {
- int longindex = -1;
- int c = _PyOS_GetOpt(argv->length, argv->items, &longindex);
- if (c == EOF) {
+ int c = _PyOS_GetOpt(&getopt);
+ if (c == -1) {
break;
}
@@ -3022,12 +3022,12 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
/* -c is the last option; following arguments
that look like options are left for the
command to interpret. */
- size_t len = wcslen(_PyOS_optarg) + 1 + 1;
+ size_t len = wcslen(getopt.arg) + 1 + 1;
wchar_t *command = PyMem_RawMalloc(sizeof(wchar_t) * len);
if (command == NULL) {
return _PyStatus_NO_MEMORY();
}
- memcpy(command, _PyOS_optarg, (len - 2) * sizeof(wchar_t));
+ memcpy(command, getopt.arg, (len - 2) * sizeof(wchar_t));
command[len - 2] = '\n';
command[len - 1] = 0;
config->run_command = command;
@@ -3040,7 +3040,7 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
that look like options are left for the
module to interpret. */
if (config->run_module == NULL) {
- config->run_module = _PyMem_RawWcsdup(_PyOS_optarg);
+ config->run_module = _PyMem_RawWcsdup(getopt.arg);
if (config->run_module == NULL) {
return _PyStatus_NO_MEMORY();
}
@@ -3051,13 +3051,13 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
switch (c) {
// Integers represent long options, see Python/getopt.c
case 1:
- // check-hash-based-pycs
- if (wcscmp(_PyOS_optarg, L"always") == 0
- || wcscmp(_PyOS_optarg, L"never") == 0
- || wcscmp(_PyOS_optarg, L"default") == 0)
+ // --check-hash-based-pycs option
+ if (wcscmp(getopt.arg, L"always") == 0
+ || wcscmp(getopt.arg, L"never") == 0
+ || wcscmp(getopt.arg, L"default") == 0)
{
status = PyConfig_SetString(config,
&config->check_hash_pycs_mode,
- _PyOS_optarg);
+ getopt.arg);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
@@ -3067,17 +3067,17 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
break;
case 2:
- // help-all
+ // --help-all option
DEFER_OPTION(c);
break;
case 3:
- // help-env
+ // --help-env option
DEFER_OPTION(c);
break;
case 4:
- // help-xoptions
+ // --help-xoptions option
DEFER_OPTION(c);
break;
@@ -3146,7 +3146,7 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
break;
case 'W':
- status = PyWideStringList_Append(warnoptions, _PyOS_optarg);
+ status = PyWideStringList_Append(warnoptions, getopt.arg);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
@@ -3177,22 +3177,22 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
}
if (config->run_command == NULL && config->run_module == NULL
- && _PyOS_optind < argv->length
- && wcscmp(argv->items[_PyOS_optind], L"-") != 0
+ && getopt.index < argv->length
+ && wcscmp(argv->items[getopt.index], L"-") != 0
&& config->run_filename == NULL)
{
- config->run_filename = _PyMem_RawWcsdup(argv->items[_PyOS_optind]);
+ config->run_filename = _PyMem_RawWcsdup(argv->items[getopt.index]);
if (config->run_filename == NULL) {
return _PyStatus_NO_MEMORY();
}
}
if (config->run_command != NULL || config->run_module != NULL) {
- /* Backup _PyOS_optind */
- _PyOS_optind--;
+ /* Backup getopt.index */
+ getopt.index--;
}
- *opt_index = _PyOS_optind;
+ *opt_index = getopt.index;
return _PyStatus_OK();
@@ -3257,6 +3257,9 @@ _PyConfig_ProcessDeferredCmdlineOption(PyConfig *config)
config_xoptions_usage();
return 0;
+ case '_':
+ // Unknown option or missing argument
+ _Py_FALLTHROUGH;
default:
config_usage(1, program);
return 2;
diff --git a/Python/preconfig.c b/Python/preconfig.c
index 844ac8e6372fc5..c8005ccdce1a73 100644
--- a/Python/preconfig.c
+++ b/Python/preconfig.c
@@ -187,15 +187,14 @@ precmdline_parse_cmdline(_PyPreCmdline *cmdline)
{
const PyWideStringList *argv = &cmdline->argv;
- _PyOS_ResetGetOpt();
+ struct _PyOS_GetOpt getopt;
+ _PyOS_GetOpt_Init(&getopt, argv->length, argv->items);
/* Don't log parsing errors into stderr here: PyConfig_Read()
is responsible for that */
- _PyOS_opterr = 0;
+ getopt.error = 0;
do {
- int longindex = -1;
- int c = _PyOS_GetOpt(argv->length, argv->items, &longindex);
-
- if (c == EOF || c == 'c' || c == 'm') {
+ int c = _PyOS_GetOpt(&getopt);
+ if (c == -1 || c == 'c' || c == 'm') {
break;
}
@@ -211,7 +210,7 @@ precmdline_parse_cmdline(_PyPreCmdline *cmdline)
case 'X':
{
PyStatus status = PyWideStringList_Append(&cmdline->xoptions,
- _PyOS_optarg);
+ getopt.arg);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]