user3694243
user3694243

Reputation: 244

An issue with while read loop

One thing about the while loop work.

I have following code:

f2(){ if [[ -t 0 ]] ; then echo "$*" ; else echo $(cat) ; fi ; }
echo -e 'a\nb\nc' | while read var ; do f2 $var ; done

What is interesting to me, is why the 'a' letter does not printed. However if i use the for loop, everything seem to work as expected. Like this:

for var in `echo -e 'a\nb\nc'`; do f2 $var ; done

It is nedded to preserve these echoes, because them are replacements for other scripts that give output on stdout, but them give the exactly same result. Some values that should be printed are missing.

Thank you all in advance.

Upvotes: 0

Views: 119

Answers (2)

rici
rici

Reputation: 241931

Let's take a careful look at this line:

echo -e 'a\nb\nc' | while read var ; do f2 $var ; done

That:

  • Creates a pipe
  • starts a subprocess to run the echo command, redirecting its stdout to the pipe
  • starts a subprocess to run the while command, redirecting its stdin to the pipe.

So let's reduce that to its essentials, using this useful tool:

readlink -f /dev/stdin

readlink -f /dev/stdin (on Linux, anyway) will tell us what stdin resolves to. In an interactive shell, it will show that stdin is a terminal:

$ readlink -f /dev/stdin
/dev/pts/14

Now, let's go back to the pipeline:

$ echo | readlink -f /dev/stdin
/proc/28050/fd/pipe:[608866]

As expected, stdin is a pipe.

Now, the actual right-hand side of the pipe was:

| while read var ; do f2 $var ; done

where f2 starts by checking if [[ -t 0 ]]: in other words, f2 checks to see if stdin is a terminal. We can see that stdin is not a terminal, so f2 will proceed to the else clause:

echo $(cat)

which prints the rest of stdin, replacing line-endings with whitespace. The stdin redirect was originally three lines, a, b and c, but the read already read the first line, so what is left are the b and the c, and that's what cat will return.

Needless to say, the logic is wrong. Why would you want f2 to check whether its input had been redirected?

More normal might be to check if there are any arguments: if (($#)); then. But since I have no idea what you are trying to do, I can't provide a definitive solution.

Although in this case it would probably be better to fix f2, it is possible to avoid using stdin for the while read loop by using process substitution and a different file descriptor:

while read -u3 var ; do f2 $var ; done 3< <(echo $'a\nb\nc')

Here, 3< causes a redirect of file descriptor 3 (an unused file descriptor) and read -u3 tells read to read from fd 3 instead of stdin. <(command) is process substitution, which creates a name which can be used to read the output of the command.

Upvotes: 3

dgeorgiev
dgeorgiev

Reputation: 941

I believe 'a' gets into var, and the rest of the stdin gets consumed/processed by cat.

Perhaps the following example can help visualize this:

In this example, only 'read' consumes the stdin.

echo -e 'a\nb\nc' | while read var ; do echo Iterated $var ; done
Iterated a
Iterated b
Iterated c

With cat, read consumes the first portion of the stdin, and the rest gets consumed by cat.

$ echo -e 'a\nb\nc' | while read var ; do echo $(cat); echo Iterated $var ; done
b c
Iterated a

Upvotes: 0

Related Questions