@@ -102,6 +102,67 @@ private func processIsEqual(_ a : UnsafeRawPointer?, _ b : UnsafeRawPointer?) ->
102
102
return true
103
103
}
104
104
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
+
105
166
open class Process : NSObject {
106
167
private static func setup( ) {
107
168
struct Once {
@@ -318,8 +379,6 @@ open class Process: NSObject {
318
379
}
319
380
320
381
#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
382
var command : [ String ] = [ launchPath]
324
383
if let arguments = self . arguments {
325
384
command. append ( contentsOf: arguments)
@@ -412,7 +471,7 @@ open class Process: NSObject {
412
471
CFSocketCreateRunLoopSource ( kCFAllocatorDefault, socket, 0 )
413
472
CFRunLoopAddSource ( managerThreadRunLoop? . _cfRunLoop, source, kCFRunLoopDefaultMode)
414
473
415
- try command . joined ( separator : " " ) . withCString ( encodedAs: UTF16 . self) { wszCommandLine in
474
+ try quoteWindowsCommandLine ( command ) . withCString ( encodedAs: UTF16 . self) { wszCommandLine in
416
475
try currentDirectoryURL. path. withCString ( encodedAs: UTF16 . self) { wszCurrentDirectory in
417
476
try szEnvironment. withCString ( encodedAs: UTF16 . self) { wszEnvironment in
418
477
if CreateProcessW ( nil , UnsafeMutablePointer < WCHAR > ( mutating: wszCommandLine) ,
0 commit comments