https://github.com/python/cpython/commit/db2d1a5a159b8e7862648750f80cde0b30d02f47
commit: db2d1a5a159b8e7862648750f80cde0b30d02f47
branch: main
author: Serhiy Storchaka <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-09-23T21:23:33-04:00
summary:
gh-93016: Fix parsing of arguments in the IDLE "Run... Customized" dialog
(#157635)
On Windows, split the command line as the Python executable does instead
of using the POSIX rules, so that backslashes are not escape characters.
Display previous arguments quoted and joined, not as a Tcl list.
files:
A Misc/NEWS.d/next/IDLE/2026-09-16-20-00-00.gh-issue-93016.cliarg.rst
M Lib/idlelib/idle_test/test_query.py
M Lib/idlelib/idle_test/test_util.py
M Lib/idlelib/query.py
M Lib/idlelib/util.py
diff --git a/Lib/idlelib/idle_test/test_query.py
b/Lib/idlelib/idle_test/test_query.py
index 58c173723a5ada..3b0a3388f64e33 100644
--- a/Lib/idlelib/idle_test/test_query.py
+++ b/Lib/idlelib/idle_test/test_query.py
@@ -280,11 +280,20 @@ def test_blank_args(self):
dialog = self.Dummy_CustomRun(' ')
self.assertEqual(dialog.cli_args_ok(), [])
+ @unittest.skipIf(sys.platform == 'win32', 'not an error on Windows')
def test_invalid_args(self):
dialog = self.Dummy_CustomRun("'no-closing-quote")
self.assertEqual(dialog.cli_args_ok(), None)
self.assertIn('No closing', dialog.entry_error['text'])
+ @unittest.skipUnless(sys.platform == 'win32', 'Windows only')
+ def test_windows_args(self):
+ # gh-93016: backslashes are not escapes on Windows.
+ dialog = self.Dummy_CustomRun(r'c:\Users "c:\Program Files"')
+ self.assertEqual(dialog.cli_args_ok(),
+ [r'c:\Users', r'c:\Program Files'])
+ self.assertEqual(dialog.entry_error['text'], '')
+
def test_good_args(self):
args = ['-n', '10', '--verbose', '-p', '/path', '--name']
dialog = self.Dummy_CustomRun(' '.join(args) + ' "my name"')
@@ -444,6 +453,12 @@ def test_click_args(self):
dialog.entry.insert(END, ' c')
dialog.button_ok.invoke()
self.assertEqual(dialog.result, (['a', 'b=1', 'c'], True))
+ # gh-93016: arguments with spaces and backslashes round-trip.
+ args = ['a b', r'c:\dir\x']
+ dialog = query.CustomRun(root, 'Title', cli_args=args, _utest=True)
+ self.assertNotIn('{', dialog.entry.get())
+ dialog.button_ok.invoke()
+ self.assertEqual(dialog.result, (args, True))
root.destroy()
diff --git a/Lib/idlelib/idle_test/test_util.py
b/Lib/idlelib/idle_test/test_util.py
index d90a0784075d3e..adf871fe4399cb 100644
--- a/Lib/idlelib/idle_test/test_util.py
+++ b/Lib/idlelib/idle_test/test_util.py
@@ -3,7 +3,7 @@
import sys
import unittest
from unittest import mock
-from test.support import requires
+from test.support import requires, subTests
from test.support.isolation import runInSubprocess
import tkinter
from tkinter import EventType
@@ -162,5 +162,38 @@ def test_fix_x11_paste(self):
self.assertEqual(after, before[cls])
+class CLIargsTest(unittest.TestCase):
+ "Test the command line splitting and joining functions (gh-93016)."
+
+ # Expected results were verified against sys.argv of python.exe.
+ @subTests('cli_string,args', [
+ (r'c:\Users', [r'c:\Users']),
+ (r'\\server\share', [r'\\server\share']),
+ (r'"c:\Program Files\x" 1 2', [r'c:\Program Files\x', '1', '2']),
+ (r' x y ', ['x', 'y']),
+ ('a\tb', ['a', 'b']),
+ (r'"a b"c', ['a bc']),
+ (r'a"b c"d', ['ab cd']),
+ (r'"a""b"', ['a"b']),
+ (r'a\"b', ['a"b']),
+ (r'a\\"b c" d', ['a\\b c', 'd']),
+ (r'a\\\"b', ['a\\"b']),
+ (r'"x\\"', ['x\\']),
+ ('"c:\\Users\\"', ['c:\\Users"']),
+ ('x\\', ['x\\']),
+ (r'""', ['']),
+ (r'"" a', ['', 'a']),
+ (r'"', ['']),
+ ('', []),
+ ])
+ def test_split_windows(self, cli_string, args):
+ self.assertEqual(util._split_windows(cli_string), args)
+
+ @subTests('args', [['a'], ['a b'], [r'c:\x'], ['q"q'], ["s's"], [''],
+ ['\\'], ['\\"'], ['a b', r'c:\x', '', 'q"q']])
+ def test_split_join(self, args):
+ self.assertEqual(util.split_cli_args(util.join_cli_args(args)), args)
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py
index 5f9bdc031e544b..6217b8123de76d 100644
--- a/Lib/idlelib/query.py
+++ b/Lib/idlelib/query.py
@@ -21,7 +21,6 @@
import importlib.util, importlib.abc
import os
-import shlex
from sys import executable, platform # Platform is set for one test.
from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
@@ -30,6 +29,8 @@
from tkinter.font import Font
from tkinter.simpledialog import _setup_dialog
+from idlelib.util import split_cli_args, join_cli_args
+
class Query(Toplevel):
"""Base class for getting verified answer from a user.
@@ -332,6 +333,7 @@ def entry_ok(self):
path = self.path_ok()
return None if name is None or path is None else (name, path)
+
class CustomRun(Query):
"""Get settings for custom run of module.
@@ -344,12 +346,11 @@ def __init__(self, parent, title, *, cli_args=[],
_htest=False, _utest=False):
"""cli_args is a list of strings.
- The list is assigned to the default Entry StringVar.
- The strings are displayed joined by ' ' for display.
+ The strings are quoted and joined for display in the Entry.
"""
message = 'Command Line Arguments for sys.argv:'
super().__init__(
- parent, title, message, text0=cli_args,
+ parent, title, message, text0=join_cli_args(cli_args),
_htest=_htest, _utest=_utest)
def create_extra(self):
@@ -369,7 +370,7 @@ def cli_args_ok(self):
"Return command line arg list or None if error."
cli_string = self.entry.get().strip()
try:
- cli_args = shlex.split(cli_string, posix=True)
+ cli_args = split_cli_args(cli_string)
except ValueError as err:
self.showerror(str(err))
return None
diff --git a/Lib/idlelib/util.py b/Lib/idlelib/util.py
index f408daf728d4a9..68391d5eef2b9f 100644
--- a/Lib/idlelib/util.py
+++ b/Lib/idlelib/util.py
@@ -12,6 +12,7 @@
* std streams (pyshell, run),
* warning stuff (pyshell, run).
"""
+import re
import sys
# .pyw is for Windows; .pyi is for typing stub files.
@@ -114,6 +115,60 @@ def wheel_event(event, widget=None):
return 'break'
+_cli_token_re = re.compile(r"""
+ (?P<backslashes>\\*)(?P<quotes>"+)
+ | (?P<literal>\\+|[^ \t"\\]+) # backslashes not followed by a quote
+ | (?P<space>[ \t]+)
+""", re.VERBOSE)
+
+
+def _split_windows(cli_string):
+ """Split a command line into arguments as the C runtime does.
+
+ See
https://learn.microsoft.com/cpp/c-language/parsing-c-command-line-arguments
+ """
+ args = []
+ arg = None # None when not in an argument.
+ quoted = False
+ for m in _cli_token_re.finditer(cli_string):
+ match m.lastgroup:
+ case 'space' if not quoted:
+ if arg is not None:
+ args.append(arg)
+ arg = None
+ case 'space' | 'literal':
+ arg = (arg or '') + m[0]
+ case _: # Backslashes followed by quotes.
+ count = len(m['backslashes'])
+ escaped = count % 2 # Odd backslashes escape a quote.
+ bare = len(m['quotes']) - escaped
+ # In a quoted part every two quotes give a literal quote;
+ # if not quoted, the first quote opens a quoted part.
+ literal = escaped + ((bare + quoted - 1) // 2 if bare else 0)
+ arg = (arg or '') + '\\' * (count // 2) + '"' * literal
+ quoted ^= bare % 2
+ if arg is not None:
+ args.append(arg)
+ return args
+
+
+def split_cli_args(cli_string): # Called in query.
+ "Split a command line as the Python executable does (gh-93016)."
+ if sys.platform == 'win32':
+ return _split_windows(cli_string)
+ import shlex
+ return shlex.split(cli_string)
+
+
+def join_cli_args(cli_args): # Called in query.
+ "Join arguments into a command line which split_cli_args() splits back."
+ if sys.platform == 'win32':
+ import subprocess
+ return subprocess.list2cmdline(cli_args)
+ import shlex
+ return shlex.join(cli_args)
+
+
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_util', verbosity=2)
diff --git
a/Misc/NEWS.d/next/IDLE/2026-09-16-20-00-00.gh-issue-93016.cliarg.rst
b/Misc/NEWS.d/next/IDLE/2026-09-16-20-00-00.gh-issue-93016.cliarg.rst
new file mode 100644
index 00000000000000..a9fe00a72fe87c
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2026-09-16-20-00-00.gh-issue-93016.cliarg.rst
@@ -0,0 +1,3 @@
+Fix parsing of command line arguments in the IDLE "Run... Customized" dialog
+on Windows: backslashes are no longer treated as escape characters. Fix also
+display of the previous arguments in the dialog.
_______________________________________________
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]