Skip to content

Commit 5dac4b0

Browse files
committed
Remove all unsafe APIs
1 parent a410b36 commit 5dac4b0

File tree

2 files changed

+139
-76
lines changed

2 files changed

+139
-76
lines changed

Sources/ShellOut.swift

Lines changed: 101 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -79,201 +79,224 @@ import ShellQuote
7979
errorHandle: FileHandle? = nil,
8080
environment: [String : String]? = nil
8181
) throws -> String {
82-
return try shellOut(
83-
to: command.string,
82+
try shellOut(
83+
to: command.command,
84+
arguments: command.arguments,
8485
at: path,
8586
process: process,
8687
outputHandle: outputHandle,
8788
errorHandle: errorHandle,
88-
environment: environment
89+
environment: environment,
90+
quoteArguments: false
8991
)
9092
}
9193

9294
/// Structure used to pre-define commands for use with ShellOut
9395
public struct ShellOutCommand {
9496
/// 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]
96100

97101
/// 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)
100136
}
101137
}
102138

103139
/// Git commands
104140
public extension ShellOutCommand {
105141
/// Initialize a git repository
106142
static func gitInit() -> ShellOutCommand {
107-
return ShellOutCommand(string: "git init")
143+
return ShellOutCommand(safeCommand: "git", arguments: ["init"])
108144
}
109145

110146
/// Clone a git repository at a given URL
111147
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+
113151
path.map { command.append(argument: $0) }
114152

115153
if quiet {
116-
command.append(" --quiet")
154+
command.append(argument: "--quiet")
117155
}
118156

119-
return ShellOutCommand(string: command)
157+
return command
120158
}
121159

122160
/// Create a git commit with a given message (also adds all untracked file to the index)
123161
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)
125164
command.append(argument: message)
126165

127166
if quiet {
128-
command.append(" --quiet")
167+
command.append(argument: "--quiet")
129168
}
130169

131-
return ShellOutCommand(string: command)
170+
return command
132171
}
133172

134173
/// Perform a git push
135174
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"])
137177
remote.map { command.append(argument: $0) }
138178
branch.map { command.append(argument: $0) }
139179

140180
if quiet {
141-
command.append(" --quiet")
181+
command.append(argument: "--quiet")
142182
}
143183

144-
return ShellOutCommand(string: command)
184+
return command
145185
}
146186

147187
/// Perform a git pull
148188
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"])
150191
remote.map { command.append(argument: $0) }
151192
branch.map { command.append(argument: $0) }
152193

153194
if quiet {
154-
command.append(" --quiet")
195+
command.append(argument: "--quiet")
155196
}
156197

157-
return ShellOutCommand(string: command)
198+
return command
158199
}
159200

160201
/// Run a git submodule update
161202
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)
163205

164206
if initializeIfNeeded {
165-
command.append(" --init")
207+
command.append(argument: "--init")
166208
}
167209

168210
if recursive {
169-
command.append(" --recursive")
211+
command.append(argument: "--recursive")
170212
}
171213

172214
if quiet {
173-
command.append(" --quiet")
215+
command.append(argument: "--quiet")
174216
}
175217

176-
return ShellOutCommand(string: command)
218+
return command
177219
}
178220

179221
/// Checkout a given git branch
180222
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])
182224

183225
if quiet {
184-
command.append(" --quiet")
226+
command.append(argument: "--quiet")
185227
}
186228

187-
return ShellOutCommand(string: command)
229+
return command
188230
}
189231

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+
192237
}
193238
}
194239

195240
/// File system commands
196241
public extension ShellOutCommand {
197242
/// Create a folder with a given name
198243
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])
201245
}
202246

203247
/// Create a file with a given name and contents (will overwrite any existing file with the same name)
204248
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)
211252
}
212253

213254
/// Move a file from one path to another
214255
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])
219257
}
220258

221259
/// Copy a file from one path to another
222260
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])
227262
}
228263

229264
/// Remove a file
230265
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])
235267
}
236268

237269
/// Open a file using its designated application
238270
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])
241272
}
242273

243274
/// Read a file as a string
244275
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])
247277
}
248278

249279
/// Create a symlink at a given path, to a given target
250280
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])
255282
}
256283

257284
/// Expand a symlink at a given path, returning its target path
258285
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])
261287
}
262288
}
263289

264290
/// Marathon commands
265291
public extension ShellOutCommand {
266292
/// Run a Marathon Swift script
267293
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)
272295
}
273296

274297
/// Update all Swift packages managed by Marathon
275298
static func updateMarathonPackages() -> ShellOutCommand {
276-
return ShellOutCommand(string: "marathon update")
299+
.init(safeCommand: "marathon", arguments: ["update"])
277300
}
278301
}
279302

@@ -293,50 +316,54 @@ public extension ShellOutCommand {
293316

294317
/// Create a Swift package with a given type (see SwiftPackageType for options)
295318
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)
298322
}
299323

300324
/// Update all Swift package dependencies
301325
static func updateSwiftPackages() -> ShellOutCommand {
302-
return ShellOutCommand(string: "swift package update")
326+
.init(safeCommand: "swift", arguments: ["package", "update"])
303327
}
304328

305329
/// Generate an Xcode project for a Swift package
306330
static func generateSwiftPackageXcodeProject() -> ShellOutCommand {
307-
return ShellOutCommand(string: "swift package generate-xcodeproj")
331+
.init(safeCommand: "swift", arguments: ["package", "generate-xcodeproj"])
308332
}
309333

310334
/// Build a Swift package using a given configuration (see SwiftBuildConfiguration for options)
311335
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)
313339
}
314340

315341
/// Test a Swift package using a given configuration (see SwiftBuildConfiguration for options)
316342
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)
318346
}
319347
}
320348

321349
/// Fastlane commands
322350
public extension ShellOutCommand {
323351
/// Run Fastlane using a given lane
324352
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])
327354
}
328355
}
329356

330357
/// CocoaPods commands
331358
public extension ShellOutCommand {
332359
/// Update all CocoaPods dependencies
333360
static func updateCocoaPods() -> ShellOutCommand {
334-
return ShellOutCommand(string: "pod update")
361+
.init(safeCommand: "pod", arguments: ["update"])
335362
}
336363

337364
/// Install all CocoaPods dependencies
338365
static func installCocoaPods() -> ShellOutCommand {
339-
return ShellOutCommand(string: "pod install")
366+
.init(safeCommand: "pod", arguments: ["install"])
340367
}
341368
}
342369

0 commit comments

Comments
 (0)