Skip to content

Commit 74ded2a

Browse files
[3.14] gh-93016: Fix parsing of arguments in the IDLE "Run... Customized" dialog (GH-157635) (#158049)
gh-93016: Fix parsing of arguments in the IDLE "Run... Customized" dialog (GH-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. (cherry picked from commit db2d1a5) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent edc1034 commit 74ded2a

5 files changed

Lines changed: 113 additions & 6 deletions

File tree

‎Lib/idlelib/idle_test/test_query.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,20 @@ def test_blank_args(self):
280280
dialog = self.Dummy_CustomRun(' ')
281281
self.assertEqual(dialog.cli_args_ok(), [])
282282

283+
@unittest.skipIf(sys.platform == 'win32', 'not an error on Windows')
283284
def test_invalid_args(self):
284285
dialog = self.Dummy_CustomRun("'no-closing-quote")
285286
self.assertEqual(dialog.cli_args_ok(), None)
286287
self.assertIn('No closing', dialog.entry_error['text'])
287288

289+
@unittest.skipUnless(sys.platform == 'win32', 'Windows only')
290+
def test_windows_args(self):
291+
# gh-93016: backslashes are not escapes on Windows.
292+
dialog = self.Dummy_CustomRun(r'c:\Users "c:\Program Files"')
293+
self.assertEqual(dialog.cli_args_ok(),
294+
[r'c:\Users', r'c:\Program Files'])
295+
self.assertEqual(dialog.entry_error['text'], '')
296+
288297
def test_good_args(self):
289298
args = ['-n', '10', '--verbose', '-p', '/path', '--name']
290299
dialog = self.Dummy_CustomRun(' '.join(args) + ' "my name"')
@@ -444,6 +453,12 @@ def test_click_args(self):
444453
dialog.entry.insert(END, ' c')
445454
dialog.button_ok.invoke()
446455
self.assertEqual(dialog.result, (['a', 'b=1', 'c'], True))
456+
# gh-93016: arguments with spaces and backslashes round-trip.
457+
args = ['a b', r'c:\dir\x']
458+
dialog = query.CustomRun(root, 'Title', cli_args=args, _utest=True)
459+
self.assertNotIn('{', dialog.entry.get())
460+
dialog.button_ok.invoke()
461+
self.assertEqual(dialog.result, (args, True))
447462
root.destroy()
448463

449464

‎Lib/idlelib/idle_test/test_util.py‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
import unittest
55
from unittest import mock
6-
from test.support import requires
6+
from test.support import requires, subTests
77
from test.support.isolation import runInSubprocess
88
import tkinter
99
from tkinter import EventType
@@ -163,5 +163,38 @@ def test_fix_x11_paste(self):
163163
self.assertEqual(after, before[cls])
164164

165165

166+
class CLIargsTest(unittest.TestCase):
167+
"Test the command line splitting and joining functions (gh-93016)."
168+
169+
# Expected results were verified against sys.argv of python.exe.
170+
@subTests('cli_string,args', [
171+
(r'c:\Users', [r'c:\Users']),
172+
(r'\\server\share', [r'\\server\share']),
173+
(r'"c:\Program Files\x" 1 2', [r'c:\Program Files\x', '1', '2']),
174+
(r' x y ', ['x', 'y']),
175+
('a\tb', ['a', 'b']),
176+
(r'"a b"c', ['a bc']),
177+
(r'a"b c"d', ['ab cd']),
178+
(r'"a""b"', ['a"b']),
179+
(r'a\"b', ['a"b']),
180+
(r'a\\"b c" d', ['a\\b c', 'd']),
181+
(r'a\\\"b', ['a\\"b']),
182+
(r'"x\\"', ['x\\']),
183+
('"c:\\Users\\"', ['c:\\Users"']),
184+
('x\\', ['x\\']),
185+
(r'""', ['']),
186+
(r'"" a', ['', 'a']),
187+
(r'"', ['']),
188+
('', []),
189+
])
190+
def test_split_windows(self, cli_string, args):
191+
self.assertEqual(util._split_windows(cli_string), args)
192+
193+
@subTests('args', [['a'], ['a b'], [r'c:\x'], ['q"q'], ["s's"], [''],
194+
['\\'], ['\\"'], ['a b', r'c:\x', '', 'q"q']])
195+
def test_split_join(self, args):
196+
self.assertEqual(util.split_cli_args(util.join_cli_args(args)), args)
197+
198+
166199
if __name__ == '__main__':
167200
unittest.main(verbosity=2)

‎Lib/idlelib/query.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import importlib.util, importlib.abc
2323
import os
24-
import shlex
2524
from sys import executable, platform # Platform is set for one test.
2625

2726
from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
@@ -30,6 +29,8 @@
3029
from tkinter.font import Font
3130
from tkinter.simpledialog import _setup_dialog
3231

32+
from idlelib.util import split_cli_args, join_cli_args
33+
3334
class Query(Toplevel):
3435
"""Base class for getting verified answer from a user.
3536
@@ -332,6 +333,7 @@ def entry_ok(self):
332333
path = self.path_ok()
333334
return None if name is None or path is None else (name, path)
334335

336+
335337
class CustomRun(Query):
336338
"""Get settings for custom run of module.
337339
@@ -344,12 +346,11 @@ def __init__(self, parent, title, *, cli_args=[],
344346
_htest=False, _utest=False):
345347
"""cli_args is a list of strings.
346348
347-
The list is assigned to the default Entry StringVar.
348-
The strings are displayed joined by ' ' for display.
349+
The strings are quoted and joined for display in the Entry.
349350
"""
350351
message = 'Command Line Arguments for sys.argv:'
351352
super().__init__(
352-
parent, title, message, text0=cli_args,
353+
parent, title, message, text0=join_cli_args(cli_args),
353354
_htest=_htest, _utest=_utest)
354355

355356
def create_extra(self):
@@ -369,7 +370,7 @@ def cli_args_ok(self):
369370
"Return command line arg list or None if error."
370371
cli_string = self.entry.get().strip()
371372
try:
372-
cli_args = shlex.split(cli_string, posix=True)
373+
cli_args = split_cli_args(cli_string)
373374
except ValueError as err:
374375
self.showerror(str(err))
375376
return None

‎Lib/idlelib/util.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* std streams (pyshell, run),
1313
* warning stuff (pyshell, run).
1414
"""
15+
import re
1516
import sys
1617

1718
# .pyw is for Windows; .pyi is for typing stub files.
@@ -114,6 +115,60 @@ def wheel_event(event, widget=None):
114115
return 'break'
115116

116117

118+
_cli_token_re = re.compile(r"""
119+
(?P<backslashes>\\*)(?P<quotes>"+)
120+
| (?P<literal>\\+|[^ \t"\\]+) # backslashes not followed by a quote
121+
| (?P<space>[ \t]+)
122+
""", re.VERBOSE)
123+
124+
125+
def _split_windows(cli_string):
126+
"""Split a command line into arguments as the C runtime does.
127+
128+
See https://learn.microsoft.com/cpp/c-language/parsing-c-command-line-arguments
129+
"""
130+
args = []
131+
arg = None # None when not in an argument.
132+
quoted = False
133+
for m in _cli_token_re.finditer(cli_string):
134+
match m.lastgroup:
135+
case 'space' if not quoted:
136+
if arg is not None:
137+
args.append(arg)
138+
arg = None
139+
case 'space' | 'literal':
140+
arg = (arg or '') + m[0]
141+
case _: # Backslashes followed by quotes.
142+
count = len(m['backslashes'])
143+
escaped = count % 2 # Odd backslashes escape a quote.
144+
bare = len(m['quotes']) - escaped
145+
# In a quoted part every two quotes give a literal quote;
146+
# if not quoted, the first quote opens a quoted part.
147+
literal = escaped + ((bare + quoted - 1) // 2 if bare else 0)
148+
arg = (arg or '') + '\\' * (count // 2) + '"' * literal
149+
quoted ^= bare % 2
150+
if arg is not None:
151+
args.append(arg)
152+
return args
153+
154+
155+
def split_cli_args(cli_string): # Called in query.
156+
"Split a command line as the Python executable does (gh-93016)."
157+
if sys.platform == 'win32':
158+
return _split_windows(cli_string)
159+
import shlex
160+
return shlex.split(cli_string)
161+
162+
163+
def join_cli_args(cli_args): # Called in query.
164+
"Join arguments into a command line which split_cli_args() splits back."
165+
if sys.platform == 'win32':
166+
import subprocess
167+
return subprocess.list2cmdline(cli_args)
168+
import shlex
169+
return shlex.join(cli_args)
170+
171+
117172
if __name__ == '__main__':
118173
from unittest import main
119174
main('idlelib.idle_test.test_util', verbosity=2)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix parsing of command line arguments in the IDLE "Run... Customized" dialog
2+
on Windows: backslashes are no longer treated as escape characters. Fix also
3+
display of the previous arguments in the dialog.

0 commit comments

Comments
 (0)