在下面的程序中,如果我将变量$foo设置为第一个if语句中的值1,它的工作原理是它的值被记住在if语句之后。然而,当我在while语句中的if语句中将相同的变量设置为值2时,在while循环之后就会被忘记。它的行为就像我在while循环中使用了变量$foo的某种副本,并且我只修改了这个特定的副本。下面是一个完整的测试程序:
#!/bin/bash
set -e
set -u
foo=0
bar="hello"
if [[ "$bar" == "hello" ]]
then
foo=1
echo "Setting \$foo to 1: $foo"
fi
echo "Variable \$foo after if statement: $foo"
lines="first line\nsecond line\nthird line"
echo -e $lines | while read line
do
if [[ "$line" == "second line" ]]
then
foo=2
echo "Variable \$foo updated to $foo inside if inside while loop"
fi
echo "Value of \$foo in while loop body: $foo"
done
echo "Variable \$foo after while loop: $foo"
# Output:
# $ ./testbash.sh
# Setting $foo to 1: 1
# Variable $foo after if statement: 1
# Value of $foo in while loop body: 1
# Variable $foo updated to 2 inside if inside while loop
# Value of $foo in while loop body: 2
# Value of $foo in while loop body: 2
# Variable $foo after while loop: 1
# bash --version
# GNU bash, version 4.1.10(4)-release (i686-pc-cygwin)
更新# 2
蓝月亮的回答中有解释。
替代方案:
消除回声
while read line; do
...
done <<EOT
first line
second line
third line
EOT
在here-is-the-document中添加回显
while read line; do
...
done <<EOT
$(echo -e $lines)
EOT
在后台执行echo命令:
coproc echo -e $lines
while read -u ${COPROC[0]} line; do
...
done
显式重定向到文件句柄(注意< <!):
exec 3< <(echo -e $lines)
while read -u 3 line; do
...
done
或者直接重定向到stdin:
while read line; do
...
done < <(echo -e $lines)
chepner(消除回声):
arr=("first line" "second line" "third line");
for((i=0;i<${#arr[*]};++i)) { line=${arr[i]};
...
}
变量$lines可以在不启动新的子shell的情况下转换为数组。字符\和n必须转换为一些字符(例如,一个真正的新行字符),并使用IFS(内部字段分隔符)变量将字符串分割为数组元素。可以这样做:
lines="first line\nsecond line\nthird line"
echo "$lines"
OIFS="$IFS"
IFS=$'\n' arr=(${lines//\\n/$'\n'}) # Conversion
IFS="$OIFS"
echo "${arr[@]}", Length: ${#arr[*]}
set|grep ^arr
结果是
first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")
echo -e $lines | while read line
...
done
while循环在子shell中执行。因此,一旦子shell退出,您对变量所做的任何更改都将不可用。
相反,你可以使用一个here字符串来重写while循环,使其位于主shell进程中;只有echo -e $lines将在子shell中运行:
while read line
do
if [[ "$line" == "second line" ]]
then
foo=2
echo "Variable \$foo updated to $foo inside if inside while loop"
fi
echo "Value of \$foo in while loop body: $foo"
done <<< "$(echo -e "$lines")"
在分配行时,可以通过立即展开反斜杠序列来消除上面here-string中相当难看的echo。美元的…的引用形式可以用在这里:
lines=$'first line\nsecond line\nthird line'
while read line; do
...
done <<< "$lines"
这是一个有趣的问题,涉及到伯恩壳层和亚壳层的一个非常基本的概念。在这里,我通过进行某种过滤提供了一个不同于前面的解决方案的解决方案。我将举一个在现实生活中可能有用的例子。这是一个片段,用于检查下载的文件是否符合已知的校验和。校验和文件如下所示(仅显示3行):
49174 36326 dna_align_feature.txt.gz
54757 1 dna.txt.gz
55409 9971 exon_transcript.txt.gz
shell脚本:
#!/bin/sh
.....
failcnt=0 # this variable is only valid in the parent shell
#variable xx captures all the outputs from the while loop
xx=$(cat ${checkfile} | while read -r line; do
num1=$(echo $line | awk '{print $1}')
num2=$(echo $line | awk '{print $2}')
fname=$(echo $line | awk '{print $3}')
if [ -f "$fname" ]; then
res=$(sum $fname)
filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na == $1 && nb == $2) { print "TRUE"; } else { print "FALSE"; }}')
if [ "$filegood" = "FALSE" ]; then
failcnt=$(expr $failcnt + 1) # only in subshell
echo "$fname BAD $failcnt"
fi
fi
done | tail -1) # I am only interested in the final result
# you can capture a whole bunch of texts and do further filtering
failcnt=${xx#* BAD } # I am only interested in the number
# this variable is in the parent shell
echo failcnt $failcnt
if [ $failcnt -gt 0 ]; then
echo $failcnt files failed
else
echo download successful
fi
父shell和子shell之间通过echo命令进行通信。您可以为父shell选择一些易于解析的文本。这种方法并没有打破你正常的思维方式,只是你需要做一些后期处理。为此,您可以使用grep、sed、awk等工具。