【问题标题】:A variable modified inside a while loop is not remembered不会记住在 while 循环内修改的变量
【发布时间】:2013-05-27 02:20:36
【问题描述】:

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

【问题讨论】:

标签: bash while-loop scope sh


【解决方案1】:
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")"

您可以通过在分配lines 时立即展开反斜杠序列来摆脱上面字符串中相当丑陋的echo。此处可以使用$'...' 形式的引用:

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

【讨论】:

  • 最好把&lt;&lt;&lt; "$(echo -e "$lines")"改成简单的&lt;&lt;&lt; "$lines"
  • 如果来源来自tail -f而不是固定文本怎么办?
  • @mteee 你可以使用while read -r line; do echo "LINE: $line"; done &lt; &lt;(tail -f file)(显然循环不会终止,因为它会继续等待来自tail的输入)。
  • 我仅限于 /bin/sh - 有没有在旧 shell 中工作的替代方法?
  • @AvinashYadav 问题与while 循环或for 循环无关;而是使用子shell,即cmd1 | cmd2cmd2 在子shell 中。因此,如果在子shell 中执行for 循环,则会出现意外/有问题的行为。
【解决方案2】:

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

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

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

【讨论】:

  • here-doc 的 +1,因为 lines 变量的唯一目的似乎是提供 while 循环。
  • @chepner:谢谢!我又加了一个,献给你!
  • 给出了另一个解决方案here: for line in $(echo -e $lines); do ... done
  • @dma_k 感谢您的评论!此解决方案将导致 6 行包含一个单词。 OP 的要求不同...
  • 赞成。在此处的子外壳中运行 echo 是为数不多的在 ash 中工作的解决方案之一
【解决方案3】:

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

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

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

管道的每个元素,甚至是内置函数或 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="$1" B="$2" C="$3" D="$4"

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

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

【讨论】:

  • 你忘记了家仇的问题。有时很难找到与写答案的人相同的单词组合,这样您既不会被错误的结果淹没,也不会过滤掉所说的答案。
【解决方案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.

【讨论】:

  • 也许这里的表面之下有一个不错的答案,但你已经将格式扼杀到了尝试和阅读不愉快的地步。
  • 你的意思是原始代码读起来很舒服? (我刚刚关注了:p)
【解决方案5】:

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

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

将脚本的第一行更改为:

#!/bin/bash

#!/bin/ksh

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

【讨论】:

    【解决方案6】:

    这是一个有趣的问题,涉及到 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和子shell通过echo命令进行通信。您可以为父 shell 选择一些易于解析的文本。这种方法并没有打破你正常的思维方式,只是你需要做一些后期处理。为此,您可以使用 grep、sed、awk 等。

    【讨论】:

      【解决方案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
      

      【讨论】:

      • 非常有用。您可以在 while 循环中有多个变量,并将每个变量回显到不同的数字 echo $i &gt; 2; echo $j &gt; 3。然后你可以在while循环read -r i &lt; 2; read -r j &lt; 3之后将它们中的每一个重定向回全局变量。
      【解决方案8】:

      虽然这是一个老问题并且被问了好几次,但这就是我在下班后对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)"
      

      【讨论】:

      • 你试过了吗? xxx=0; while true; do if true; then xxx=100 ; fi; break; done; echo $xxx
      猜你喜欢
      • 2021-08-29
      • 2014-02-19
      • 1970-01-01
      • 2019-08-01
      • 2013-09-13
      • 2022-07-07
      • 1970-01-01
      • 2020-10-10
      • 1970-01-01
      相关资源
      最近更新 更多