Skip to content

Commit a650fab

Browse files
committed
Implement Missing Command Line Quoting on Windows
1 parent a733b6b commit a650fab

File tree

1 file changed

+57
-2
lines changed

1 file changed

+57
-2
lines changed

Foundation/Process.swift

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

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

320376
#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/
323377
var command: [String] = [launchPath]
324378
if let arguments = self.arguments {
325379
command.append(contentsOf: arguments)
326380
}
381+
command = command.map(quoteWindowsCommandArg)
327382

328383
var siStartupInfo: STARTUPINFOW = STARTUPINFOW()
329384
siStartupInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)

0 commit comments

Comments
 (0)