2

In zsh,

echo "hello" 1>&1 1>&1 1>&1 | cat

prints hello 8 times, while

echo "hello" 1>&1 1>&1 1>&1 1>&1 | cat

prints hello 16 times. So hello is printed 2^n times, where n is equal to the number of 1>&1 in the above pipe.

What I am looking for? Multiple sources suggest that the order in which the pipes+redirections occur is:

"The standard input, standard output, or both of a command shall be considered to be assigned by the pipeline before any redirection specified by redirection operators that are part of the command"

src: https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/utilities/V3_chap02.html#tag_18_09_02, https://unix.stackexchange.com/a/672961/456507

The rough way of putting this is: sequence in which redirections and pipelines are processed is "left to right, after a pipe".

Someone in zsh Libera chat IRC suggested that, "echo's fd1 is pointed to what will be cat's fd0" before redirections are processed. Can someone expand that way of wording to include the redirections 1>&1? This is important, because I think if one can word it, then one can word it similarly for other redirections and pipelines and understand what is going on. Feel free to use some fictitious C/C++ pointer terminology if you feel like!

3
  • ksh prints 'hello' once. So does bash. So does dash. Commented Oct 14, 2021 at 1:07
  • @MarcWilson Yep, zsh has multios feature which is enabled by default. Am looking for someone to word it in a way that can enable imagining a flow diagram each time I look at zsh redirections and pipes. Check my other recent questions on zsh, they make the head scratch. Like this: unix.stackexchange.com/questions/672939/…
    – codepoet
    Commented Oct 14, 2021 at 1:10
  • @reportaman I've yet to find any reason to bother with zsh. Every time I look at it, it's another incompatibility with the rest of the world. You might as well use fish. Commented Oct 14, 2021 at 15:14

1 Answer 1

3

This behavior is specific to the multios feature which is specific to zsh. So other documentation, such as the POSIX specification, won't help you that much. Zsh is POSIX-compliant in this respect when multios is turned off, so the POSIX behavior does give part of the story, but only part.

The most natural way to understand redirections, both with and without multios, is with an imperative mindset. Redirections of a command are processed from left to right (except that the pipes go first), and each redirection modifies the state of the process. Here's what the shell does.

  1. Initially the command has the same file descriptors open as the code surrounding it.

  2. If the command is on the right-hand side of a pipe, connect the read side of the pipe to file descriptor 0 (standard input). If the command is on the left-hand side of a pipe (including |&), connect the write side of the pipe is connected to file descriptor 1 (standard output).

  3. Apply redirections in order. Here I'm only listing the main relevant cases. See the manual for a complete list of redirection operators. N stands for zero or more digits or for a file descriptor variable. M stands for one or more digits and FILENAME stands for a file name (which can be a process substitution).

    • N<&M duplicates file descriptor M to N, i.e. N becomes connected to whatever M is connected to. Whatever was previously connected to file descriptor N no longer matters.
    • N<FILENAME opens FILENAME for input and connects it to file descriptor N. Whatever was previously connected to file descriptor N no longer matters.
    • N>&M, N>FILENAME and N>>FILENAME are similar to the < forms except that N>FILENAME opens the file for overwriting and N>>FILENAME opens the file for appending, except in the following case.
      If multios is enabled (it is by default) and there was already a redirection (including a pipe) on file descriptor N in this command, then instead of connecting file descriptor N to M-or-FILENAME, zsh creates a pipe P, forks a built-in tee-like process which reads from the pipe and duplicates its output to M-or-FILENAME and to what was on file descriptor M, and connects N to the write end of the pipe. That is, a second redirection on file descriptor N is roughly equivalent executing exec N> >(tee FILENAME >&N) before the command. (Note that this is equivalent to applying the redirection N> >(tee FILENAME >&N) in simple cases, but not when there are three or more redirections on the same file descriptor.)
    • N<&- or N>&- closes file descriptor N.
  4. If the command is on the left-hand side of |&, perform the 2>&1 redirection. (This is equivalent to sending standard error through the pipe in simple cases, but not always in complex cases.)

So with multiple redirections 1>&1 1>&1 1>&1 …, with multios, the first redirection does nothing in itself: redirecting a file descriptor to itself is a no-op (except it's an error if the file descriptor is closed). But it still counts as a redirection for multios, so the second 1>&1 creates a pipe to a tee-like process that duplicates its input, with both outputs going to the original stdout. The third 1>&1 creates another tee-like process which duplicates its input, with both outputs going to what is now stdout, which is the input of the first tee, so the data is duplicated twice. A fourth 1>&1 duplicates the input with both outputs going to the second tee, resulting in three duplications, i.e. 8 times the input. And so on. With multiple redirections and a pipe, the pipe counts as a redirection, so the first 1>&1 is the second redirection of file descriptor 1 and starts a duplication, which is why echo "hello" 1>&1 1>&1 1>&1 | cat prints hello 8 times. It's equivalent to

( exec 1> >(tee /dev/fd/1 1>&1);
  exec 1> >(tee /dev/fd/1 1>&1);
  exec 1> >(tee /dev/fd/1 1>&1);
  echo "hello" ) | cat

or

{ { { { echo hello; } > >(tee /dev/fd/1 1>&1); } > >(tee /dev/fd/1 1>&1); } > >(tee /dev/fd/1 1>&1); } | tr a-z A-Z

Exercise 1: explain the behavior of the following command.

/bin/echo hello 1>&1 1>&1 1>&-

1>&1 1>&1 duplicates the output to stdout. Then 1>&- closes stdout, so echo has nowhere to write and complains.

Exercise 2: explain the difference between these two commands.

/bin/echo hello 1>&- 1>&2
/bin/echo hello 1>&1 1>&1 1>&- 1>&2

In the first case, there is only one non-closing redirection on file descriptor 1, so the multio behavior doesn't kick in. 1>&- closes fd 1 and 1>&2 is an ordinary redirection, so the command prints hello on fd 2, just like /bin/echo 1>&2 would. In the second case, there are multiple redirections on fd 1, so after 1>&- closes it, zsh performs a multio redirection for 1>&2. This involves forking a tee-like process that would write to what is currently connected to fd 1, but fd 1 is closed, hence this fails with multio failed for fd 1: bad file descriptor.

Exercise 2b: explain

/bin/echo hello 1>&1 1>&- 1>&2

I think closing the file descriptor resets the is-this-the-first-redirection state, so the second redirection is not a multio one, and this is equivalent to /bin/echo hello 1>&2.

You must log in to answer this question.

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