How do I run a terminal command in a Swift script? (e.g. xcodebuild)
Solution 1:
If you don't use command outputs in Swift code, following would be sufficient:
#!/usr/bin/env swift
import Foundation
@discardableResult
func shell(_ args: String...) -> Int32 {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
task.launch()
task.waitUntilExit()
return task.terminationStatus
}
shell("ls")
shell("xcodebuild", "-workspace", "myApp.xcworkspace")
Updated: for Swift3/Xcode8
Solution 2:
If you would like to use command line arguments "exactly" as you would in command line (without separating all the arguments), try the following.
(This answer improves off of LegoLess's answer and can be used in Swift 5)
import Foundation
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
// Example usage:
shell("ls -la")
Updated / safer function calls 10/23/21: It's possible to run into a runtime error with the above shell command and if so, try swapping to the updated calls below. You'll need to use a do catch statement around the new shell command but hopefully this saves you some time searching for a way to catch unexpected error(s) too.
Explanation: Since task.launch() isn't a throwing function it cannot be caught and I was finding it to occasionally simply crash the app when called. After much internet searching, I found the Process class has deprecated task.launch() in favor of a newer function task.run() which does throw errors properly w/out crashing the app. To find out more about the updated methods, please see: https://eclecticlight.co/2019/02/02/scripting-in-swift-process-deprecations/
import Foundation
func safeShell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/zsh") //<--updated
do {
try task.run() //<--updated
}
catch{ throw error }
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
// Example usage:
do {
safeShell("ls -la")
}
catch {
print("\(error)") //handle or silence the error here
}
Solution 3:
The problem here is that you cannot mix and match Bash and Swift. You already know how to run Swift script from command line, now you need to add the methods to execute Shell commands in Swift. In summary from PracticalSwift blog:
func shell(_ launchPath: String, _ arguments: [String]) -> String?
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)
return output
}
The following Swift code will execute xcodebuild
with arguments and then output the result.
shell("xcodebuild", ["-workspace", "myApp.xcworkspace"]);
As for searching the directory contents (which is what ls
does in Bash), I suggest using NSFileManager
and scanning the directory directly in Swift, instead of Bash output, which can be a pain to parse.
Solution 4:
Utility function In Swift 3.0
This also returns the tasks termination status and waits for completion.
func shell(launchPath: String, arguments: [String] = []) -> (String? , Int32) {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
task.waitUntilExit()
return (output, task.terminationStatus)
}
Solution 5:
If you'd like to use the bash environment for calling commands use the following bash function which uses a fixed up version of Legoless. I had to remove a trailing newline from the shell function's result.
Swift 3.0:(Xcode8)
import Foundation
func shell(launchPath: String, arguments: [String]) -> String
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)!
if output.characters.count > 0 {
//remove newline character.
let lastIndex = output.index(before: output.endIndex)
return output[output.startIndex ..< lastIndex]
}
return output
}
func bash(command: String, arguments: [String]) -> String {
let whichPathForCommand = shell(launchPath: "/bin/bash", arguments: [ "-l", "-c", "which \(command)" ])
return shell(launchPath: whichPathForCommand, arguments: arguments)
}
For example to get the current working git branch of the current working directory:
let currentBranch = bash("git", arguments: ["describe", "--contains", "--all", "HEAD"])
print("current branch:\(currentBranch)")