Blog

Swift Tip: Reading from Standard Input/Output

The open-source Markdown Playgrounds app we’ve been documenting on Swift Talk uses the Swift REPL to execute Swift code blocks. This means that we’re reading the standard output of the REPL process to show the results:

stdOut = Pipe()
process = Process()
process.launchPath = "/usr/bin/swift"
process.standardOutput = stdOut
process.launch()

token = NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: stdOut.fileHandleForReading, queue: nil) { _ in
    let handle = note.object as! FileHandle
    // Read the available data ...
    handle.waitForDataInBackgroundAndNotify()
}

stdOut.fileHandleForReading.waitForDataInBackgroundAndNotify()

We reach the important part of this process when we start reading data from the file handle. The REPL output is UTF-8 encoded, so the data we’re reading consists of UTF-8 code units. Since we’re receiving the data in chunks, we’re unable to assume that the chunks end on valid character boundaries.

For example, if we naively turn the available data into a string, we might run into issues like this:

148 1@2x

To avoid such issues, we have to buffer the incoming data and only transform it into a string when we’re sure that the data doesn’t end on an incomplete character. For our use case, we can determine this by waiting for newline characters.

We can abstract this logic into a simple struct:

struct REPLBuffer {
    private var buffer = Data()

    mutating func append(_ data: Data) -> String? {
        buffer.append(data)
        if let string = String(data: buffer, encoding: .utf8), string.last?.isNewline == true {
            buffer.removeAll()
            return string
        }
        return nil
    }
}

We can append to this buffer each time the file handle has data available. If the data ends on a newline, it returns the string — otherwise it just returns nil:

var buffer = REPLBuffer()
token = NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: stdOut.fileHandleForReading, queue: nil, using: { _ in
    let handle = note.object as! FileHandle
    if let string = buffer.append(handle.availableData) {
        print(string)
    }
    // ...
})

This pattern isn’t just useful for reading the standard output of a REPL process, you can use it whenever you’re reading from a file handle where the data might be incomplete.

The Markdown Playgrounds macOS app is open-source, you can check out the code or try it for yourself. We give a five minute overview of the app at the beginning of Swift Talk 145 (a public episode).

To support our work, subscribe to Swift Talk, or give a subscription as a gift.


Stay up-to-date with our newsletter or follow us on Twitter.

Back to the Blog

recent posts