3

How come FILE_FOUND is 0 at the end of this bugger :

FILE_FOUND=0

touch /tmp/$$.txt

ls -1 /tmp/$$.* 2>/dev/null | while read item; do
    FILE_FOUND=1
    echo "FILE_FOUND = $FILE_FOUND"
done

echo "FILE_FOUND = $FILE_FOUND"

rm -f /tmp/$$.txt 2>/dev/null

??!!

On Unix FILE_FOUND stays at 1 (as it should), but on Linux (RedHat, Cygwin, ..) it jumps back to 0!!

Is it a Linux shell feature, not a bug? :)

Please help.

Jens
  • 69,818
  • 15
  • 125
  • 179
ExpertNoob1
  • 900
  • 1
  • 8
  • 17

6 Answers6

5

common issue which is caused because you're piping into the while, which is therefore run in a subshell, which can't pass environment variables back to its parent. I'm guessing that "unix" is different in this regard as you're running a different shell there (ksh?)

piping to the while loop may not be required. could you use this idiom instead?

for item in /tmp/$$.*; do
    ....
done

If you must use a subshell, then you'll have to do something external to the processes like:

touch /tmp/file_found
pixelbeat
  • 30,615
  • 9
  • 51
  • 60
  • I'm not sure about that. If there was a '(' ... ')' around the while you'd definitely be correct. – Stephen C Nov 25 '09 at 13:45
  • 2
    http://mywiki.wooledge.org/BashFAQ/024 - "The reason for this surprising behaviour is that a while/for/until loop runs in a SubShell when it's part of a pipeline." and "Different shells behave differently when using redirection or pipes with a loop" – Dennis Williamson Nov 25 '09 at 13:48
  • would `export FILE_FOUND=1` help? – PP. Nov 25 '09 at 13:51
  • 2
    no http://stackoverflow.com/questions/496702/can-a-shell-script-set-environment-variables-of-the-calling-shell – pixelbeat Nov 25 '09 at 13:56
3

This is a "feature" of the "bash" shell. The "bash" manual entry states this clearly:

Each command in a pipeline is executed as a separate process (i.e., in a subshell).

The same construct executed with the Korn shell ("ksh") runs the "while" in the same process (not in a subshell) and hence gives the expected answer. So I checked the spec.

The POSIX shell specification is not crystal clear on this, but its does not say anything about changing the "shell execution environment", so I think that the UNIX / Korn shell implementations are compliant and the Bourne Again shell implementation is not. But then, "bash" does not claim to be POSIX compliant!

Lesmana
  • 25,663
  • 9
  • 82
  • 87
Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • it took me a while to spot the difference. – Amarghosh Nov 25 '09 at 13:47
  • I think this is a typo in the question. Bash would not even execute the file without the "do". – falstro Nov 25 '09 at 13:51
  • @roe - you are correct. I think this is really a bash "feature", but I'm still researching. – Stephen C Nov 25 '09 at 13:53
  • @Stephen C: that might be, however, it wouldn't "jump back to 0" on linux, because that line would never be executed, so it's just a typo in the question (o is right next to 0 on the keyboard). – falstro Nov 25 '09 at 13:55
  • @roe - actually, it WOULD jump back, because the variable is set and displayed (the first time) in the subshell. That's what the quoted sentence above means!! – Stephen C Nov 26 '09 at 03:38
2

It has already been mentioned, but since you're piping into the while, the entire while-loop is run in a subshell. I'm not exactly sure which shell you're using on 'Unix', which I suppose means Solaris, but bash should behave consistenly regardless of platform.

To solve this problem, you can do a lot of things, the most common is to examin the result of the while loop somehow, like so

result=`mycommand 2>/dev/null | while read item; do echo "FILE_FOUND"; done`

and look for data in $result. Another common approach is to have the while loop produce valid variable assignments, and eval it directly.

eval `mycommand | while read item; do echo "FILE_FOUND=1"; done`

which will be evaluated by your 'current' shell to assign the given variables.

I'm assuming you don't want to just iterate over files, in which case you should be doing

for item in /tmp/$$.*; do
  # whatever you want to do
done
falstro
  • 34,597
  • 9
  • 72
  • 86
2

As others have mentioned, it's the extra shell you're creating by using the pipe notation. Try this:

while read item; do
    FILE_FOUND=1
    echo "FILE_FOUND = $FILE_FOUND"
done < <(ls -1 /tmp/$$.* 2>/dev/null)

In this version, the while loop is in your script's shell, while the ls is a new shell (the opposite of what your script is doing).

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
eduffy
  • 39,140
  • 13
  • 95
  • 92
  • Assuming that the user's shell understands **process substitution** (which is likely on linux) – glenn jackman Nov 25 '09 at 14:18
  • did you actually try this? it feels like bash will put the while loop in a subshell to get the piping correct here as well. – falstro Nov 25 '09 at 15:08
0

another way , bring the result back,

FILE_FOUND=0
result=$(echo "something" | while read item; do
    FILE_FOUND=1
    echo "$FILE_FOUND"

done )
echo "FILE_FOUND outside while = $result"
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
-2

Ummmm... ls -1 not l is on your script?

t0mm13b
  • 34,087
  • 8
  • 78
  • 110