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

2 Answers 2

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.

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

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.