在while循环中修改的变量不会被记住

在下面的程序中,如果我将变量$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)
233056 次浏览
echo -e $lines | while read line
...
done

while循环在子shell中执行。因此,一旦子shell退出,您对变量所做的任何更改都将不可用。

相反,你可以使用< em >这里字符串< / em >重写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")"

你可以通过在赋值lines时立即展开反斜杠序列来摆脱上面here-string中相当难看的echo。可以在这里使用$'...'形式的引用:

lines=$'first line\nsecond line\nthird line'
while read line; do
...
done <<< "$lines"

更新# 2

蓝月亮的回答中有解释。

替代方案:

消除echo

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(消除echo):

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

答案还描述了由管道创建的子shell中设置的变量的一般情况:

E4) 如果我管道输出的命令到read variable,为什么 当读取命令完成时,输出不显示在$variable中吗?< / em > < / p > 这与Unix之间的父子关系有关 流程。它影响在管道中运行的所有命令,而不仅仅是 简单调用read。例如,通过管道输出命令的输出 while循环重复调用read将导致

管道的每个元素,甚至是内置或shell函数, 在单独的进程中运行,该进程是运行 管道。子进程不能影响其父进程的环境。 当read命令将变量设置为输入时,即 变量只在子shell中设置,而不是在父shell中设置。当

. subshell退出,变量值丢失
许多以read variable结尾的管道可以被转换 的输出,这将捕获 指定的命令。然后可以将输出分配给a 变量:< / p >
grep ^gnu /usr/lib/news/active | wc -l | read ngroup

可以转换成

ngroup=$(grep ^gnu /usr/lib/news/active | wc -l)
不幸的是,这不能将文本拆分 多个变量,就像read在给定多个变量时所做的那样 参数。如果需要这样做,可以使用 将输出读入变量 并使用bash模式删除来分解变量 展开运算符或使用以下的一些变体 方法。< / p >

输入/usr/local/bin/ipaddr是以下shell脚本:

#! /bin/sh
host `hostname` | awk '/address/ {print $NF}'

而不是使用

/usr/local/bin/ipaddr | read A B C D

将本地机器的IP地址分解为单独的八字节,使用

OIFS="$IFS"
IFS=.
set -- $(/usr/local/bin/ipaddr)
IFS="$OIFS"
A="$1" B="$2" C="$3" D="$4"
但是要注意,这将改变shell的位置 参数。如果你需要它们,你应该保存它们 这个。< / p >
这是一般的方法——在大多数情况下你不需要这样做

.设置$IFS为另一个值

其他一些用户提供的替代方案包括:

read A B C D << HERE
$(IFS=.; echo $(/usr/local/bin/ipaddr))
HERE

而且,如果可以进行工艺替换,

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))

用一个非常简单的方法怎么样

    +call your while loop in a function
- set your value inside (nonsense, but shows the example)
- return your value inside
+capture your value outside
+set outside
+display outside




#!/bin/bash
# set -e
# set -u
# No idea why you need this, not using here


foo=0
bar="hello"


if [[ "$bar" == "hello" ]]
then
foo=1
echo "Setting  \$foo to $foo"
fi


echo "Variable \$foo after if statement: $foo"


lines="first line\nsecond line\nthird line"


function my_while_loop
{


echo -e $lines | while read line
do
if [[ "$line" == "second line" ]]
then
foo=2; return 2;
echo "Variable \$foo updated to $foo inside if inside while loop"
fi


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"
return 2;
fi


# Code below won't be executed since we returned from function in 'if' statement
# We aready reported the $foo var beint set to 2 anyway
echo "Value of \$foo in while loop body: $foo"


done
}


my_while_loop; foo="$?"


echo "Variable \$foo after while loop: $foo"




Output:
Setting  $foo 1
Variable $foo after if statement: 1
Value of $foo in while loop body: 1
Variable $foo after while loop: 2


bash --version


GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
Copyright (C) 2007 Free Software Foundation, Inc.

嗯…我几乎发誓,这是为原来的伯恩壳工作,但没有访问到一个运行的副本,只是现在检查。

然而,对于这个问题,有一个非常琐碎的变通办法。

将脚本的第一行更改为:

#!/bin/bash

#!/bin/ksh

果不其然!如果您已经安装了Korn shell,那么在管道的末尾读取就可以了。

这是一个有趣的问题,涉及到伯恩壳层和亚壳层的一个非常基本的概念。在这里,我通过进行某种过滤提供了一个不同于前面的解决方案的解决方案。我将举一个在现实生活中可能有用的例子。这是一个片段,用于检查下载的文件是否符合已知的校验和。校验和文件如下所示(仅显示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等工具。

我使用stderr存储在一个循环中,并从它外面读取。 这里var i最初被设置并在循环中读取为1.

# reading lines of content from 2 files concatenated
# inside loop: write value of var i to stderr (before iteration)
# outside: read var i from stderr, has last iterative value


f=/tmp/file1
g=/tmp/file2
i=1
cat $f $g | \
while read -r s;
do
echo $s > /dev/null;  # some work
echo $i > 2
let i++
done;
read -r i < 2
echo $i
或使用heredoc方法减少子shell中的代码量。 注意迭代的i值可以在while循环之外读取
i=1
while read -r s;
do
echo $s > /dev/null
let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i

虽然这是一个老问题,被问了几次,这是我在摆弄here字符串几个小时后所做的事情,对我来说唯一有效的选择是在while循环子shell期间将值存储在文件中,然后检索它。简单。

使用echo语句进行存储,使用cat语句进行检索。bash用户必须chown目录或具有读写chmod权限。

#write to file
echo "1" > foo.txt


while condition; do
if (condition); then
#write again to file
echo "2" > foo.txt
fi
done


#read from file
echo "Value of \$foo in while loop body: $(cat foo.txt)"