163

I want to replace my CI bash scripts with swift. I can't figure out how to invoke normal terminal command such as ls or xcodebuild

#!/usr/bin/env xcrun swift import Foundation // Works println("Test") // Works ls // Fails xcodebuild -workspace myApp.xcworkspace // Fails 

$ ./script.swift ./script.swift:5:1: error: use of unresolved identifier 'ls' ls // Fails ^ ... etc .... 

18 Answers 18

227

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.standardInput = nil 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 @discardableResult // Add to suppress warnings when you don't want/need a result 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 task.standardInput = nil try task.run() //<--updated let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)! return output } 

Examples:

// Example usage capturing error: do { try safeShell("ls -la") } catch { print("\(error)") //handle or silence the error here } // Example usage where you don't care about the error and want a nil back instead let result = try? safeShell("ls -la") // Example usage where you don't care about the error or the return value try? safeShell("ls -la") 

Note: For the last case where you are using try? and aren't using the result, for some reason the compiler still warns you even though it's marked as @discardableResult. This only happens with try?, not try within a do-try-catch block or from within a throwing function. Either way, you can safely ignore it.

Sign up to request clarification or add additional context in comments.

8 Comments

This answer should really be much higher up as it solves many of the issues of the previous ones.
+1. It should be noted for osx users that /bin/bash refers to bash-3.2. If you want to use bash's more advanced features, change the path (/usr/bin/env bash usually is a good alternative)
Anyone can help with this? Arguments don't pass stackoverflow.com/questions/62203978/…
Great snippet! One minor note: since the function already throws, you don't need the do .. try .. catch block, just use try task.run()
You should also add task.standardInput = nil before task.run() is called. This can be painfully hard to diagnose, since the kernel will silently stop the child process if it attempts to read from stdin. This can be subtle because the child can be doing useful work while monitoring stdin for input. The moment you type something on the keyboard, the child sees that there's input waiting and calls read(). The kernel responds by sending a SIGSTOP and the child freezes. I'm. kind of surprised that this isn't the default for Process().
|
161

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

13 Comments

'NSTask' has been renamed to 'Process'
Is Process() still in Swift 4? I'm getting an undefined symbol. :/
@ArnaldoCapo It still works fine for me! Here's an example: #!/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")
I tried that I got : I tried that I got : i.imgur.com/Ge1OOCG.png
Process is available on macOS only
|
40

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.

6 Comments

Great - I made a few edits to make this compile, however I am getting a runtime exception when trying to invoke shell("ls", []) - 'NSInvalidArgumentException', reason: 'launch path not accessible' Any ideas?
NSTask does not search the executable (using your PATH from the environment) as the shell does. The launch path must be an absolute path (e.g. "/bin/ls") or a path relative to the current working directory.
stackoverflow.com/questions/386783/… PATH is basically a shell's concept and not reachable.
Great - it works now. I posted the full script + a few modifications for completeness. Thank you.
Using shell("cd", "~/Desktop/") , I get: /usr/bin/cd: line 4: cd: ~/Desktop/: No such file or directory
|
24

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) } 

2 Comments

import Foundation missing
Sadly, not for iOS.
18

Just to update this since Apple has deprecated both .launchPath and launch(), here's an updated utility function for Swift 4 that should be a little more future proof.

Note: Apple's documentation on the replacements (run(), executableURL, etc) are basically empty at this point.

import Foundation // wrapper function for shell commands // must provide full path to executable func shell(_ launchPath: String, _ arguments: [String] = []) -> (String?, Int32) { let task = Process() task.executableURL = URL(fileURLWithPath: launchPath) task.arguments = arguments let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe do { try task.run() } catch { // handle errors print("Error: \(error.localizedDescription)") } let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) task.waitUntilExit() return (output, task.terminationStatus) } // valid directory listing test let (goodOutput, goodStatus) = shell("/bin/ls", ["-la"]) if let out = goodOutput { print("\(out)") } print("Returned \(goodStatus)\n") // invalid test let (badOutput, badStatus) = shell("ls") 

Should be able to paste this directly into a playground to see it in action.

1 Comment

Works OK in a Swift 5 playground provided the Playground Settings Platform is "macOS". The "iOS" playground platform gets a "Cannot find 'Process' in scope" error message.
16

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)") 

Comments

14

Full script based on Legoless's answer

#!/usr/bin/env swift import Foundation func printShell(launchPath: String, arguments: [String] = []) { let output = shell(launchPath: launchPath, arguments: arguments) if (output != nil) { print(output!) } } 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 } // > ls // > ls -a -g printShell(launchPath: "/bin/ls") printShell(launchPath: "/bin/ls", arguments:["-a", "-g"]) 

Comments

8

Updating for Swift 4.0 (dealing with changes to String)

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.count > 0 { //remove newline character. let lastIndex = output.index(before: output.endIndex) return String(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) } 

1 Comment

give the example
8

After trying some of the solutions posted here, I found that the best way to execute commands was using the -c flag for the arguments.

@discardableResult func shell(_ command: String) -> (String?, Int32) { let task = Process() task.launchPath = "/bin/bash" task.arguments = ["-c", command] 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) } let _ = shell("mkdir ~/Desktop/test") 

Comments

4
import Foundation enum Commands { struct Result { public let statusCode: Int32 public let output: String } static func run(_ command: String, environment: [String: String]? = nil, executableURL: String = "/bin/bash", dashc: String = "-c") -> Result { // create process func create(_ executableURL: String, dashc: String, environment: [String: String]?) -> Process { let process = Process() if #available(macOS 10.13, *) { process.executableURL = URL(fileURLWithPath: executableURL) } else { process.launchPath = "/bin/bash" } if let environment = environment { process.environment = environment } process.arguments = [dashc, command] return process } // run process func run(_ process: Process) throws { if #available(macOS 10.13, *) { try process.run() } else { process.launch() } process.waitUntilExit() } // read data func fileHandleData(fileHandle: FileHandle) throws -> String? { var outputData: Data? if #available(macOS 10.15.4, *) { outputData = try fileHandle.readToEnd() } else { outputData = fileHandle.readDataToEndOfFile() } if let outputData = outputData { return String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) } return nil } let process = create(executableURL, dashc: dashc, environment: environment) let outputPipe = Pipe() process.standardOutput = outputPipe let errorPipe = Pipe() process.standardError = errorPipe do { try run(process) let outputActual = try fileHandleData(fileHandle: outputPipe.fileHandleForReading) ?? "" let errorActual = try fileHandleData(fileHandle: errorPipe.fileHandleForReading) ?? "" if process.terminationStatus == EXIT_SUCCESS { return Result(statusCode: process.terminationStatus, output: outputActual) } return Result(statusCode: process.terminationStatus, output: errorActual) } catch let error { return Result(statusCode: process.terminationStatus, output: error.localizedDescription) } } } 

Usage

let result = Commands.run("ls") debugPrint(result.output) debugPrint(result.statusCode) 

or using swift-commands

import Commands Commands.Bash.system("ls") 

Comments

2

For Swift v.5+:

Does not contain any "deprecated" phrases at the time of writing. Does not crash if an unknown command is called.

extension Process { static func shell(path: String = "/bin/zsh", args:[String] = []) -> (Int32, String?, String?) { let task = Process() let pipeOut = Pipe() let pipeErr = Pipe() task.standardInput = nil task.standardOutput = pipeOut task.standardError = pipeErr task.executableURL = URL(fileURLWithPath: path) task.arguments = args do { try task.run() task.waitUntilExit() let dataOut = pipeOut.fileHandleForReading.readDataToEndOfFile() let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile() return ( task.terminationStatus, dataOut.isEmpty ? nil : String(data: dataOut, encoding: .utf8), dataErr.isEmpty ? nil : String(data: dataErr, encoding: .utf8) ) } catch { return (0, nil, nil) } } } 

Example for call NodeJS script:

do { let (status, output, error) = Process.shell( path: "/usr/local/bin/node", args: ["./script.js"] ) dump(status) dump(output) dump(error) } 

Example for call Python script:

do { let (status, output, error) = Process.shell( path: "/usr/local/bin/python3", args: ["./script.py"] ) dump(status) dump(output) dump(error) } 

Example for call PHP script:

do { let (status, output, error) = Process.shell( path: "/usr/local/bin/php", args: ["./script.php"] ) dump(status) dump(output) dump(error) } 

Example for call Bash command:

do { let (status, output, error) = Process.shell( path: "/bin/bash", args: ["-c", "command"] ) dump(status) dump(output) dump(error) } do { let (status, output, error) = Process.shell( path: "/usr/bin/env", args: ["bash", "-c", "command"] ) dump(status) dump(output) dump(error) } 

Research of all use cases

All of the following options work fine directly via Bash, but calling them via Swift results in the following:

Shell Command (bash → command):

command /bin/bash -c command /usr/bin/env bash -c command 

Call script via Bash (bash → script|shebang(interpreter)|chmod+x):

./script -> PY: import fail | PHP: No such file | JS: No such file /bin/bash -c ./script -> PY: import fail | PHP: No such file | JS: No such file /usr/bin/env bash -c ./script -> PY: import fail | PHP: No such file | JS: No such file 

Call script directly (interpreter → script):

interpreter ./script -> PY: nil | PHP: nil | JS: nil /usr/local/bin/interpreter ./script -> PY: OK | PHP: OK | JS: OK /usr/bin/env interpreter ./script -> PY: import fail | PHP: No such file | JS: No such file 

Comments

0

Mixing rintaro and Legoless's answers for Swift 3

@discardableResult func shell(_ args: String...) -> String { let task = Process() task.launchPath = "/usr/bin/env" task.arguments = args let pipe = Pipe() task.standardOutput = pipe task.launch() task.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() guard let output: String = String(data: data, encoding: .utf8) else { return "" } return output } 

Comments

0

Small improvement with the support for env variables:

func shell(launchPath: String, arguments: [String] = [], environment: [String : String]? = nil) -> (String , Int32) { let task = Process() task.launchPath = launchPath task.arguments = arguments if let environment = environment { task.environment = environment } 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) } 

Comments

0

Example of using Process class to run a Python script.

Also:

 - added basic exception handling - setting environment variables (in my case I had to do it to get Google SDK to authenticate correctly) - arguments import Cocoa func shellTask(_ url: URL, arguments:[String], environment:[String : String]) throws ->(String?, String?){ let task = Process() task.executableURL = url task.arguments = arguments task.environment = environment let outputPipe = Pipe() let errorPipe = Pipe() task.standardOutput = outputPipe task.standardError = errorPipe try task.run() let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() let output = String(decoding: outputData, as: UTF8.self) let error = String(decoding: errorData, as: UTF8.self) return (output,error) } func pythonUploadTask() { let url = URL(fileURLWithPath: "/usr/bin/python") let pythonScript = "upload.py" let fileToUpload = "/CuteCat.mp4" let arguments = [pythonScript,fileToUpload] var environment = ProcessInfo.processInfo.environment environment["PATH"]="usr/local/bin" environment["GOOGLE_APPLICATION_CREDENTIALS"] = "/Users/j.chudzynski/GoogleCredentials/credentials.json" do { let result = try shellTask(url, arguments: arguments, environment: environment) if let output = result.0 { print(output) } if let output = result.1 { print(output) } } catch { print("Unexpected error:\(error)") } } 

1 Comment

where do you place the file "upload.py'
0

I've built SwiftExec, a small library for running such commands:

import SwiftExec var result: ExecResult do { result = try exec(program: "/usr/bin/git", arguments: ["status"]) } catch { let error = error as! ExecError result = error.execResult } print(result.exitCode!) print(result.stdout!) print(result.stderr!) 

It's a single-file library which can easily be copy-pasted into projects or installed using SPM. It's tested and simplifies error handling.

There's also ShellOut, which additionally supports a variety of pre-defined commands.

Comments

0

I saw many apps running a terminal command like:

cd /Applications/Theirappname.app/Contents/Resources && do sth here

This command is not different from running a shell script and if the app is not in Applications folder, it won't be executed correctly because this error will occur: No such file or directory: /Applications/Theirappname.app. Therefore, if you want to run an executable file in your Resources folder, you should use this code:

func runExec() -> Int32 { let task = Process() task.arguments = [Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists/")!.path] //If it does not have an extension then you just leave it empty //You can remove subdirectory if it does not exist task.launch() task.waitUntilExit() return task.terminationStatus } 

If your executable file requires an/some argument(s), the code will look like this:

func runExec() -> Int32 { let task = Process() task.launchPath = "/bin/bash" task.launchPath = Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists")?.path //If it does not have an extension then you just leave it empty //You can remove subdirectory if it does not exist task.arguments = ["arg1","arg2"] task.launch() task.waitUntilExit() return task.terminationStatus } 

Comments

0

I'm in the process of re-factoring some existing Objective-C code that used NSTask to Swift, and one key thing missing in other answers is how you should be handling large quantities of stdout/stderr output. Failure to do this seems to result in hangs in the launched process.

One of the commands I commonly launch can produce hundreds of KB of output to both stdout and stderr.

To deal with this, I buffer the output thusly:

 import Foundation struct ShellScriptExecutor { static func runScript(_ script: ShellScript) -> ShellScriptResult { var errors: String = "" let tempFile = copyToTempFile(script) let process = Process() let stdout = Pipe() let stderr = Pipe() var stdoutData = Data.init(capacity: 8192) var stderrData = Data.init(capacity: 8192) process.standardOutput = stdout process.standardError = stderr process.executableURL = URL(fileURLWithPath: "/bin/zsh") process.arguments = [tempFile] do { try process.run() // Buffer the data while running while process.isRunning { stdoutData.append(pipeToData(stdout)) stderrData.append(pipeToData(stderr)) } process.waitUntilExit() stdoutData.append(pipeToData(stdout)) errors = dataToString(stderrData) + pipeToString(stderr) } catch { print("Process failed for " + tempFile + ": " + error.localizedDescription) } // Clean up if !tempFile.isEmpty { do { try FileManager.default.removeItem(atPath: tempFile) } catch { print("Unable to remove " + tempFile + ": " + error.localizedDescription) } } return ShellScriptResult(stdoutData, script.resultType, errors) } static private func copyToTempFile(_ script: ShellScript) -> String { let tempFile: String = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString + ".sh", isDirectory: false).path if FileManager.default.createFile(atPath: tempFile, contents: Data(script.script.utf8), attributes: nil) { return tempFile; } else { return "" } } static private func pipeToString(_ pipe: Pipe) -> String { return dataToString(pipeToData(pipe)) } static private func dataToString(_ data: Data) -> String { return String(decoding: data, as: UTF8.self) } static private func pipeToData(_ pipe: Pipe) -> Data { return pipe.fileHandleForReading.readDataToEndOfFile() } } 

ShellScript and ShellScriptResult are just simple wrapper classes, e.g.

import Foundation struct ShellScript { var script: String { return _script } var resultType: String { return _resultType } private var _script: String private var _resultType: String init(_ script: String, _ resultType: String) { _script = script _resultType = resultType } } 
import Foundation struct ShellScriptResult { var resultType: String { return _resultType } var output: Data { return _output } var errors: String { return _errors } var status: Int32 { return _status; } private var _resultType: String private var _output: Data private var _errors: String private var _status: Int32 init(_ output: Data, _ resultType: String, _ errors: String, _ status: Int32) { _output = output _resultType = resultType _errors = errors _status = status } } 

2 Comments

The best answer so far...but...where are the "simple wrapper classes"? Specifically for ShellScript which has "resultType" property but no indication what it is supposed to be. Enumeration of some sort?
@user1318024, as requested.
0
func swiftSyncFileExec() -> String { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/swift") process.arguments = ["/yourpath/path/swift.swift"] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe try! process.run() let data = try! pipe.fileHandleForReading.readToEnd() let outputString = String(data: data!, encoding: .utf8)! return outputString } 

Usage:

runSwiftShellWithArgs() 

3 Comments

Suggest in your catch you return a negative value to show failure: return (-1, nil, nil), 0 means success no?
@DavidH Hi, David. I didn't test all cases.
It's not testing - a value of zero on a shell command means success. A negative value implies failure. I just used your code successfully to build a nice command file, but I need to know for sure if a command succeeds (returns 0) or fails.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.