22

I'd like to make a bash script output additional information to file descriptors (FDs) greater than or equal to 3, when they are open. To test whether an FD is open, I devised the following trick:

if (printf '' 1>&3) 2>&-; then
  # File descriptor 3 is open
else
  # File descriptor 3 is not open
fi

This is sufficient for my needs, but I'm curious as to whether there is a more idiomatic way of testing if an FD is valid. I'm especially interested about whether there exists a mapping of the fcntl(1) syscall to a shell command, which would allow the retrieval of FD flags (O_WRONLY and O_RDWR to test whether the FD is writable, and O_RDONLY and O_RDWR to test whether the FD is readable).

7 Answers 7

20

In ksh (both AT&T and pdksh variants) or zsh, you can do:

if print -nu3; then
  echo fd 3 is writeable
fi

They won't write anything on that fd, but still check if the fd is writable (using fcntl(3, F_GETFL)) and report an error otherwise:

$ ksh -c 'print -nu3' 3< /dev/null
ksh: print: -u: 3: fd not open for writing

(which you can redirect to /dev/null).

With bash, I think your only option is to check if a dup() succeeds like in your approach, though that won't guarantee that the fd is writable (or call an external utility (zsh/perl...) to do the fcntl()).

Note that in bash (like most shells), if you use (...) instead of {...;}, that will fork an extra process. You can use:

if { true >&3; } 2> /dev/null

instead to avoid the fork (except in the Bourne shell where redirecting compound commands always causes a subshell). Don't use : instead of true as that's a special builtin, so would cause the shell to exit when bash is in POSIX compliance mode.

You could however shorten it to:

if { >&3; } 2> /dev/null
1
  • 5
    @mikeserve, re: your edit, what's that with <>? The shell is not going to read from its stderr, why would you want to open it in read+write? What do you mean with what happened to intrinsic?? Commented Aug 25, 2019 at 8:46
6

In the POSIX command Application Usage description you'll find the following:

There are some advantages to suppressing the special characteristics of special built-ins on occasion. For example:

command exec > unwritable-file

does not cause a non-interactive script to abort, so that the output status can be checked by the script.

This is why you can just do:

if    command >&3
then  echo 3 is open >&3
else  ! echo 3 is not open
fi    2<>/dev/null

Or...

{ command >&3
  printf %s\\n%.0d  string "0$(($??8:0))" >&"$(($??1:3))"
} 2<>/dev/null

Which will write string followed by a \newline either to stdout or 3 and still pass on a non-zero exit status when 3 is not open because the math done on $? winds up failing to convert the octal 08 to %decimal but truncates to nothing at all the octal 00.

Or...

command exec >&3 || handle_it

But if you're using ksh93, you can just do:

fds

For a list of of open file descriptors. Add -l to see where they go.

2
  • command exec >&3 || echo $? does not echo the exit status, but command >&3 || echo $? does.
    – jarno
    Commented Mar 31, 2021 at 17:18
  • This doesn't actually work on bash -- gives '3: Bad file descriptor' if it is not open
    – Chris Dodd
    Commented Apr 13, 2022 at 20:13
3

Open file descriptors can be found in /proc/<pid>/fd. To list, for example, the open file descriptors of the current shell you can issue ls -l /proc/$$/fd which should give you something like:

total 0
lrwx------ 1 testuser testuser 64 jun  1 09:11 0 -> /dev/pts/3
lrwx------ 1 testuser testuser 64 jun  1 09:11 1 -> /dev/pts/3
lrwx------ 1 testuser testuser 64 jun  1 09:11 2 -> /dev/pts/3
lrwx------ 1 testuser testuser 64 jun  1 09:39 255 -> /dev/pts/3

When you open a file using:

touch /tmp/myfile
exec 7</tmp/myfile

It should be listed by a new ls -l /proc/$$/fd:

lr-x------ 1 testuser testuser 64 jun  1 09:11 7 -> /tmp/myfile

If you close the file descriptor again using exec 7>&- it is also not listed in /proc/$$/fd anymore.

8
  • 2
    All this is quite specific to Linux. FWIW.
    – lcd047
    Commented Jun 1, 2015 at 9:27
  • 1
    Tested it on Linux as well as on Solaris (10 and 11). The difference is that you need to use pfiles <pid> to see which file descriptor is connected to which file while ls -l displays the connection on Linux.
    – Lambert
    Commented Jun 1, 2015 at 9:31
  • 1
    I like the compactness of [ -e /proc/$$/fd/3 ], but I prefer not to rely on procfs, as it is deprecated in FreeBSD and possibly other un*ces as well.
    – Witiko
    Commented Jun 1, 2015 at 9:33
  • 1
    Brings me to the alternative of using pfiles <pid> or lsof -p <pid> to see which file descriptors are open.
    – Lambert
    Commented Jun 1, 2015 at 9:33
  • 1
    /proc doesn't exist at all on OpenBSD. On FreeBSD and NetBSD it has to be mount-ed explicitly, and /proc/<PID> don't have a subdirectory fd.
    – lcd047
    Commented Jun 1, 2015 at 9:35
3

Your trick looks cute; but for an idiomatic way I wonder why you didn't use:

if ( exec 1>&3 ) 2>&-
7
  • This is, indeed, a cleaner way.
    – Witiko
    Commented Jun 1, 2015 at 9:34
  • 7
    That creates a subshell though which is most shells means forking a process. That doesn't guarantee the fd is writeable. You can use { true >&3; } 2> /dev/null to avoid the fork. Or { command exec >&3; } 2> /dev/null if you want to redirect stdout to it. Commented Jun 1, 2015 at 15:28
  • @Stephane; The subshell trick that @Witiko invented was to not affect the file descriptors of the current environment when using a redirection to obtain a redirection. - Could you elaborate on the "writable fd" you mention?
    – Janis
    Commented Jun 1, 2015 at 15:39
  • 2
    { true >&3; } 2> /dev/null will not affect the current environment either and won't fork (except in the Bourne shell). I mean that (exec 1>&3) 2>&- will return true for a fd open in read-only mode. Commented Jun 1, 2015 at 15:52
  • 1
    exec being a special builtin will exit the shell if it fails (for bash, only when in POSIX compliance mode). command exec prevents that. true is not a special builtin. Note that exec and command exec do affect the current environment (that's why I said if you want to redirect stdout to it). Commented Jun 1, 2015 at 16:02
0

The question is quite old - but anyway - why just do not use builtins?

for i in {0..5} ; do if [ -t $i ]; then echo "$i is a valid FD"; else echo "$i is INVALID FD"; fi; done

Output:

0 is a valid FD
1 is a valid FD
2 is a valid FD
3 is INVALID FD
4 is INVALID FD
5 is INVALID FD

So, to answer the question - would suggest:

if [ -t 3 ]; then
  # File descriptor 3 is open
else
  # File descriptor 3 is not open
fi
3
  • 3
    -t does not test if a file descriptor is valid, but if it's connected to a tty. Prepend a echo yup | to your script, and will say that 0 is INVALID FD, while in fact it's very valid fd, a pipe.
    – user313992
    Commented Feb 25, 2020 at 8:21
  • @mosvy According to the POSIX standard, the -t test only returns true if the file descriptor is associated with a terminal. The reference is here. Commented Sep 23, 2021 at 14:54
  • 1
    @SiuChingPong-AsukaKenji- "associated with a terminal" is the same thing as "connected to a tty", but using different words.
    – user313992
    Commented Oct 16, 2021 at 9:07
0

For Linux atleast, [[ -e /dev/fd/${FD} ]] will do, where FD is the bash-variable that stores the FD you want to test. The command succeeds when FD is open, fails otherwise.

Demo:

exec 3<>/tmp/some-file
[[ -e /dev/fd/3 ]] && echo 'FD is open' || echo 'FD is closed'
exec 3<&- ; exec 3>&-
[[ -e /dev/fd/3 ]] && echo 'FD is open' || echo 'FD is closed'
-1

If you are interested in a low forking solution so as to use it repeatdly, I would suggest this function:

checkfd() {
    exec 2>/dev/null
    if exec >&3 ; then
        exec 1>/dev/tty
        echo "fd3 OK"
    else
        echo "fd3 KO"
    fi
    exec 2>/dev/tty
}

And here is what it produces with a zsh:

$ checkfd            
fd3 KO
$ checkfd 3>/dev/null
fd3 OK
$
7
  • In most shells exec >&3 will kill the shell when 3 is not open.
    – mikeserv
    Commented Jun 1, 2015 at 16:52
  • At least it is working on zsh and bash. Could you provide the shell on which the failing exec caused an exit?
    – athena
    Commented Jun 1, 2015 at 16:56
  • Yeah. In bash do set -o posix and try again. In zsh... i think it's a matter of setting the env var POSIX_BUILTINS to a not-null value - but i forget offhand. In any case, zsh is not a shell which attempts to POSIX compliance, and so it is definitively non-standard. Both of those shells eschew compatibility for what some believe is convenience.
    – mikeserv
    Commented Jun 1, 2015 at 17:01
  • It is also working on plain Bourne shell.
    – athena
    Commented Jun 1, 2015 at 17:02
  • In bash, with set -o posix a try is successful.
    – athena
    Commented Jun 1, 2015 at 17:05

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .