1

I use the function below to run shell commands, but I can't seem to get it to print the output while the command is running. For example if I run /usr/sbin/system_profiler, I have to wait until the command is done executing before I can see the output.

How do I print the output of a shell command while the command is still running?

func runCommand(cmd: String, args: String...) -> (output: [String], error: [String], exitCode: Int32) {
print("running shell command")
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
print("shell end")
return (output, error, status)
}
gooberboobbutt
  • 797
  • 2
  • 8
  • 19

2 Answers2

1

You have to make your task asynchronous and readInBackgroundAndNotify.

Something like this (untested), it uses the terminationHandler and adds the readCompletionNotification observer to get the notifications.

var output = ""

func runCommand(cmd: String, args: String..., completion: @escaping (String, String, Int32) -> Void) {
    print("running shell command")
    let task = Process()
    task.launchPath = cmd
    task.arguments = args
    let outpipe = Pipe()
    task.standardOutput = outpipe
    let errpipe = Pipe()
    task.standardError = errpipe
    task.terminationHandler = { [unowned self] returnedTask in
        NotificationCenter.default.removeObserver(self,
                                                  name: FileHandle.readCompletionNotification,
                                                  object: (returnedTask.standardOutput as! Pipe).fileHandleForReading)
        let status = returnedTask.terminationStatus
        if status == 0 {
           completion(output, "", status)
        } else {
            let errorData = errpipe.fileHandleForReading.readDataToEndOfFile()
            let errorString = String(data:errorData, encoding: .utf8)!
            completion("", errorString, status)
        }
    }
    let outputHandle = (task.standardOutput as! Pipe).fileHandleForReading
    NotificationCenter.default.addObserver(forName: FileHandle.readCompletionNotification, object: outputHandle, queue: OperationQueue.current, using: { notification in
        if let data = notification.userInfo?[NSFileHandleNotificationDataItem] as? Data, !data.isEmpty {
            output.append(String(data: data, encoding: . utf8)!)
        } else {
            task.terminate()
            return
        }
        outputHandle.readInBackgroundAndNotify()
    })
    outputHandle.readInBackgroundAndNotify()
    task.launch()
}

If you want to print the data while receiving replace output.append(String(data: data, encoding: . utf8)!) with print(String(data: data, encoding: . utf8)!) and then you probably don't need the completion handler either.

vadian
  • 274,689
  • 30
  • 353
  • 361
0

Specifically, the block reason is outpipe.fileHandleForReading.readDataToEndOfFile()

You may try the following to verify this: ...

     task.standardOutput = outpipe
     let errpipe = Pipe()
      task.standardError = errpipe

    task.launch()

       repeat{
    let outdata = outpipe.fileHandleForReading.readData(ofLength: 100) //.readDataToEndOfFile()
    if var string = String(data: outdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        output = string.components(separatedBy: "\n")
 print(output)
    }
    let errdata = errpipe.fileHandleForReading.readData(ofLength: 100)// .readDataToEndOfFile()
    if var string = String(data: errdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        error = string.components(separatedBy: "\n")
        }} while (task.isRunning)

or add a readerHandler:

 outpipe.fileHandleForReading.readabilityHandler = { file in
         let  outdata = fileHandle.readData(ofLength: 100)
            if var string = String(data: outdata, encoding: .utf8) {
                string = string.trimmingCharacters(in: .newlines)
             print( string.components(separatedBy: "\n"))

        } 

Then you can have many methods to avoid this, link tap in a Timer or Observer in other threads.

E.Coms
  • 11,065
  • 2
  • 23
  • 35