swiftnsfilehandle

How to detect end-of-file for FileHandle.standardInput from Terminal


I have a Swift program that reads from FileHandle.standardInput (in Objective-C, this would be +[NSFileHandle fileHandleWithStandardInput]. It should terminate reading when it hits end-of-file on the input stream, but when I run it using the Terminal (on macOS Sierra) as input, it doesn't detect end-of-file when I hit Ctrl+D.

Here is a simplified example of what I'm doing. This program simply reads from standard input and writes what it has read to standard output:

#!/usr/bin/swift

import Foundation

let input = FileHandle.standardInput
let output = FileHandle.standardOutput

let bufferSize = 1024

var data = input.readData(ofLength: bufferSize)
while data.count > 0 {
    output.write(data)
    data = output.readData(ofLength: bufferSize)
}

I expect readData(ofLength:) to return a Data object with a count of zero when it reaches end of file.

When I run with a file redirected to standard input, like this:

./echo.swift < foo.txt

It writes out the file and terminates.

However, if I run it like this:

./echo.swift

and then type some text and hit Ctrl+D, I expect it to treat the Ctrl+D as end-of-file and terminate. But it doesn't do that. It just keeps running and echoing lines. It will eventually terminate if I hit Ctrl+D over and over, but that's not what I want.

Changing the bufferSize doesn't seem to help. I get the same behavior if I set it to 1.

I suspect I need to set some sort of buffering parameter on the stdin file descriptor, or the terminal device, or catch a signal, or something, but I don't know what.

I know that I could use the C stdio fread() API instead, which properly detects an end-of-file condition from the terminal, or I could use Swift's readLine(_:) to read from standard input without worrying about file handles/descriptors, but I want to know if there is a way to do this with FileHandle or the raw file descriptor without re-implementing C stdio.


Update: After spending an hour reviewing Apple's LibC source, I concluded "It's complicated" and so now I'm just using fread(..., stdin) in my program. But I'd still like to know if there is some simple way to get this to work with FileHandle.


Solution

  • Today, Swift 5, something like this ...

    while (FileHandle.standardInput.availableData.count > 0) {
      FileHandle.standardOutput.write(FileHandle.standardInput.availableData)
    }
    

    Simple and does work almost like a cat program, some stuff to tune for that.

    Better with correct behaviour :

    var data: Data
    repeat {
      data = FileHandle.standardInput.availableData
      FileHandle.standardOutput.write(data)
    } while (data.count > 0)
    

    The doc about it :

    The data currently available through the receiver, up to the the maximum size that can be represented by an NSData object.

    If the receiver is a file, this method returns the data obtained by reading the file from the current file pointer to the end of the file. If the receiver is a communications channel, this method reads up to a buffer of data and returns it; if no data is available, the method blocks. Returns an empty data object if the end of file is reached. This method raises NSFileHandleOperationException if attempts to determine the file-handle type fail or if attempts to read from the file or channel fail