Get terminal output after a command swift
NSTask
is class to run another program as a subprocess. You can capture the program's output, error output, exit status and much more.
Expanding on my answer to xcode 6 swift system() command,here is a simple utility function to run a command synchronously,and return the output, error output and exit code (now updated for Swift 2):
func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) { var output : [String] = [] var error : [String] = [] let task = NSTask() task.launchPath = cmd task.arguments = args let outpipe = NSPipe() task.standardOutput = outpipe let errpipe = NSPipe() task.standardError = errpipe task.launch() let outdata = outpipe.fileHandleForReading.readDataToEndOfFile() if var string = String.fromCString(UnsafePointer(outdata.bytes)) { string = string.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet()) output = string.componentsSeparatedByString("\n") } let errdata = errpipe.fileHandleForReading.readDataToEndOfFile() if var string = String.fromCString(UnsafePointer(errdata.bytes)) { string = string.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet()) error = string.componentsSeparatedByString("\n") } task.waitUntilExit() let status = task.terminationStatus return (output, error, status)}
Sample usage:
let (output, error, status) = runCommand("/usr/bin/git", args: "status")print("program exited with status \(status)")if output.count > 0 { print("program output:") print(output)}if error.count > 0 { print("error output:") print(error)}
Or, if you are only interested in the output, but not in the error messages or exit code:
let output = runCommand("/usr/bin/git", args: "status").output
Output and error output are returned as an array of strings, onestring for each line.
The first argument to runCommand()
must be the full path to anexecutable, such as "/usr/bin/git"
. You can start the program using a shell (which is what system()
also does):
let (output, error, status) = runCommand("/bin/sh", args: "-c", "git status")
The advantage is that the "git" executable is automatically foundvia the default search path. The disadvantage is that you have toquote/escape arguments correctly if they contain spaces or othercharacters which have a special meaning in the shell.
Update for Swift 3:
func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) { var output : [String] = [] var error : [String] = [] let task = Process() task.launchPath = cmd task.arguments = args let outpipe = Pipe() task.standardOutput = outpipe let errpipe = Pipe() task.standardError = errpipe task.launch() let outdata = outpipe.fileHandleForReading.readDataToEndOfFile() if var string = String(data: outdata, encoding: .utf8) { string = string.trimmingCharacters(in: .newlines) output = string.components(separatedBy: "\n") } let errdata = errpipe.fileHandleForReading.readDataToEndOfFile() if var string = String(data: errdata, encoding: .utf8) { string = string.trimmingCharacters(in: .newlines) error = string.components(separatedBy: "\n") } task.waitUntilExit() let status = task.terminationStatus return (output, error, status)}
system
spawns a new process so you can’t capture its ouput. The equivalent that gives you a way to do this would be popen
, which you could use like this:
import Darwinlet fp = popen("ping -c 4 localhost", "r")var buf = Array<CChar>(count: 128, repeatedValue: 0)while fgets(&buf, CInt(buf.count), fp) != nil, let str = String.fromCString(buf) { print(str)}fclose(fp)
However, don’t do it this way. Use NSTask
as Martin describes.
edit: based on your request to run multiple commands in parallel, here is some probably-unwise code:
import Darwinlet commands = [ "tail /etc/hosts", "ping -c 2 localhost",]let fps = commands.map { popen($0, "r") }var buf = Array<CChar>(count: 128, repeatedValue: 0)let results: [String] = fps.map { fp in var result = "" while fgets(&buf, CInt(buf.count), fp) != nil, let str = String.fromCString(buf) { result += str } return result}fps.map { fclose($0) }println("\n\n----\n\n".join(map(zip(commands,results)) { "\($0):\n\($1)" }))
(seriously, use NSTask
)
my 2 cents for swift 5.x , macOS with call back, invoked when done.
final func doTaskFor(cmd: String, arguments: [String], callback: CallBackWithStr = nil){let task = Process()let absolutePath = <add your specific path..> let fullCmd = absolutePath+cmd#if DEBUG// used to debug.let debugstr :String = fullCmd + " " + arguments.oneLine()print(debugstr)#endiftask.executableURL = URL(fileURLWithPath: fullCmd)task.arguments = arguments// Create 2 Pipes and make the tasklet outPipe = Pipe()task.standardOutput = outPipelet errPipe = Pipe()task.standardError = errPipetask.terminationHandler = { (process) in print("\ndidFinish: \(!process.isRunning)") // Get the data let outData = outPipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: outData, encoding: .utf8) // print(output!) // Get the error let errData = errPipe.fileHandleForReading.readDataToEndOfFile() let err = String(data: errData, encoding: .utf8) // print(err!) // usually output is empty if error. callback?(output ?? "") }do { try task.run()} catch { let msg = " \(error)" Log(msg: msg, safe: true) print(msg) }
}