19

I run some commands in terminal with this code:

system("the command here")

And after I want to know what is the result of running this command, e.g. if I run

system("git status")

I want to read the actual information about changes in my repo. Is there any way to do that in swift?

pomo_mondreganto
  • 2,028
  • 2
  • 28
  • 56

4 Answers4

38

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, one string for each line.

The first argument to runCommand() must be the full path to an executable, 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 found via the default search path. The disadvantage is that you have to quote/escape arguments correctly if they contain spaces or other characters 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)
}
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 1
    I still can't run multiple programs in one session. For example, if the output for `status` doesn't contain words "nothing" and "clear" (some changes were made) I need to run `git add .` and then `git commit` and `git push origin master` – pomo_mondreganto Apr 08 '15 at 16:18
  • @NikitinRoman: What do you mean with "one session"? Can't you run `runCommand("/usr/bin/git", "status")` first and then `runCommand("/usr/bin/git", "add", ".")` and finally `runCommand("/usr/bin/git", "push", "origin", "master")` ? – Martin R Apr 08 '15 at 16:21
  • I firstly I need to run `cd Project` and after it one by one all this commands, without changing directory – pomo_mondreganto Apr 08 '15 at 16:24
  • 2
    @NikitinRoman: Now I see what you mean. NSTask has a currentDirectoryPath property, that can be set to the directory where the command should be executed. I'll update the code ... – Martin R Apr 08 '15 at 16:27
  • @NikitinRoman: You are welcome! – Note that "cd" is *shell built-in*. If you type "cd project" in the shell, no external command is executed, the shell just remembers the new working directory and uses it as current working directory for the next commands. – Martin R Apr 08 '15 at 16:32
  • Something strange started to happen.. every time I run the program, it shows signal SIGABRT on line `task.launch()` and shows this error: `2015-04-09 18:36:50.224 Commit changes[34595:9180071] An uncaught exception was raised 2015-04-09 18:36:50.224 Commit changes[34595:9180071] launch path not accessible` – pomo_mondreganto Apr 09 '15 at 15:39
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/74873/discussion-between-martin-r-and-nikitin-roman). – Martin R Apr 09 '15 at 19:41
  • Updated for Swift 2: 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) } – UKDataGeek Apr 28 '16 at 16:50
  • Can anyone update the UnsafePointer usage to Swift 3? – Narwhal Oct 25 '16 at 22:59
  • @Narwhal: I did not have the time to update it before now. – Martin R Oct 26 '16 at 12:26
  • @Michal: Thanks for suggesting an improvement. The above Swift 3 does still work with Swift 5 (I just tested it). You are welcome to post your suggested code as an answer. – Martin R Aug 23 '20 at 12:34
  • If I want to get the asni code through NSTask, can you tell me how to do it? I want to render the colors myself.@MartinR – Karim Aug 10 '23 at 09:28
3

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 Darwin

let 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 Darwin

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

Community
  • 1
  • 1
Airspeed Velocity
  • 40,491
  • 8
  • 113
  • 118
  • The second argument to `fgets()` should be the actual buffer size :) – One reason that I avoid `popen()` if possible is that you have to quote/escape the arguments correctly if they contain spaces or any special she'll characters, that can get ugly. – Martin R Apr 08 '15 at 13:14
  • 1
    D’oh! You’re right of course, I mostly post this because I’m amazed the C interop is so smooth. – Airspeed Velocity Apr 08 '15 at 13:15
  • 1
    is there any way to run multiple commands one by one and get output for each of them? – pomo_mondreganto Apr 08 '15 at 15:01
  • @Nikitin Roman: Something like this: http://stackoverflow.com/questions/9400287/how-to-run-nstask-with-multiple-commands An alternative is to call .sh file that has your multiple commands. – Sentry.co Dec 15 '16 at 17:55
0

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)

#endif

task.executableURL = URL(fileURLWithPath: fullCmd)
task.arguments = arguments

// Create 2 Pipes and make the task
let outPipe = Pipe()
task.standardOutput = outPipe

let errPipe = Pipe()
task.standardError = errPipe

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

}

ingconti
  • 10,876
  • 3
  • 61
  • 48
0

Swift 5

Tu run your commands and get its outputs you can use next simple extention of Process class:

extension String : LocalizedError {
    public var errorDescription: String? { self }
}

extension Process {
    
    func run(_ executableURL: URL, arguments: [String]? = nil) throws -> String {
        self.executableURL = executableURL
        self.arguments = arguments
        
        let pipe = Pipe()
        standardOutput = pipe
        standardError = pipe
        
        try run()
        waitUntilExit()
        
        guard terminationStatus == EXIT_SUCCESS else {
            let error = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
            throw (error?.trimmingCharacters(in: .newlines) ?? "")
        }
        
        let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
        return output?.trimmingCharacters(in: .newlines) ?? ""
    }
}

How to use:

let process = Process()

do {
    let output = try process.run(URL(fileURLWithPath: "/bin/zsh"), arguments: ["-c", "echo 'hello'"])
    print("Output: \(output)")
}
catch {
    print(error)
}
print("Status: \(process.terminationStatus)")

Outputs:

Output: hello
Status: 0
iUrii
  • 11,742
  • 1
  • 33
  • 48