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

时间:2013-05-31 09:38:25

标签: bash while-loop scope sh

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

#!/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)

8 个答案:

答案 0 :(得分:192)

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

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

相反,您可以使用here string重写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-string中相当丑陋的lines$'...'引用形式可以在那里使用:

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

答案 1 :(得分:45)

<强>已更新#2

Blue Moons的回答是解释。

替代解决方案:

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

答案 2 :(得分:9)

您是第742342位用户询问此bash FAQ.该答案还描述了由管道创建的子shell中设置的变量的一般情况:

  

E4)如果我将命令的输出传递给read variable,为什么呢   当读取命令完成时,输出是否显示在$variable中?

     

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

     

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

     

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

read variable
     

可以转换为

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

不幸的是,这并没有将文本分开   多个变量,当给定多个变量时读取   参数。如果你需要这样做,你可以使用   上面的命令替换将输出读入变量   并使用bash模式删除来删除变量   扩展运营商或使用以下某些变体   方法

     

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

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

而不是使用

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

将本地计算机的IP地址分成单独的八位字节,使用

/usr/local/bin/ipaddr | read A B C D
     但是,请注意,这会改变shell的位置   参数。如果你需要它们,你应该在做之前保存它们   此

     

这是一般方法 - 在大多数情况下,您不需要   将$ IFS设置为不同的值。

     

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

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

,并且,在可用的过程替换的情况下,

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

答案 3 :(得分:1)

嗯......我几乎发誓这对原版Bourne shell有效,但是现在无法访问正在运行的副本进行检查。

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

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

#!/bin/bash

#!/bin/ksh

瞧瞧!假设您安装了Korn shell,那么管道末端的读取工作正常。

答案 4 :(得分:1)

我使用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中的代码量。 请注意,可以在while循环之外读取迭代i值。

i=1
while read -r s;
do
  echo $s > /dev/null
  let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i

答案 5 :(得分: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等来执行此操作。

答案 6 :(得分:0)

尽管这是一个古老的问题,曾被问过几次,但这是我在用here字符串烦躁几个小时后正在做什么,而对我有用的唯一选择是在while循环期间将值存储在文件中子外壳,然后将其检索。很简单。

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

答案 7 :(得分:-1)

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

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