tags:

views:

161

answers:

6
+1  Q: 

Linux shell bug?

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.

+4  A: 

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
I'm not sure about that. If there was a '(' ... ')' around the while you'd definitely be correct.
Stephen C
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
would `export FILE_FOUND=1` help?
PP
no http://stackoverflow.com/questions/496702/can-a-shell-script-set-environment-variables-of-the-calling-shell
pixelbeat
Thank you all. This completely answers my question!How do I mark it as solved?
ExpertNoob1
+2  A: 

This line:

ls -1 /tmp/$$.* 2>/dev/null | while read item; d0

should be:

ls -1 /tmp/$$.* 2>/dev/null | while read item; do

But that's not the root of the issue, because the script (only) works as described by the OP with this typo corrected.

The real answer is that 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!

Stephen C
it took me a while to spot the difference.
Amarghosh
I think this is a typo in the question. Bash would not even execute the file without the "do".
roe
@roe - you are correct. I think this is really a bash "feature", but I'm still researching.
Stephen C
@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).
roe
@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
+1  A: 

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
roe
A: 

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

Hope this helps, Best regards, Tom.

tommieb75
+2  A: 

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).

eduffy
Assuming that the user's shell understands **process substitution** (which is likely on linux)
glenn jackman
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.
roe
A: 

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"
Nope: you need *all* references to `$FILE_FOUND` *inside* the braces.
glenn jackman