@@ -79,201 +79,224 @@ import ShellQuote
79
79
errorHandle: FileHandle ? = nil ,
80
80
environment: [ String : String ] ? = nil
81
81
) throws -> String {
82
- return try shellOut (
83
- to: command. string,
82
+ try shellOut (
83
+ to: command. command,
84
+ arguments: command. arguments,
84
85
at: path,
85
86
process: process,
86
87
outputHandle: outputHandle,
87
88
errorHandle: errorHandle,
88
- environment: environment
89
+ environment: environment,
90
+ quoteArguments: false
89
91
)
90
92
}
91
93
92
94
/// Structure used to pre-define commands for use with ShellOut
93
95
public struct ShellOutCommand {
94
96
/// The string that makes up the command that should be run on the command line
95
- public var string : String
97
+ public var command : String
98
+
99
+ public var arguments : [ String ]
96
100
97
101
/// Initialize a value using a string that makes up the underlying command
98
- public init ( string: String ) {
99
- self . string = string
102
+ public init ( command: String , arguments: [ String ] = [ ] , quoteArguments: Bool = true ) throws {
103
+ guard !ShellQuote. hasUnsafeContent ( command) else {
104
+ throw ShellOutCommand . Error ( message: " Command must not contain characters that require quoting, was: \( command) " )
105
+ }
106
+
107
+ self . command = command
108
+ self . arguments = quoteArguments ? arguments. map ( ShellQuote . quote) : arguments
109
+ }
110
+
111
+ public init ( safeCommand: String , arguments: [ String ] = [ ] , quoteArguments: Bool = true ) {
112
+ self . command = safeCommand
113
+ self . arguments = quoteArguments ? arguments. map ( ShellQuote . quote) : arguments
114
+ }
115
+
116
+ var string : String { ( [ command] + arguments) . joined ( separator: " " ) }
117
+
118
+ func appending( arguments: [ String ] , quoteArguments: Bool = true ) -> Self {
119
+ . init(
120
+ safeCommand: self . command,
121
+ arguments: self . arguments + ( quoteArguments ? arguments. map ( ShellQuote . quote) : arguments) ,
122
+ quoteArguments: false
123
+ )
124
+ }
125
+
126
+ func appending( argument: String , quoteArguments: Bool = true ) -> Self {
127
+ appending ( arguments: [ argument] , quoteArguments: quoteArguments)
128
+ }
129
+
130
+ mutating func append( arguments: [ String ] , quoteArguments: Bool = true ) {
131
+ self . arguments = self . arguments + ( quoteArguments ? arguments. map ( ShellQuote . quote) : arguments)
132
+ }
133
+
134
+ mutating func append( argument: String , quoteArguments: Bool = true ) {
135
+ append ( arguments: [ argument] , quoteArguments: quoteArguments)
100
136
}
101
137
}
102
138
103
139
/// Git commands
104
140
public extension ShellOutCommand {
105
141
/// Initialize a git repository
106
142
static func gitInit( ) -> ShellOutCommand {
107
- return ShellOutCommand ( string : " git init " )
143
+ return ShellOutCommand ( safeCommand : " git " , arguments : [ " init " ] )
108
144
}
109
145
110
146
/// Clone a git repository at a given URL
111
147
static func gitClone( url: URL , to path: String ? = nil , allowingPrompt: Bool = true , quiet: Bool = true ) -> ShellOutCommand {
112
- var command = " \( git ( allowingPrompt: allowingPrompt) ) clone \( url. absoluteString) "
148
+ var command = git ( allowingPrompt: allowingPrompt)
149
+ . appending ( arguments: [ " clone " , url. absoluteString] )
150
+
113
151
path. map { command. append ( argument: $0) }
114
152
115
153
if quiet {
116
- command. append ( " --quiet" )
154
+ command. append ( argument : " --quiet " )
117
155
}
118
156
119
- return ShellOutCommand ( string : command)
157
+ return command
120
158
}
121
159
122
160
/// Create a git commit with a given message (also adds all untracked file to the index)
123
161
static func gitCommit( message: String , allowingPrompt: Bool = true , quiet: Bool = true ) -> ShellOutCommand {
124
- var command = " \( git ( allowingPrompt: allowingPrompt) ) add . && git commit -a -m "
162
+ var command = git ( allowingPrompt: allowingPrompt)
163
+ . appending ( arguments: [ " add . && git commit -a -m " ] , quoteArguments: false )
125
164
command. append ( argument: message)
126
165
127
166
if quiet {
128
- command. append ( " --quiet" )
167
+ command. append ( argument : " --quiet " )
129
168
}
130
169
131
- return ShellOutCommand ( string : command)
170
+ return command
132
171
}
133
172
134
173
/// Perform a git push
135
174
static func gitPush( remote: String ? = nil , branch: String ? = nil , allowingPrompt: Bool = true , quiet: Bool = true ) -> ShellOutCommand {
136
- var command = " \( git ( allowingPrompt: allowingPrompt) ) push "
175
+ var command = git ( allowingPrompt: allowingPrompt)
176
+ . appending ( arguments: [ " push " ] )
137
177
remote. map { command. append ( argument: $0) }
138
178
branch. map { command. append ( argument: $0) }
139
179
140
180
if quiet {
141
- command. append ( " --quiet" )
181
+ command. append ( argument : " --quiet " )
142
182
}
143
183
144
- return ShellOutCommand ( string : command)
184
+ return command
145
185
}
146
186
147
187
/// Perform a git pull
148
188
static func gitPull( remote: String ? = nil , branch: String ? = nil , allowingPrompt: Bool = true , quiet: Bool = true ) -> ShellOutCommand {
149
- var command = " \( git ( allowingPrompt: allowingPrompt) ) pull "
189
+ var command = git ( allowingPrompt: allowingPrompt)
190
+ . appending ( arguments: [ " pull " ] )
150
191
remote. map { command. append ( argument: $0) }
151
192
branch. map { command. append ( argument: $0) }
152
193
153
194
if quiet {
154
- command. append ( " --quiet" )
195
+ command. append ( argument : " --quiet " )
155
196
}
156
197
157
- return ShellOutCommand ( string : command)
198
+ return command
158
199
}
159
200
160
201
/// Run a git submodule update
161
202
static func gitSubmoduleUpdate( initializeIfNeeded: Bool = true , recursive: Bool = true , allowingPrompt: Bool = true , quiet: Bool = true ) -> ShellOutCommand {
162
- var command = " \( git ( allowingPrompt: allowingPrompt) ) submodule update "
203
+ var command = git ( allowingPrompt: allowingPrompt)
204
+ . appending ( arguments: [ " submodule update " ] , quoteArguments: false )
163
205
164
206
if initializeIfNeeded {
165
- command. append ( " --init" )
207
+ command. append ( argument : " --init " )
166
208
}
167
209
168
210
if recursive {
169
- command. append ( " --recursive" )
211
+ command. append ( argument : " --recursive " )
170
212
}
171
213
172
214
if quiet {
173
- command. append ( " --quiet" )
215
+ command. append ( argument : " --quiet " )
174
216
}
175
217
176
- return ShellOutCommand ( string : command)
218
+ return command
177
219
}
178
220
179
221
/// Checkout a given git branch
180
222
static func gitCheckout( branch: String , quiet: Bool = true ) -> ShellOutCommand {
181
- var command = " git checkout " . appending ( argument : branch)
223
+ var command = ShellOutCommand ( safeCommand : " git " , arguments : [ " checkout " , branch] )
182
224
183
225
if quiet {
184
- command. append ( " --quiet" )
226
+ command. append ( argument : " --quiet " )
185
227
}
186
228
187
- return ShellOutCommand ( string : command)
229
+ return command
188
230
}
189
231
190
- private static func git( allowingPrompt: Bool ) -> String {
191
- return allowingPrompt ? " git " : " env GIT_TERMINAL_PROMPT=0 git "
232
+ private static func git( allowingPrompt: Bool ) -> Self {
233
+ allowingPrompt
234
+ ? . init( safeCommand: " git " )
235
+ : . init( safeCommand: " env " , arguments: [ " GIT_TERMINAL_PROMPT=0 " , " git " ] )
236
+
192
237
}
193
238
}
194
239
195
240
/// File system commands
196
241
public extension ShellOutCommand {
197
242
/// Create a folder with a given name
198
243
static func createFolder( named name: String ) -> ShellOutCommand {
199
- let command = " mkdir " . appending ( argument: name)
200
- return ShellOutCommand ( string: command)
244
+ . init( safeCommand: " mkdir " , arguments: [ name] )
201
245
}
202
246
203
247
/// Create a file with a given name and contents (will overwrite any existing file with the same name)
204
248
static func createFile( named name: String , contents: String ) -> ShellOutCommand {
205
- var command = " echo "
206
- command. append ( argument: contents)
207
- command. append ( " > " )
208
- command. append ( argument: name)
209
-
210
- return ShellOutCommand ( string: command)
249
+ . init( safeCommand: " echo " , arguments: [ contents] )
250
+ . appending ( argument: " > " , quoteArguments: false )
251
+ . appending ( argument: name)
211
252
}
212
253
213
254
/// Move a file from one path to another
214
255
static func moveFile( from originPath: String , to targetPath: String ) -> ShellOutCommand {
215
- let command = " mv " . appending ( argument: originPath)
216
- . appending ( argument: targetPath)
217
-
218
- return ShellOutCommand ( string: command)
256
+ . init( safeCommand: " mv " , arguments: [ originPath, targetPath] )
219
257
}
220
258
221
259
/// Copy a file from one path to another
222
260
static func copyFile( from originPath: String , to targetPath: String ) -> ShellOutCommand {
223
- let command = " cp " . appending ( argument: originPath)
224
- . appending ( argument: targetPath)
225
-
226
- return ShellOutCommand ( string: command)
261
+ . init( safeCommand: " cp " , arguments: [ originPath, targetPath] )
227
262
}
228
263
229
264
/// Remove a file
230
265
static func removeFile( from path: String , arguments: [ String ] = [ " -f " ] ) -> ShellOutCommand {
231
- let command = " rm " . appending ( arguments: arguments)
232
- . appending ( argument: path)
233
-
234
- return ShellOutCommand ( string: command)
266
+ . init( safeCommand: " rm " , arguments: arguments + [ path] )
235
267
}
236
268
237
269
/// Open a file using its designated application
238
270
static func openFile( at path: String ) -> ShellOutCommand {
239
- let command = " open " . appending ( argument: path)
240
- return ShellOutCommand ( string: command)
271
+ . init( safeCommand: " open " , arguments: [ path] )
241
272
}
242
273
243
274
/// Read a file as a string
244
275
static func readFile( at path: String ) -> ShellOutCommand {
245
- let command = " cat " . appending ( argument: path)
246
- return ShellOutCommand ( string: command)
276
+ . init( safeCommand: " cat " , arguments: [ path] )
247
277
}
248
278
249
279
/// Create a symlink at a given path, to a given target
250
280
static func createSymlink( to targetPath: String , at linkPath: String ) -> ShellOutCommand {
251
- let command = " ln -s " . appending ( argument: targetPath)
252
- . appending ( argument: linkPath)
253
-
254
- return ShellOutCommand ( string: command)
281
+ . init( safeCommand: " ln " , arguments: [ " -s " , targetPath, linkPath] )
255
282
}
256
283
257
284
/// Expand a symlink at a given path, returning its target path
258
285
static func expandSymlink( at path: String ) -> ShellOutCommand {
259
- let command = " readlink " . appending ( argument: path)
260
- return ShellOutCommand ( string: command)
286
+ . init( safeCommand: " readlink " , arguments: [ path] )
261
287
}
262
288
}
263
289
264
290
/// Marathon commands
265
291
public extension ShellOutCommand {
266
292
/// Run a Marathon Swift script
267
293
static func runMarathonScript( at path: String , arguments: [ String ] = [ ] ) -> ShellOutCommand {
268
- let command = " marathon run " . appending ( argument: path)
269
- . appending ( arguments: arguments)
270
-
271
- return ShellOutCommand ( string: command)
294
+ . init( safeCommand: " marathon " , arguments: [ " run " , path] + arguments)
272
295
}
273
296
274
297
/// Update all Swift packages managed by Marathon
275
298
static func updateMarathonPackages( ) -> ShellOutCommand {
276
- return ShellOutCommand ( string : " marathon update " )
299
+ . init ( safeCommand : " marathon " , arguments : [ " update " ] )
277
300
}
278
301
}
279
302
@@ -293,50 +316,54 @@ public extension ShellOutCommand {
293
316
294
317
/// Create a Swift package with a given type (see SwiftPackageType for options)
295
318
static func createSwiftPackage( withType type: SwiftPackageType = . library) -> ShellOutCommand {
296
- let command = " swift package init --type \( type. rawValue) "
297
- return ShellOutCommand ( string: command)
319
+ . init( safeCommand: " swift " ,
320
+ arguments: [ " package init --type \( type) " ] ,
321
+ quoteArguments: false )
298
322
}
299
323
300
324
/// Update all Swift package dependencies
301
325
static func updateSwiftPackages( ) -> ShellOutCommand {
302
- return ShellOutCommand ( string : " swift package update " )
326
+ . init ( safeCommand : " swift " , arguments : [ " package " , " update " ] )
303
327
}
304
328
305
329
/// Generate an Xcode project for a Swift package
306
330
static func generateSwiftPackageXcodeProject( ) -> ShellOutCommand {
307
- return ShellOutCommand ( string : " swift package generate-xcodeproj " )
331
+ . init ( safeCommand : " swift " , arguments : [ " package " , " generate-xcodeproj " ] )
308
332
}
309
333
310
334
/// Build a Swift package using a given configuration (see SwiftBuildConfiguration for options)
311
335
static func buildSwiftPackage( withConfiguration configuration: SwiftBuildConfiguration = . debug) -> ShellOutCommand {
312
- return ShellOutCommand ( string: " swift build -c \( configuration. rawValue) " )
336
+ . init( safeCommand: " swift " ,
337
+ arguments: [ " build -c \( configuration) " ] ,
338
+ quoteArguments: false )
313
339
}
314
340
315
341
/// Test a Swift package using a given configuration (see SwiftBuildConfiguration for options)
316
342
static func testSwiftPackage( withConfiguration configuration: SwiftBuildConfiguration = . debug) -> ShellOutCommand {
317
- return ShellOutCommand ( string: " swift test -c \( configuration. rawValue) " )
343
+ . init( safeCommand: " swift " ,
344
+ arguments: [ " test -c \( configuration) " ] ,
345
+ quoteArguments: false )
318
346
}
319
347
}
320
348
321
349
/// Fastlane commands
322
350
public extension ShellOutCommand {
323
351
/// Run Fastlane using a given lane
324
352
static func runFastlane( usingLane lane: String ) -> ShellOutCommand {
325
- let command = " fastlane " . appending ( argument: lane)
326
- return ShellOutCommand ( string: command)
353
+ . init( safeCommand: " fastlane " , arguments: [ lane] )
327
354
}
328
355
}
329
356
330
357
/// CocoaPods commands
331
358
public extension ShellOutCommand {
332
359
/// Update all CocoaPods dependencies
333
360
static func updateCocoaPods( ) -> ShellOutCommand {
334
- return ShellOutCommand ( string : " pod update " )
361
+ . init ( safeCommand : " pod " , arguments : [ " update " ] )
335
362
}
336
363
337
364
/// Install all CocoaPods dependencies
338
365
static func installCocoaPods( ) -> ShellOutCommand {
339
- return ShellOutCommand ( string : " pod install " )
366
+ . init ( safeCommand : " pod " , arguments : [ " install " ] )
340
367
}
341
368
}
342
369
0 commit comments