Skip to content

Commit fd5641d

Browse files
authored
Merge pull request #2032 from jmittert/FixWinCommandLine
Implement Missing Command Line Quoting on Windows
2 parents 80bbe98 + a257b03 commit fd5641d

File tree

1 file changed

+62
-3
lines changed

1 file changed

+62
-3
lines changed

Foundation/Process.swift

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,67 @@ private func processIsEqual(_ a : UnsafeRawPointer?, _ b : UnsafeRawPointer?) ->
102102
return true
103103
}
104104

105+
#if os(Windows)
106+
107+
private func quoteWindowsCommandLine(_ commandLine: [String]) -> String {
108+
func quoteWindowsCommandArg(arg: String) -> String {
109+
// Windows escaping, adapted from Daniel Colascione's "Everyone quotes
110+
// command line arguments the wrong way" - Microsoft Developer Blog
111+
if !arg.contains(where: {" \t\n\"".contains($0)}) {
112+
return arg
113+
}
114+
115+
// To escape the command line, we surround the argument with quotes. However
116+
// the complication comes due to how the Windows command line parser treats
117+
// backslashes (\) and quotes (")
118+
//
119+
// - \ is normally treated as a literal backslash
120+
// - e.g. foo\bar\baz => foo\bar\baz
121+
// - However, the sequence \" is treated as a literal "
122+
// - e.g. foo\"bar => foo"bar
123+
//
124+
// But then what if we are given a path that ends with a \? Surrounding
125+
// foo\bar\ with " would be "foo\bar\" which would be an unterminated string
126+
127+
// since it ends on a literal quote. To allow this case the parser treats:
128+
//
129+
// - \\" as \ followed by the " metachar
130+
// - \\\" as \ followed by a literal "
131+
// - In general:
132+
// - 2n \ followed by " => n \ followed by the " metachar
133+
// - 2n+1 \ followed by " => n \ followed by a literal "
134+
var quoted = "\""
135+
var unquoted = arg.unicodeScalars
136+
137+
while !unquoted.isEmpty {
138+
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
139+
// String ends with a backslash e.g. foo\bar\, escape all the backslashes
140+
// then add the metachar " below
141+
let backslashCount = unquoted.count
142+
quoted.append(String(repeating: "\\", count: backslashCount * 2))
143+
break
144+
}
145+
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
146+
if (unquoted[firstNonBackslash] == "\"") {
147+
// This is a string of \ followed by a " e.g. foo\"bar. Escape the
148+
// backslashes and the quote
149+
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
150+
quoted.append(String(unquoted[firstNonBackslash]))
151+
} else {
152+
// These are just literal backslashes
153+
quoted.append(String(repeating: "\\", count: backslashCount))
154+
quoted.append(String(unquoted[firstNonBackslash]))
155+
}
156+
// Drop the backslashes and the following character
157+
unquoted.removeFirst(backslashCount + 1)
158+
}
159+
quoted.append("\"")
160+
return quoted
161+
}
162+
return commandLine.map(quoteWindowsCommandArg).joined(separator: " ")
163+
}
164+
#endif
165+
105166
open class Process: NSObject {
106167
private static func setup() {
107168
struct Once {
@@ -318,8 +379,6 @@ open class Process: NSObject {
318379
}
319380

320381
#if os(Windows)
321-
// TODO(compnerd) quote the commandline correctly
322-
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
323382
var command: [String] = [launchPath]
324383
if let arguments = self.arguments {
325384
command.append(contentsOf: arguments)
@@ -412,7 +471,7 @@ open class Process: NSObject {
412471
CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0)
413472
CFRunLoopAddSource(managerThreadRunLoop?._cfRunLoop, source, kCFRunLoopDefaultMode)
414473

415-
try command.joined(separator: " ").withCString(encodedAs: UTF16.self) { wszCommandLine in
474+
try quoteWindowsCommandLine(command).withCString(encodedAs: UTF16.self) { wszCommandLine in
416475
try currentDirectoryURL.path.withCString(encodedAs: UTF16.self) { wszCurrentDirectory in
417476
try szEnvironment.withCString(encodedAs: UTF16.self) { wszEnvironment in
418477
if CreateProcessW(nil, UnsafeMutablePointer<WCHAR>(mutating: wszCommandLine),

0 commit comments

Comments
 (0)