Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift stdout redirect to a string

Tags:

stdout

swift

pipe

I'm trying to redirect stdout to a string with Swift, to collect whatever is passed to the print function. I've read through a few resources that suggest my approach should be working, however, the following proof of concept script only outputs "Starting":

import Foundation

let outPipe = Pipe()
var outString = "Initial"
outPipe.fileHandleForReading.readabilityHandler = { fileHandle in
    outString += String(data: fileHandle.availableData, encoding: .utf8)!
}
print("Starting")

// Redirect
setvbuf(stdout, nil, _IONBF, 0)
dup2(outPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)

print("Captured")
freopen("/dev/stdout", "a", stdout)
print(outString)

Any ideas where I've gone wrong? All help is appreciated!

like image 334
Jake Avatar asked Nov 29 '25 17:11

Jake


1 Answers

The main problem is that the pipe's read handler is called on a secondary queue, but your program does not have a run loop. Therefore the program terminates before the event handler is called once.

In a command line application, one option so solve this is with a “dispatch semaphore” which is signaled in the event handler, once it detect an end-of-file condition on the pipe. And in order to make this work, we must close the writing end of the pipe eventually.

Also the standard output file descriptor should be saved and restored later. Reopening "/dev/stdout" will not work if the program is run in the Xcode debugger console.

The following code produces the expected output. It is meant as a “proof of concept” example. In your production code you'll want to avoid things like a forced try!.

import Foundation

let outPipe = Pipe()
var outString = "Initial"
let sema = DispatchSemaphore(value: 0)
outPipe.fileHandleForReading.readabilityHandler = { fileHandle in
    let data = fileHandle.availableData
    if data.isEmpty  { // end-of-file condition
        fileHandle.readabilityHandler = nil
        sema.signal()
    } else {
        outString += String(data: data,  encoding: .utf8)!
    }
}
print("Starting")

// Redirect
setvbuf(stdout, nil, _IONBF, 0)
let savedStdout = dup(STDOUT_FILENO)
dup2(outPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)

print("Captured")

// Undo redirection
dup2(savedStdout, STDOUT_FILENO)
try! outPipe.fileHandleForWriting.close()
close(savedStdout)
sema.wait() // Wait until read handler is done

print(outString) // Prints "InitialCaptured"
like image 199
Martin R Avatar answered Dec 01 '25 11:12

Martin R



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!