0

Given the pipeline

printf '%s\n'  1 2 3 4 5 6 7 8 9 |
while read -r num
do
  echo "$num" > /dev/stderr
  echo "$num"
done |
while read -r num
do
  echo $(( $num * 10 ))
  [ "$num" -eq 5 ] && break
done

bash will output something like

1
2
10
3
20
4
30
5
40
50
6
7
8
9

to the terminal. The places in the single digit sequence where the two-digit numbers are inserted will be different each time you run the pipeline because of how buffering and/or scheduling work.

I would like to have the first while-read loop terminate at once when the second loop terminates; that is, I want the pipeline to not output any single digits after it has printed the number 50.

I tried using the command stdbuf -o0 to set the buffer to 0, as suggested by this article and Force line-buffering of stdout in a pipeline.

stdbuf -o0 printf '%s\n'  1 2 3 4 5 6 7 8 9 |
while read -r num
do
  echo "$num" > /dev/stderr
  echo "$num"
done |
while read -r num
do
  echo $(( $num * 10 ))
  [ "$num" -eq 5 ] && break
done

hoping to achieve this, but the printf command and the first loop still stay alive and can print their output after the last loop has printed the number 50.

I also tried set -o pipefail and turning the second loop into the function:

g ()
{
  while read -r num
  do
    echo $(( $num * 10 ))
    [ "$num" -eq 5 ] && break
  done
  return 1
}

and modifying the pipeline accordingly:

printf '%s\n'  1 2 3 4 5 6 7 8 9 |
while read -r num
do
  echo "$num" > /dev/stderr
  echo "$num"
done |
g

(and then calling set +o pipefail afterwards), but that also fails to achieve what I want.

Apparently there is some fundamental error in my understanding of how pipelines and buffering work. Please explain to me what I am missing and how I can get the output I want.

6
  • 1
    Remember that all commands in a pipe are launched in parallel so the second command doesn't "run until the printf command stops sending data", it runs until it receives the break. If you try with something like printf "%s\n" {1..50000} so the first command will take longer to finish, you will see it is stopped long before we get to 50000. Does that help?
    – terdon
    Commented Jun 4 at 10:19
  • 1
    The size of the buffer is settable via setvbuf(3). As long as the stream does not refer to the interactive device (it doesn't in this case), it is fully buffered. Recent similar question: unix.stackexchange.com/q/772640/367454 Commented Jun 4 at 12:36
  • 1
    On a side note, one can end the line with a pipe followed by a newline, as that constitutes an incomplete pipeline. If entered interactively, the shell will wait for the next command in the pipeline. In a script, a line ending with a pipe and a newline won't be considered an error. All the examples in this question would be better formatted that way. Commented Jun 4 at 12:43
  • 1
    that linked article says "This happens because there is a buffer in the pipe, or specifically in the standard streams.", and that mixes up two separate things: the buffers inside the program (which setbuf might affect), and the buffers in the pipe within the OS (which it doesn't). Also it misses that stderr customarily isn't buffered.
    – ilkkachu
    Commented Jun 4 at 20:39
  • 1
    anyway, the usual process would be that after the last process in the pipe exits, the earlier processes in the pipe eventually get SIGPIPE or EPIPE from writing to the now-readerless pipe, causing them to also exit. The fact that it doesn't happen immediately doesn't usually matter, since most of the time, one only pulls output out of the end of the pipeline... So, really, I do wonder, what it is you're actually doing here? And if there might be some other way to do it. Or you could just pipe all the output to something that would delete all lines after the 50.
    – ilkkachu
    Commented Jun 4 at 20:43

2 Answers 2

0

The problem (as, I believe, you at least partly understand) is that the first loop runs all the way through 1 to 9, filling the pipe buffer, before the second loop gets to 5.  If you add sleep 1 (pause for one second) to the bottom of the first loop, not only will you get better sequencing of the output (1, 10, 2, 20, 3, 30, ...), but you will give the second loop a chance to finish before the first loop writes 6.

You can probably give sleep a fractional argument (e.g., sleep 0.5).

2
  • Adding sleep 1 to the first loop does make the pipeline do what I want. I assumed that setting the buffers to 0 would force the first loop to wait for the second one. Perhaps I have misunderstood stdbuf. Is it not setting the buffers for all the processes in the pipeline?
    – fuumind
    Commented Jun 4 at 17:28
  • 2
    @fuumind stdbuf may (or may not) change in what chunks data leaves or enters the program. It does not affect the pipe buffer that exists between programs in a pipe. Commented Jun 4 at 18:07
0

One could modify the pipeline into the following:

printf '%s\n'  1 2 3 4 5 6 7 8 9 |
while read -r num
do
  echo "$num" > /dev/stderr
  echo "$num" | cat -u || break
done |
while read -r num
do
  echo $(( $num * 10 ))
  [ "$num" -eq 5 ] && break
done

That would make the pipeline print the numbers in order. It would also make the first loop notice that the second one no longer exists when it reaches its sixth iteration so it will still print the number 6 after the number 50 though.

The option -u makes cat it operate without buffers. If you're using GNU cat then the -u option is not needed since GNU cat never uses buffers and simply ignores the option, according to the man page.

This does not modify the pipe buffers, which exist between the programs.

You must log in to answer this question.

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