Shell – How to Test if a File Descriptor is Valid for Input

file-descriptorsshell

In the question "Testing if a file descriptor is valid", a test is sought for testing whether a file descriptor is opened or not.

The answers all focus on testing whether the file descriptor is opened for output, but how may one test whether the file descriptor is opened for input?

This came up in a comment thread for an answer to another question, where the answer said, paraphrasing,

if [ -n "$1" ]; then
    # read input from file "$1" (we're assuming it exists)
elif [ ! -t 0 ]; then
    # read input from standard input (from pipe or redirection)
else
    # no input given (we don't want to read from the terminal)
fi

The problem with [ ! -t 0 ] is that the -t test is true if the file descriptor is open and associated with a terminal. If the test is false, then the descriptor is either closed, or not associated with a terminal (i.e. we're reading from a pipe or redirection). Testing with [ ! -t 0 ] is therefore not a guarantee that the file descriptor is even valid.

How to determine whether it's valid (so that read would not complain) or whether it's closed?

Best Answer

The check is easy to do in C with either read(fd, 0, 0) or (fcntl(fd, F_GETFL) & O_WRONLY) == 0. I wasn't able to trick any standard utility into doing just that, so here are some possible workarounds.

On linux, you can use /proc/PID/fdinfo/FD:

if [ ! -d "/proc/$$/fd/$fd" ] && grep -sq '^flags.*[02]$' "/proc/$$/fdinfo/$fd"; then
    echo "$fd" is valid for input
fi

On OpenBSD and NetBSD, you can use /dev/fd/FD and dd with a zero count:

if dd if=/dev/fd/3 count=0 3<&"$fd" 2>/dev/null; then
    echo "$fd" is valid for input
fi

On FreeBSD, only the first 3 fds are provided by default in /dev/fd; you should either mount fdescfs(5) on /dev/fd or:

if (dd if=/dev/fd/0 count=0 <&"$fd") 2>/dev/null; then
    echo "$fd" is valid for input
fi

Notes:

On some systems, bash does its emulation of /dev/fd/FD, and so cat </dev/fd/7 may work completely different from cat /dev/fd/7. Same caveat applies to gawk.

A read(2) with length 0 (or an open(2) without O_TRUNC in its flags) will not update the access time or any other timestamps.

On linux, a read(2) will always fail on a directory, even if it was opened without the O_DIRECTORY flag. On other Unix systems, a directory may be read just like another file.

The standard leaves unspecified whether dd count=0 will copy no blocks or all blocks from the file: the former is the behaviour of GNU dd (gdd) and of the dd from *BSD.

Related Question