Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ def run(*popenargs,
return CompletedProcess(process.args, retcode, stdout, stderr)


def list2cmdline(seq):
def list2cmdline(seq, escape_wildcard=False):
"""
Translate a sequence of arguments into a command line
string, using the same rules as the MS C runtime:
Expand Down Expand Up @@ -573,6 +573,10 @@ def list2cmdline(seq):
if c == '\\':
# Don't know if we need to double yet.
bs_buf.append(c)
elif escape_wildcard and c == '*':
result.append('\\*')
elif escape_wildcard and c == '?':
result.append('\\?')
elif c == '"':
# Double backslashes.
result.append('\\' * len(bs_buf)*2)
Expand Down Expand Up @@ -741,6 +745,15 @@ class Popen(object):

pass_fds (POSIX only)

escape_wildcard: (Win32 only, POSIX have no effect) If true, the wildcard
character * and ? will be escaped to \* and \? on win32. This is useful
for program such as git that need wildcard character as parameter and
not match to local files.
For such command `git describe --match=v* --always`, if we don't escape
* to \*, then the Windows msvcrt will automatically match the files like
--match=v123 if it exist; and pass '--match=v123' arg to git, and that's
not git wanted, git wants the original '--match=v*'.

encoding and errors: Text mode encoding and error handling to use for
file objects stdin, stdout and stderr.

Expand All @@ -756,7 +769,7 @@ def __init__(self, args, bufsize=-1, executable=None,
startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False,
pass_fds=(), *, user=None, group=None, extra_groups=None,
encoding=None, errors=None, text=None, umask=-1):
encoding=None, errors=None, text=None, umask=-1, escape_wildcard=False):
"""Create new Popen instance."""
_cleanup()
# Held while anything is calling waitpid before returncode has been
Expand Down Expand Up @@ -952,7 +965,7 @@ def __init__(self, args, bufsize=-1, executable=None,
errread, errwrite,
restore_signals,
gid, gids, uid, umask,
start_new_session)
start_new_session, escape_wildcard)
except:
# Cleanup if the child failed starting.
for f in filter(None, (self.stdin, self.stdout, self.stderr)):
Expand Down Expand Up @@ -1336,7 +1349,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
unused_restore_signals,
unused_gid, unused_gids, unused_uid,
unused_umask,
unused_start_new_session):
unused_start_new_session, escape_wildcard):
"""Execute program (MS Windows version)"""

assert not pass_fds, "pass_fds not supported on Windows."
Expand All @@ -1346,14 +1359,14 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
elif isinstance(args, bytes):
if shell:
raise TypeError('bytes args is not allowed on Windows')
args = list2cmdline([args])
args = list2cmdline([args], escape_wildcard)
elif isinstance(args, os.PathLike):
if shell:
raise TypeError('path-like args is not allowed when '
'shell is true')
args = list2cmdline([args])
args = list2cmdline([args], escape_wildcard)
else:
args = list2cmdline(args)
args = list2cmdline(args, escape_wildcard)

if executable is not None:
executable = os.fsdecode(executable)
Expand Down Expand Up @@ -1664,7 +1677,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
errread, errwrite,
restore_signals,
gid, gids, uid, umask,
start_new_session):
start_new_session, escape_wildcard):
"""Execute program (POSIX version)"""

if isinstance(args, (str, bytes)):
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,14 @@ def test_no_leaking(self):
shutil.rmtree(tmpdir)

def test_list2cmdline(self):
self.assertEqual(subprocess.list2cmdline(['a b* c', 'd', 'e'], False),
'"a b* c" d e')
self.assertEqual(subprocess.list2cmdline(['a b* c', 'd', 'e'], True),
'"a b\\* c" d e')
self.assertEqual(subprocess.list2cmdline(['a b? c', 'd', 'e'], False),
'"a b? c" d e')
self.assertEqual(subprocess.list2cmdline(['a b? c', 'd', 'e'], True),
'"a b\\? c" d e')
self.assertEqual(subprocess.list2cmdline(['a b c', 'd', 'e']),
'"a b c" d e')
self.assertEqual(subprocess.list2cmdline(['ab"c', '\\', 'd']),
Expand Down