Zsh POSIX – Equivalent of Bash’s {var}>&1

bashposixshellzsh

Is there an equivalent of {var}>&1 in zsh?

The bash manual says:

Each redirection that may be preceded by a file descriptor number may instead be preceded by a word of the form {varname}. In this case, for each redirection operator except >&- and <&-, the shell will allocate a file descriptor greater than 10 and assign it to {varname}. If >&- or <&- is preceded by {varname}, the value of varname defines the file descriptor to close.

An example usage of this is capturing STDERR from a command:

{ error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&$tmp ; } 2>&1); } {tmp}>&1

How would one do the same in zsh, and also in POSIX-land?

I'm after a generic solution, how do I:

  • Find the unused file descriptor
  • Emulate the {var}>&1 syntax

Best Answer

Support for {var}>... was added to ksh93, bash and zsh at the same time on a suggestion of a zsh developer. The {var}>... operator works in zsh, but not for compound commands.

Also note that while in:

cmd 3>&1

The fd 3 is open only for cmd, in

cmd {var}>&1

The dynamically allocated fd (stored in $var) remains open after cmd returns in both zsh and bash. That operator is mostly designed to be used with exec (see also sysopen in zsh for a more straightforward interface to the open() system call).

So your code above is missing exec {tmp}>&- to release that fd afterwards in bash.

So here you could do:

if exec {tmp}>&1; then
   errors=$(exec 2>&1 >&"$tmp" {tmp}>&- && ls -ld /x /bin | tr o Z)
   exec {tmp}>&-
fi

Which would work in bash, zsh and ksh93 and not leak a fd. (the quotes around $tmp are only needed in bash and when its posix option is not enabled). Note that in ksh93 or when zsh or bash are in POSIX mode, a failing exec causes the shell to exit (dup() failing here could be caused by stdout being closed or some limit on number of open files being reached or other pathological cases for which you may want to exit anyway).

But here, you don't need a dynamically allocated fd, just use fd 3 for instance which is not used in that code:

{ errors=$(exec 2>&1 >&3 3>&-; ls -ld /x /bin | tr o Z); } 3>&1

Which would work in any Bourne-like shell.

Like in the dynamic fd approach above even if it's not as obvious, if the dup2() (in 3>&1) fails, the assignment will not be run, so you may want to make sure errors is initialised before (with a unset -v errors for instance).

Note that it doesn't matter whether the fd 3 is otherwise open or in use in the rest of the script (that original fd if open will be left untouched and restored at the end), what matters is whether the code that you are embedded inside the $(...) expects fd 3 to be open.

Only fds 0, 1 and 2 are expected to be open by applications, other fds are not. ls and tr don't expect anything about fd 3. Cases where you may need to use a different fd is when your code explicitly makes use of that fd and expects it to have been open beforehand like if instead of ls, you had cat /dev/fd/3 where fd 3 is expected to have been open to some resource somewhere earlier in your script.

To answer the question on how to assign the first free fd in POSIX shells, I don't think there's a way with the POSIX shell and utilities API. It may also not make sense. The shell may do what it wants internally with any fd provided that doesn't get in the way of its own API. For instance, you may find that fd 11 is free now, but may later be used by the shell for something internal and you writing to it could affect its behaviour. Also note that in POSIX sh, you can only manipulate fds 0 to 9.