7575OUTPUT_ENCODING_ENV_VAR = 'AWS_CLI_OUTPUT_ENCODING'
7676PYTHONUTF8_ENV_VAR = 'PYTHONUTF8'
7777
78+ # cmd.exe characters that require double-quoting to be treated as literals.
79+ # https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd
80+ _WIN_CMD_UNSAFE_CHARS = set ('&<>[]|{}^=;!\' ()+,`~ \t ' )
81+
7882LOG = logging .getLogger (__name__ )
7983
8084
@@ -261,13 +265,14 @@ def compat_input(prompt):
261265 # This is unused directly when the user pastes the cross-device
262266 # verification code, which may be longer than some terminal's buffers
263267 import readline # noqa: F401
268+
264269 return raw_input ()
265270 except ImportError :
266271 LOG .debug ('readline module not available' )
267272 return raw_input ()
268273
269274
270- def compat_shell_quote (s , platform = None ):
275+ def compat_shell_quote (s , platform = None , shell = False ):
271276 """Return a shell-escaped version of the string *s*
272277
273278 Unfortunately `shlex.quote` doesn't support Windows, so this method
@@ -277,13 +282,15 @@ def compat_shell_quote(s, platform=None):
277282 platform = sys .platform
278283
279284 if platform == "win32" :
280- return _windows_shell_quote (s )
285+ if shell :
286+ return _windows_cmd_shell_quote (s )
287+ return _windows_argv_quote (s )
281288 else :
282289 return shlex .quote (s )
283290
284291
285- def _windows_shell_quote (s ):
286- """Return a Windows shell -escaped version of the string *s*
292+ def _windows_argv_quote (s ):
293+ """Return a Windows argv -escaped version of the string *s*
287294
288295 Windows has potentially bizarre rules depending on where you look. When
289296 spawning a process via the Windows C runtime the rules are as follows:
@@ -305,37 +312,37 @@ def _windows_shell_quote(s):
305312 return '""'
306313
307314 buff = []
308- num_backspaces = 0
315+ num_backslashes = 0
309316 for character in s :
310317 if character == '\\ ' :
311318 # We can't simply append backslashes because we don't know if
312319 # they will need to be escaped. Instead we separately keep track
313320 # of how many we've seen.
314- num_backspaces += 1
321+ num_backslashes += 1
315322 elif character == '"' :
316- if num_backspaces > 0 :
323+ if num_backslashes > 0 :
317324 # The backslashes are part of a chain that lead up to a
318325 # double quote, so they need to be escaped.
319- buff .append ('\\ ' * (num_backspaces * 2 ))
320- num_backspaces = 0
326+ buff .append ('\\ ' * (num_backslashes * 2 ))
327+ num_backslashes = 0
321328
322329 # The double quote also needs to be escaped. The fact that we're
323330 # seeing it at all means that it must have been escaped in the
324331 # original source.
325332 buff .append ('\\ "' )
326333 else :
327- if num_backspaces > 0 :
334+ if num_backslashes > 0 :
328335 # The backslashes aren't part of a chain leading up to a
329336 # double quote, so they can be inserted directly without
330337 # being escaped.
331- buff .append ('\\ ' * num_backspaces )
332- num_backspaces = 0
338+ buff .append ('\\ ' * num_backslashes )
339+ num_backslashes = 0
333340 buff .append (character )
334341
335- # There may be some leftover backspaces if they were on the trailing
342+ # There may be some leftover backslashes if they were on the trailing
336343 # end, so they're added back in here.
337- if num_backspaces > 0 :
338- buff .append ('\\ ' * num_backspaces )
344+ if num_backslashes > 0 :
345+ buff .append ('\\ ' * num_backslashes )
339346
340347 new_s = '' .join (buff )
341348 if ' ' in new_s or '\t ' in new_s :
@@ -345,6 +352,60 @@ def _windows_shell_quote(s):
345352 return new_s
346353
347354
355+ def _windows_cmd_shell_quote (s ):
356+ """Return a Windows shell-escaped version of the string *s* that is
357+ safe to pass through cmd.exe
358+
359+ Handles two interpretation layers:
360+ 1. cmd.exe metacharacters - neutralized by double-quoting when
361+ the string contains any cmd.exe special characters.
362+ 2. MSVC C runtime argv parsing - backslash/double-quote escaping
363+ so the target process receives the correct argument.
364+
365+ Note: cmd.exe %VAR% expansion and !VAR! delayed expansion
366+ cannot be reliably escaped inside double quotes on the
367+ command line and are not handled here.
368+
369+ :param s: A string to escape
370+ :return: An escaped string
371+ """
372+ if not s :
373+ return '""'
374+
375+ buff = []
376+ num_backslashes = 0
377+ needs_quoting = False
378+ for character in s :
379+ if character == '\\ ' :
380+ num_backslashes += 1
381+ elif character == '"' :
382+ if num_backslashes > 0 :
383+ buff .append ('\\ ' * (num_backslashes * 2 ))
384+ num_backslashes = 0
385+ buff .append ('\\ "' )
386+ needs_quoting = True
387+ else :
388+ if num_backslashes > 0 :
389+ buff .append ('\\ ' * num_backslashes )
390+ num_backslashes = 0
391+ if character in _WIN_CMD_UNSAFE_CHARS :
392+ needs_quoting = True
393+ buff .append (character )
394+
395+ if needs_quoting :
396+ # Trailing backslashes must be doubled when we append a closing
397+ # double quote — without doubling, a trailing backslash would
398+ # escape the closing quote.
399+ if num_backslashes > 0 :
400+ buff .append ('\\ ' * (num_backslashes * 2 ))
401+ inner = '' .join (buff )
402+ return f'"{ inner } "'
403+
404+ if num_backslashes > 0 :
405+ buff .append ('\\ ' * num_backslashes )
406+ return '' .join (buff )
407+
408+
348409def get_popen_kwargs_for_pager_cmd (pager_cmd = None ):
349410 """Returns the default pager to use dependent on platform
350411
0 commit comments