@@ -102,6 +102,62 @@ private func processIsEqual(_ a : UnsafeRawPointer?, _ b : UnsafeRawPointer?) ->
102
102
return true
103
103
}
104
104
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
+
105
161
open class Process : NSObject {
106
162
private static func setup( ) {
107
163
struct Once {
@@ -318,12 +374,11 @@ open class Process: NSObject {
318
374
}
319
375
320
376
#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/
323
377
var command : [ String ] = [ launchPath]
324
378
if let arguments = self . arguments {
325
379
command. append ( contentsOf: arguments)
326
380
}
381
+ command = command. map ( quoteWindowsCommandArg)
327
382
328
383
var siStartupInfo : STARTUPINFOW = STARTUPINFOW ( )
329
384
siStartupInfo. cb = DWORD ( MemoryLayout< STARTUPINFOW> . size)
0 commit comments