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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16854280/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 23:47:10  来源:igfitidea点击:

A variable modified inside a while loop is not remembered

bashwhile-loopscopesh

提问by Eric Lilja

In the following program, if I set the variable $footo the value 1 inside the first ifstatement, it works in the sense that its value is remembered after the if statement. However, when I set the same variable to the value 2 inside an ifwhich is inside a whilestatement, it's forgotten after the whileloop. It's behaving like I'm using some sort of copy of the variable $fooinside the whileloop and I am modifying only that particular copy. Here's a complete test program:

在下面的程序中,如果我$foo在第一个if语句中将变量设置为值 1 ,它的工作原理是在 if 语句之后记住它的值。但是,当我ifwhile语句内的 an 中将相同的变量设置为值 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)

回答by P.P

echo -e $lines | while read line 
    ...
done

The whileloop is executed in a subshell. So any changes you do to the variable will not be available once the subshell exits.

while回路在子Shell执行。因此,一旦子外壳退出,您对变量所做的任何更改都将不可用。

Instead you can use a here stringto re-write the while loop to be in the main shell process; only echo -e $lineswill run in a subshell:

相反,您可以使用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")"

You can get rid of the rather ugly echoin the here-string above by expanding the backslash sequences immediately when assigning lines. The $'...'form of quoting can be used there:

您可以echo通过在分配lines. $'...'可以在那里使用引用的形式:

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

回答by TrueY

UPDATED#2

更新#2

Explanation is in Blue Moons's answer.

解释在 Blue Moons 的回答中。

Alternative solutions:

替代解决方案:

Eliminate echo

排除 echo

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

Add the echo inside the here-is-the-document

在 here-is-the-document 中添加 echo

while read line; do
...
done <<EOT
$(echo -e $lines)
EOT

Run echoin background:

echo在后台运行:

coproc echo -e $lines
while read -u ${COPROC[0]} line; do 
...
done

Redirect to a file handle explicitly (Mind the space in < <!):

显式重定向到文件句柄(注意< <! 中的空格):

exec 3< <(echo -e  $lines)
while read -u 3 line; do
...
done

Or just redirect to the stdin:

或者只是重定向到stdin

while read line; do
...
done < <(echo -e  $lines)

And one for chepner(eliminating echo):

一个用于chepner(消除echo):

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

Variable $linescan be converted to an array without starting a new sub-shell. The characters \and nhas to be converted to some character (e.g. a real new line character) and use the IFS (Internal Field Separator) variable to split the string into array elements. This can be done like:

变量$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

Result is

结果是

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")

回答by Jens

You are the 742342nd user to ask this bash FAQ.The answer also describes the general case of variables set in subshells created by pipes:

您是第 742342 位询问此bash 常见问题的用户答案还描述了在管道创建的子外壳中设置的变量的一般情况:

E4) If I pipe the output of a command into read variable, why doesn't the output show up in $variablewhen the read command finishes?

This has to do with the parent-child relationship between Unix processes. It affects all commands run in pipelines, not just simple calls to read. For example, piping a command's output into a whileloop that repeatedly calls readwill result in the same behavior.

Each element of a pipeline, even a builtin or shell function, runs in a separate process, a child of the shell running the pipeline. A subprocess cannot affect its parent's environment. When the readcommand sets the variable to the input, that variable is set only in the subshell, not the parent shell. When the subshell exits, the value of the variable is lost.

Many pipelines that end with read variablecan be converted into command substitutions, which will capture the output of a specified command. The output can then be assigned to a variable:

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

can be converted into

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

This does not, unfortunately, work to split the text among multiple variables, as read does when given multiple variable arguments. If you need to do this, you can either use the command substitution above to read the output into a variable and chop up the variable using the bash pattern removal expansion operators or use some variant of the following approach.

Say /usr/local/bin/ipaddr is the following shell script:

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

Instead of using

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

to break the local machine's IP address into separate octets, use

OIFS="$IFS"
IFS=.
set -- $(/usr/local/bin/ipaddr)
IFS="$OIFS"
A="" B="" C="" D=""

Beware, however, that this will change the shell's positional parameters. If you need them, you should save them before doing this.

This is the general approach -- in most cases you will not need to set $IFS to a different value.

Some other user-supplied alternatives include:

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

and, where process substitution is available,

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

E4)如果我将命令的输出通过管道传输到read variable,为什么$variable在读取命令完成时输出不显示?

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

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

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

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

可以转换成

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

不幸的是,这不能像 read 在给定多个变量参数时那样在多个变量之间拆分文本。如果您需要这样做,您可以使用上面的命令替换将输出读入变量并使用 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="" B="" C="" D=""

但是请注意,这会改变外壳的位置参数。如果您需要它们,您应该在执行此操作之前保存它们。

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

回答by Jonathan Quist

Hmmm... I would almost swear that this worked for the original Bourne shell, but don't have access to a running copy just now to check.

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

There is, however, a very trivial workaround to the problem.

然而,这个问题有一个非常简单的解决方法。

Change the first line of the script from:

将脚本的第一行更改为:

#!/bin/bash

to

#!/bin/ksh

Et voila! A read at the end of a pipeline works just fine, assuming you have the Korn shell installed.

等等!假设您安装了 Korn shell,那么在管道末尾读取就可以正常工作。

回答by Kemin Zhou

This is an interesting question and touches on a very basic concept in Bourne shell and subshell. Here I provide a solution that is different from the previous solutions by doing some kind of filtering. I will give an example that may be useful in real life. This is a fragment for checking that downloaded files conform to a known checksum. The checksum file look like the following (Showing just 3 lines):

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

49174 36326 dna_align_feature.txt.gz
54757     1 dna.txt.gz
55409  9971 exon_transcript.txt.gz

The shell script:

外壳脚本:

#!/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 }')
    num2=$(echo $line | awk '{print }')
    fname=$(echo $line | awk '{print }')
    if [ -f "$fname" ]; then
        res=$(sum $fname)
        filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na ==  && nb == ) { 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

The parent and subshell communicate through the echo command. You can pick some easy to parse text for the parent shell. This method does not break your normal way of thinking, just that you have to do some post processing. You can use grep, sed, awk, and more for doing so.

父shell和子shell通过echo命令进行通信。您可以为父 shell 选择一些易于解析的文本。这种方法并没有打破你正常的思维方式,只是你要做一些后期处理。为此,您可以使用 grep、sed、awk 等。

回答by Marcin

How about a very simple method

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

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