首页 文章

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

提问于
浏览
134

在下面的程序中,如果我在第一个 if 语句中将变量 $foo 设置为值1,那么它的作用就是在if语句之后记住它的值 . 但是,当我在 if 语句中的 if 内将相同的变量设置为值2时,在 while 循环之后它被遗忘了 . 它's behaving like I' m在 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)

6 回答

  • 175
    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")"
    
  • 9

    UPDATED#2

    Blue Moons的答案是解释 .

    替代方案:

    消除 echo

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

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

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

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

    E4)如果我将一个命令的输出传递给read变量,为什么当read命令完成时输出不显示在$ variable中?这与Unix进程之间的父子关系有关 . 它会影响在管道中运行的所有命令,而不仅仅是简单的读取调用 . 例如,将命令的输出传递给重复调用read的while循环将导致相同的行为 . 管道的每个元素,甚至是内置函数或shell函数,都在一个单独的进程中运行,这是运行管道的shell的子进程 . 子流程不会影响其父级环境 . 当read命令将变量设置为输入时,该变量仅在子shell中设置,而不是在父shell中设置 . 当子shell退出时,变量的值将丢失 . 许多以read变量结尾的管道都可以转换为命令替换,这将捕获指定命令的输出 . 然后可以将输出分配给变量:grep ^ gnu / usr / lib / news / active | wc -l |读ngroup
    可以转换为ngroup = $(grep ^ gnu / usr / lib / news / active | wc -l)
    遗憾的是,这并不能在多个变量之间拆分文本,因为在给定多个变量参数时读取了这些变量 . 如果需要这样做,可以使用上面的命令替换将输出读入变量并使用bash模式删除扩展运算符来切断变量,或者使用以下方法的某种变体 . 说/ usr / local / bin / ipaddr是以下shell脚本:#! / bin / sh的
    主机hostname | awk'/ address / {print $ NF}'
    而不是使用/ usr / local / bin / ipaddr |读A B C D.
    要将本地计算机的IP地址分成单独的八位字节,请使用OIFS =“$ IFS”
    IFS = .
    set - $(/ usr / local / bin / ipaddr)
    IFS = “$的OIF”
    A =“$ 1”B =“$ 2”C =“$ 3”D =“$ 4”
    但请注意,这将改变shell的位置参数 . 如果您需要它们,您应该在执行此操作之前保存它们 . 这是一般方法 - 在大多数情况下,您不需要将$ IFS设置为不同的值 . 其他一些用户提供的替代方案包括:读取A B C D << HERE
    $(IFS = . ; echo $(/ usr / local / bin / ipaddr))
    这里
    并且,在可用过程替换的情况下,读取A B C D <<(IFS = . ; echo $(/ usr / local / bin / ipaddr))

  • 0

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

    但是,这个问题有一个非常简单的解决方法 .

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

    #!/bin/bash
    

    #!/bin/ksh
    

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

  • -1

    这是一个有趣的问题,触及一个非常基本的概念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
    

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

  • 2

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

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

相关问题