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
  • Keep in mind that a redirection like command 3>&1, the redirection only applies to command, not to the rest of the script; you do not have to find an unused descriptor, it doesn't matter if fd 3 is already used in the script. The only way this could break is if a) command expects to be passed open fds from the parent process (which is usually well documented) b) command expects that the first new fd opened by it will be = 3. To guard against b), use 13>... or 7 instead of 3. Many people are confused by this. – mosvy Oct 2 '18 at 10:21

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}>&-

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.

| improve this answer | |
  • Why are the quotes not needed when posix is set in bash? While you've solved my specific problem, is there a generic way to solve it? (Sorry my initial question wording was not clear) – Tom Hale Oct 2 '18 at 9:20
  • @TomHale, POSIX requires split+glob not be enabled in redirection targets when non-interactive. bash only does it in POSIX mode. That means the behaviour of >&$tmp depends on the value of $IFS there. What's not generic in the solutions I suggest? – Stéphane Chazelas Oct 2 '18 at 9:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.