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


186

在下面的程序中,如果我$foo在第一条if语句中将变量设置为值1 ,则它的工作方式是在if语句之后记住其值。但是,当我将相同的变量设置ifwhile语句内部的值2时,在while循环后将其忘记。就像我$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)


shellcheck实用程序捕获了这一点(请参阅github.com/koalaman/shellcheck/wiki/SC2030);在shellcheck.net中剪切并粘贴上面的代码会 为第19行发出此反馈:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Answers:


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

您可以echo通过在分配时立即扩展反斜杠序列来摆脱上面here字符串中的难看之处lines。该$'...'报价的形式可以有使用:

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

20
更好地更改<<< "$(echo -e "$lines")"为简单<<< "$lines"
相信

如果源来自tail -f而不是固定文本怎么办?
mt eee

2
@mteee您可以使用while read -r line; do echo "LINE: $line"; done < <(tail -f file)(很明显,循环不会继续,因为它继续等待来自的输入tail)。
PP

我仅限于/ bin / sh-在旧的shell中还有其他可行的方法吗?
user9645

1
@AvinashYadav问题实际上与while循环或for循环无关;它与循环无关。而使用的子shell即cmd1 | cmd2cmd2在一个子shell。因此,如果for在子shell中执行循环,则将显示意外/问题行为。
PP

48

UPDATED#2

蓝月亮的答案中有解释。

替代解决方案:

消除 echo

while read line; do
...
done <<EOT
first line
second line
third line
EOT

在此处文档中添加回显

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

此处文档+1,因为该lines变量的唯一目的似乎是为了馈入while循环。
13年

@chepner:谢谢!我添加了另一个,献给您!
TrueY

由于目前尚未给出另一种解决方案在这里for line in $(echo -e $lines); do ... done
dma_k

@dma_k感谢您的评论!该解决方案将导致6行包含一个单词。OP的要求有所不同...
TrueY

赞成。在其中的一个子外壳中运行回声是少数在灰中工作的解决方案之一
-Hamy

9

您是742342nd个用户,要求此bash FAQ。答案还描述了在管道创建的子外壳中设置变量的一般情况:

E4)如果我将命令的输出传递到read variable,为什么$variable读命令结束后输出没有显示?

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

管道的每个元素(甚至是内置函数或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))

7
您忘记了家庭纠纷问题。有时很难找到与撰写答案的人相同的单词组合,以使您既不会陷入错误的结果,也不会过滤掉答案。
Evi1M4chine

3

嗯...我几乎发誓这对原始的Bourne外壳有用,但是暂时无法访问正在运行的副本。

但是,对于此问题有非常简单的解决方法。

从以下位置更改脚本的第一行:

#!/bin/bash

#!/bin/ksh

瞧!假设您已安装Korn shell,则在管道末尾进行读取就可以了。


0

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

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

6
也许在这里表面上有一个不错的答案,但是您已经挑剔了格式,以至于很难尝试阅读。
Mark Amery

您的意思是原始代码易于阅读?(我刚刚遵循:p)
Marcin

0

这是一个有趣的问题,涉及Bourne shell和subshel​​l中一个非常基本的概念。在这里,我通过进行某种过滤提供了一种与以前的解决方案不同的解决方案。我将举一个在现实生活中可能有用的例子。这是一个片段,用于检查下载的文件是否符合已知的校验和。校验和文件如下所示(仅显示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通过echo命令进行通信。您可以为父shell选择一些易于解析的文本。这种方法不会破坏您通常的思维方式,只是您必须进行一些后期处理。您可以使用grep,sed,awk等来实现。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.