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

A variable modified inside a while loop is not remembered

提问人:Eric Lilja 提问时间:5/31/2013 最后编辑:TradingDerivatives.euEric Lilja 更新时间:10/2/2022 访问量:255567

问:

在下面的程序中,如果我在第一个语句中将变量设置为值 1,则它的工作原理是它的值在 if 语句之后被记住。但是,当我将相同的变量设置为语句内的值 2 时,它会在循环后被遗忘。它的行为就像我在循环中使用变量的某种副本,并且我只修改了该特定副本。下面是一个完整的测试程序:$fooififwhilewhile$foowhile

#!/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)
bash while-loop 作用域 sh

评论

2赞 fedorqui 12/11/2015
这里的关键阅读:我在管道中的循环中设置变量。为什么它们在循环终止后会消失?或者,为什么我不能通过管道读取数据?
0赞 qneill 6/22/2018
shellcheck 实用程序捕获此问题(参见 github.com/koalaman/shellcheck/wiki/SC2030);剪切并粘贴上述代码 shellcheck.net 会针对第 19 行发布以下反馈:SC2030: Modification of foo is local (to subshell caused by pipeline).

答:

343赞 P.P 5/31/2013 #1
echo -e $lines | while read line 
    ...
done

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

相反,您可以使用 here 字符串将 while 循环重写为在主 shell 进程中;只会在子 shell 中运行: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 -e "$lines")"

您可以通过在分配时立即展开反斜杠序列来摆脱上面 here-string 中相当丑陋的内容。引用的形式可以在那里使用:echolines$'...'

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

评论

31赞 beliy 5/18/2017
更好地更改为简单<<< "$(echo -e "$lines")"<<< "$lines"
0赞 mt eee 11/9/2018
如果来源是来自而不是固定文本怎么办?tail -f
2赞 P.P 11/11/2018
@mteee 您可以使用(显然循环不会终止,因为它会继续等待来自 的输入)。while read -r line; do echo "LINE: $line"; done < <(tail -f file)tail
4赞 P.P 1/16/2020
@AvinashYadav 问题实际上与循环或循环无关;相反,subshell 的使用,即 in ,是在子壳中。因此,如果在子 shell 中执行循环,则将表现出意外/有问题的行为。whileforcmd1 | cmd2cmd2for
1赞 Navin Gelot 12/1/2021
使用上述语法“语法错误:重定向意外”时出现此错误。有没有其他解决方案?
60赞 TrueY 5/31/2013 #2

更新#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)

一个用于(消除):chepnerecho

arr=("first line" "second line" "third line");
for((i=0;i<${#arr[*]};++i)) { line=${arr[i]}; 
...
}

变量可以转换为数组,而无需启动新的子 shell。字符 和 必须转换为某个字符(例如,真正的换行符)并使用 IFS(内部字段分隔符)变量将字符串拆分为数组元素。这可以通过以下方式完成:$lines\n

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

评论

0赞 chepner 5/31/2013
+1 表示 here-doc,因为变量的唯一目的似乎是馈送循环。lineswhile
0赞 TrueY 5/31/2013
@chepner:谢谢!我又加了一个,献给你!
0赞 dma_k 1/20/2016
这里给出了另一种解决方案:for line in $(echo -e $lines); do ... done
0赞 TrueY 1/20/2016
@dma_k 感谢您的评论!此解决方案将产生 6 行包含一个单词。OP的要求是不同的......
0赞 Hamy 11/26/2016
投了赞成票。在 Here-is 内部的子外壳中运行 echo 是少数几个在 Ash 中有效的解决方案之一
10赞 Jens 6/1/2013 #3

你问的是这个 bash 常见问题解答。答案还描述了在管道创建的子壳中设置变量的一般情况:

E4) 如果我将命令的输出通过管道传递到 read 变量中,为什么 当读取命令完成时,输出不会以$variable显示吗?

这与Unix之间的父子关系有关 过程。它会影响管道中运行的所有命令,而不仅仅是 对 的简单调用。例如,通过管道传输命令的输出 进入一个循环,重复调用将导致 相同的行为。readwhileread

流水线的每个元素,甚至是内置函数或 shell 函数, 在单独的进程中运行,shell 的子进程运行 管道。子进程不能影响其父级的环境。 当命令将变量设置为输入时,该 变量仅在子 shell 中设置,而不是在父 shell 中设置。什么时候 子 shell 退出,变量的值丢失。read

许多以 转换为命令替换,这将捕获 指定的命令。然后可以将输出分配给 变量:read variable

grep ^gnu /usr/lib/news/active | wc -l | read ngroup

可以转换为

ngroup=$(grep ^gnu /usr/lib/news/active | wc -l)

不幸的是,这并不能将文本拆分到 多个变量,就像给定多个变量时读取一样 参数。如果需要这样做,可以使用 上面的命令替换,将输出读入变量 并使用 bash 模式删除来切碎变量 扩展运算符或使用以下变体 方法。

假设 /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"

但是要注意,这会改变外壳的位置 参数。如果你需要它们,你应该在做之前保存它们 这。

这是一般方法 - 在大多数情况下,您不需要 将$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))

评论

12赞 1/30/2016
你忘记了家庭不和的问题。有时很难找到与写答案的人相同的单词组合,这样你就不会被错误的结果淹没,也不会过滤掉所说的答案。
0赞 Graham Leggett 10/2/2022
链接标记为“此文档不再维护”。
0赞 JRFerguson 10/2/2022
@GrahamLeggett 链路已更换。
0赞 Marcin 8/30/2014 #4

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

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

评论

7赞 Mark Amery 9/11/2016
也许在表面之下有一个不错的答案,但你已经把格式屠杀到了尝试阅读不愉快的地步。
0赞 Marcin 8/11/2017
你的意思是原始代码读起来很愉快?(我刚刚关注:p)
4赞 Jonathan Quist 10/21/2014 #5

嗯......我几乎发誓这适用于原始的 Bourne shell,但现在无法访问正在运行的副本进行检查。

但是,有一个非常微不足道的解决方法。

将脚本的第一行更改为:

#!/bin/bash

#!/bin/ksh

瞧!假设您安装了 Korn shell,则在管道末尾进行读取工作正常。

评论

0赞 Vej 5/15/2022
ZSH 也可以使用。
1赞 Kemin Zhou 3/7/2018 #6

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

父级和子级通过 echo 命令进行通信。您可以为父 shell 选择一些易于解析的文本。这种方法不会破坏你正常的思维方式,只是你必须做一些后期处理。您可以使用 grep、sed、awk 等来执行此操作。

2赞 bugdot 7/16/2020 #7

我使用 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

评论

1赞 James L. 11/6/2021
非常有用。您可以在 while 循环中设置多个变量,并将每个变量回显为不同的数字。然后,您可以在 while 循环之后将它们中的每一个重定向回全局变量。echo $i > 2; echo $j > 3read -r i < 2; read -r j < 3
0赞 Ajay Singh 11/12/2020 #8

虽然这是一个老问题并且被问过好几次,但这是我在几个小时后对字符串坐立不安后所做的事情,唯一对我有用的选择是在 while 循环子 shell 期间将值存储在文件中,然后检索它。简单。here

使用语句存储,使用语句检索。bash 用户必须具有目录或具有读写访问权限。echocatchownchmod

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

评论

1赞 Nik O'Lai 8/11/2021
你试过这个吗?xxx=0; while true; do if true; then xxx=100 ; fi; break; done; echo $xxx