The code below is a simple bash script I am running on a Linux machine, and I was wondering that why the time interval between each output is four seconds instead of eight?

$ for test in test1 test2 test3; do (echo ${test}; sleep 4s; echo hop2; sleep 4s; echo hop3) | date;  done
Sun 11 Apr 2021 12:42:27 AM +07
Sun 11 Apr 2021 12:42:31 AM +07
Sun 11 Apr 2021 12:42:35 AM +07

Despite increasing the latter time value to be somewhat longer, the time interval between each output is still four seconds.

$ for test in test1 test2 test3; do (echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3) | date;  done
Sun 11 Apr 2021 12:42:44 AM +07
Sun 11 Apr 2021 12:42:48 AM +07
Sun 11 Apr 2021 12:42:52 AM +07

This is very confusing, I will really appreciate it if anyone can explain this.

What'd more confusing is that if I put the date command in the front, it looks like none of the sleep commands are executed:

$ for test in test1 test2 test3; do (date; echo ${test}; sleep 50s; echo hop2; sleep 50s; echo hop3) | date;  done
Sun 11 Apr 2021 01:22:35 AM +07
Sun 11 Apr 2021 01:22:35 AM +07
Sun 11 Apr 2021 01:22:35 AM +07
  • You haven't defined hop2. Increase the first sleep by 50 seconds instead of the second sleep and see what happens. Apr 10, 2021 at 18:15
  • @CinaedSimson I knew that if I changed the value of the first sleep, it will work as I expect, but why the second sleep doesn't do its job? Apr 10, 2021 at 18:21
  • @answer this should help explain part of this: How can I time a pipe?
    – terdon
    Apr 10, 2021 at 18:24
  • 2
    Date does not take input like that. Just replace the pipe with a semicolon inside the () might do what you expected? the echo hps dont make sense here either Apr 10, 2021 at 18:52
  • 1
    "This is very confusing..." I agree. The entire script is on one long line. If you broke it down into separate statements it would be much easier to see what is happening.
    – Wastrel
    Apr 11, 2021 at 16:04

3 Answers 3


To clarify this, let me add add some debugging output to stderr (bypassing the pipe), before & after echoing "hop2":

$ for test in test1 test2 test3; do 
      (echo ${test}; sleep 4s; echo before hop2 >&2;
       echo hop2; echo after hop2 >&2; sleep 4s; echo hop3) | date;
Sat Apr 10 11:29:46 PDT 2021
before hop2
Sat Apr 10 11:29:50 PDT 2021
before hop2
Sat Apr 10 11:29:54 PDT 2021
before hop2

Note that echo after hop2 >&2 never executes, and neither do the commands after it: the second sleep and the echo hop3.

As I understand it, here's what happens. Within the loop, two separate processes execute in parallel, with output from the first piped to input of the second. The two processes execute:

echo ${test}
sleep 4s
echo before hop2 >&2
echo hop2
echo after hop2 >&2
sleep 4s
echo hop3



Here's the rough sequence of execution (the exact sequence of the first 3 steps and the beginning of step 4 will be somewhat random):

  1. Process 1 executes echo ${test}; this writes "test1" (and a newline) into the pipe, where it's buffered so that it can be read later.
  2. Process 2 executes date, printing the current date to the terminal.
  3. Process 2 exits, closing its end of the pipe.
  4. Process 1 executes sleep 4s.
  5. After 4 seconds, process 1 executes echo before hop2 >&2, printing "before hop2" to the terminal.
  6. Process 1 tries to execute echo hop2, but since the pipe's only reader has closed it, it gets a SIGPIPE errror. This apparently causes the entire subshell process (not just the echo command) to exit.

Note that this happens only because echo is a shell builtin; if you used /bin/echo hop2 (an external command, instead of the shell's echo builtin) it would execute the second sleep as you expected.

BTW, this is relatively consistent between different shells. I get the same results running this in bash, zsh, dash, and ksh (interactively). ksh in a script is a bit different, because it apparently doesn't wait for process 1 to exit before continuing, so the dates all execute immediately, followed (4 seconds later) by a series of "before hop2" lines.

  • re. "the exact sequence of the first 3 steps", step 1 has to come before step 3, since otherwise the first echo would already trigger the signal. Good point on the builtin vs. external echo!
    – ilkkachu
    Apr 10, 2021 at 19:14
(echo ${test}; ...) | date

What are you trying to do here? You're piping data to the stdin of date, but date doesn't read any input. It just prints the date and exits.

After date exits, the pipe closes, after which any data printed to it has nowhere to go and the process writing to the pipe, the subshell (echo; sleep; echo; sleep), gets sent the SIGPIPE signal and it dies.

This is how pipelines work in general. If the left-hand side is something that can produce loads of output, perhaps even an arbitrary amount, the signal is what tells it to stop after the right-hand side loses interest.

E.g. in something like cat /dev/zero | head -c128 > /dev/null, the signal eventually kills the cat so it isn't left around indefinitely. Neither the shell nor the cat prints an error message for that. It's just part of normal operation that pipes work like this. Similarly for the case at hand here. (In some cases you do get an error message though, just don't expect to always get one for this.)

(The outer loop doesn't affect the results, so I dropped it. You can use time ( ... ) | ... to measure the time the pipeline takes.)

In Bash, you can check the exit statuses of the commands in the pipeline from the PIPESTATUS array:

$ ( echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3; ) |
    date;  declare -p PIPESTATUS
Sat Apr 10 21:34:56 EEST 2021
declare -a PIPESTATUS=([0]="141" [1]="0")

SIGPIPE is number 13 at least on Linux, so it matches the shown exit status 141 = 128 + signal number. (A process could also exit normally with status >= 128, but that's not the case here.)

Or, you could strace to see what happens:

$ strace -f bash -c '( echo ${test}; sleep 4s; echo hop2; 
                       sleep 50s; echo hop3; ) | date'
[pid 31647] write(1, "hop2\n", 5)       = -1 EPIPE (Broken pipe)
[pid 31647] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=31647, si_uid=1000} ---
[pid 31647] +++ killed by SIGPIPE +++

On the other hand, if there was something that reads the input on the right hand side of the pipe, there would be no SIGPIPE, and the whole pipeline would sleep for a total of 54 seconds:

$ time ( echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3; ) |
       cat > /dev/null 

real    0m54.005s
user    0m0.000s
sys     0m0.000s

Or, if you ignore the SIGPIPE on the writing side:

$ time ( trap '' PIPE; echo foo; sleep 4s; echo hop2; sleep 10s; 
         echo hop3; ) | date 
Sat Apr 10 21:57:07 EEST 2021
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe

real    0m14.006s
user    0m0.004s
sys     0m0.000s

With SIGPIPE ignored, the shell gets a regular error return when writing to the closed pipe, and it does print an error for that.

Note that there's also a timing issue here between the left-hand side of the pipe writing to the pipe, and the right-hand side closing it.

If I do something like this:

( echo foo; echo bar; sleep 4s; ) | date;

the sleep does run. But if I replace the two echo's with for i in {1..100}; do echo foo; done;, the LHS dies before getting to the sleep. (The race here will depend on the system.)

And regarding the case with another date as the first thing on the LHS:

(date; echo ${test}; sleep 50s; echo hop2; sleep 50s; echo hop3) | date

This is also due to timing issues. Since date is an external command, it may well be slower for the shell to launch it, than to just internally handle an echo (it is a builtin in next to all shells, after all). That makes it more likely for the date on the right-hand side to win the race and close the pipe before the first echo gets to write to it.

In general, it's not very useful to write to pipes if your intent is not to pass some data to the other side, and if you have something else to do, make sure to handle the possible SIGPIPE.


There is nothing to send to date as it receives no data on stdin.

So, using a | is simply incorrect. I believe that you need to use && instead.

But the question:

Why the time interval between each output is four seconds instead of eight?

Is still valid, but confusing to explain. The short answer is that the commands after echo hop2 never get executed. The second sleep is never used.

$ (echo test; sleep 4s; echo hop2; sleep 4s; echo hop3) | date; date
Sun 11 Apr 2021 05:30:28 AM UTC
Sun 11 Apr 2021 05:30:32 AM UTC

If we use stderr instead of stdout all the commands execute:

$ (echo test >&2; sleep 4s; echo hop2 >&2; sleep 4s; echo hop3 >&2) | date; date

Sun 11 Apr 2021 05:32:36 AM UTC
Sun 11 Apr 2021 05:32:44 AM UTC

The reason is that date doesn't receive input in stdin, any attempt to write to it will be answered with a SIGPIPE and the whole (bash) shell (what is at the left of the |) will close. Breaking any other command in the list.

The reason for the very first echo on your example to succeed is time dependent:

  1. At the beginning, the running shell will set up the pipe |.
  2. To do that, it opens a child where the command date is started.
  3. Without waiting for anything, the shell returns to the other side of the pipe and (in another child) asks it to execute echo ${test}.
  4. Since the pipe is still open (date haven't closed it) the output of the echo is buffered and the command sleep 4s gets executed.
  5. The date command gets enough time to end and close the pipe.
  6. Now, with the pipe closed the next echo will not be able to write to it. It will cause a SIGPIPE.
  7. On receiving a SIGPIPE the child shell on the left side of the | exits completely (the echo is a builtin and errors on built-ins cause the failure of the whole shell).
  8. No more commands on the pipe will be executed.

That is why this:

$ (sleep 1; echo test; sleep 4s; echo hop2 >&2; ) | date ; date 

Sun 11 Apr 2021 06:10:04 AM UTC
Sun 11 Apr 2021 06:10:05 AM UTC

Doesn't even print the first echo. The time could be as small as 0.1 seconds. That is enough to allow date to end.

It even could be a call to an external command that delay the echo just enough:

(/usr/bin/true; echo test; sleep 4s; echo confirm >&2 ) | date; date
Sun 11 Apr 2021 06:27:01 AM UTC
Sun 11 Apr 2021 06:27:01 AM UTC

Your for loop is just the above repeated three times. Thus only four seconds between each loop.

You must log in to answer this question.

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