Shell variables set inside while loop not visible outside of it
I am trying to find the pathname with the most characters in it. There might be better ways to do this. But I would like to know why this problem occurs.
LONGEST_CNT=0
find samples/ | while read line
do
line_length=$(echo $line | wc -m)
if [[ $line_length -gt $LONGES开发者_运维问答T_CNT ]]
then
LONGEST_CNT=$line_length
LONGEST_STR=$line
fi
done
echo $LONGEST_CNT : $LONGEST_STR
It somehow always returns:
0 :
If I print the results for debugging inside the while loop the values are correct. So why bash does not make these variables global?
When you pipe into a while
loop in Bash, it creates a subshell. When the subshell exits, all variables return to their previous values (which may be null or unset). This can be prevented by using process substitution.
LONGEST_CNT=0
while read -r line
do
line_length=${#line}
if (( line_length > LONGEST_CNT ))
then
LONGEST_CNT=$line_length
LONGEST_STR=$line
fi
done < <(find samples/ ) # process substitution
echo $LONGEST_CNT : $LONGEST_STR
The "correct" reply is given by Dennis. However, I find the process substitution trick extremely unreadable if the loop contains more than a few lines. When reading a script, I want to see what goes into the pipe before I see how it is processed.
So I usually prefer this trick of encapsulating the while loop in "{}".
LONGEST_CNT=0
find /usr/share/zoneinfo | \
{ while read -r line
do
line_length=${#line}
if (( line_length > LONGEST_CNT ))
then
LONGEST_CNT=$line_length
LONGEST_STR=$line
fi
done
echo $LONGEST_CNT : $LONGEST_STR
}
About finding the longest pathname. Here's an alternative:
find /usr/share/zoneinfo | while read line; do
echo ${#line} $line
done | sort -nr | head -n 1
# Result:
58 /usr/share/zoneinfo/right/America/Argentina/ComodRivadavia
Forgive me if this is considered off topic, I hope it helps someone.
Do what you always (should) do:
- separate concerns,
- avoid globals,
- document your code,
- be readable,
- maybe be POSIXy.
(Yes, I added a bit more "good practice" ingredients to the soup than absolutely necessary ;))
So my favorite "knee-jerk reaction" to problems with invisible subshell is to use function:
#!/bin/sh
longest() {
#
# Print length and body of the longest line in STDIN
#
local cur_ln # current line
local cur_sz # current size (line length)
local max_sz # greatest size so far
local winner # longest string so far
max_sz=0
while read -r cur_ln
do
cur_sz=${#cur_ln}
if test "$cur_sz" -gt "$max_sz";
then
max_sz=$cur_sz
winner=$cur_ln
fi
done
echo "$max_sz" : "$winner"
}
find /usr/share/zoneinfo | longest
# ok, if you really wish to use globals, here you go ;)
LONGEST_CNT=0
LONGEST_CNT=$(
find /usr/share/zoneinfo \
| longest \
| cut -d: -f1 \
| xargs echo\
)
echo "LONGEST_CNT='$LONGEST_CNT'"
Aside from avoiding the subshell annoyance, it gives you perfect place to document the code, and sort-of adds namespacing: notice that inside function you can use much shorter and simpler variable names without losing any of readability.
精彩评论