是 while 循环还是管道导致全局变量表现异常

是 while 循环还是管道导致全局变量表现异常

请有人解释一下COUNTER以下代码中变量的奇怪行为(从我的观点来看)?

#!/bin/bash

COUNTER=0

function increment {
    ((COUNTER++))
}

function report {
    echo "COUNTER: $COUNTER ($1)"
}

function reset_counter {
    COUNTER=0
}

function increment_if_yes {
    answer=$1
    if [ "$answer" == "yes" ]
    then
        increment
    fi
}

function break_it {
    echo -e "maybe\nyes\nno" | \
    while read LL
    do
        increment_if_yes $LL
    done    
}

report start # counter should be 0
increment
report one
increment_if_yes yes
report two
increment_if_yes no
report "still two"

reset_counter
report reset

break_it
report "I'd expect one"

我希望COUNTER位于1脚本的末尾,但它是0

$ ./broken_variable.sh 
COUNTER: 0 (start)
COUNTER: 1 (one)
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
COUNTER: 0 (I'd expect one)

答案1

OP的当前代码在 中按预期工作ksh,并且它也可能在其他 shell 中工作,但不是bash......

导致循环breakit()在子进程内触发,这又意味着 while 循环的所有函数调用也在子进程内触发。echo ... | while ...while

子进程(while本例中为循环)接收一个复制变量的COUNTER,因此子进程所做的任何更改COUNTER仅适用于复制变量的。当子进程退出时,对复制COUNTER都丢失了。当控制权返回到父进程时,(原始)COUNTER变量的值与启动子进程之前的值相同。

要实现所需的行为,您需要确保while循环在父进程中运行。一种使用进程替换的方法:

while read LL
do
    increment_if_yes "$LL"
done < <( echo -e "maybe\nyes\nno" )

答案2

这个更简单的例子可能会有所帮助:

$ c=0
$ printf 'a\nb\nc\n' | while read i; do (( c++ )); echo "c is now $c"; done
c is now 1
c is now 2
c is now 3
$ echo "$c"
0

正如您所看到的,这再现了您在脚本中观察到的行为。原因是,因为您将数据通过管道传输到while,这意味着它将启动一个子 shell,该子 shell 继承所有父级变量的副本,但是当循环退出时,它不会将这些副本导出回父级。换句话说,您没有增加COUNTER变量,而是增加了变量的副本,该副本在循环结束后立即被销毁。

如果您尝试脚本的修改版本,您可以看到它的实际效果:

#!/bin/bash

COUNTER=0

function increment {
  echo "increment called"
    ((COUNTER++))
}

function report {
    echo "COUNTER: $COUNTER ($1)"
}

function reset_counter {
    COUNTER=0
}

function increment_if_yes {
    answer=$1
    if [ "$answer" == "yes" ]
    then
        increment
    fi
}

function break_it {
  echo "aa COUNTER at start of break_it: $COUNTER"
    echo -e "maybe\nyes\nno" | \
    while read LL
    do
        echo "bb COUNTER in loop top: $COUNTER"
        increment_if_yes $LL
        echo "bb COUNTER in loop bottom: $COUNTER"
    done
    echo "aa COUNTER at end of break_it: $COUNTER"
}

report start # counter should be 0
increment
report one
increment_if_yes yes
report two
increment_if_yes no
report "still two"

reset_counter
report reset

break_it
report "I'd expect one"

运行此打印:

COUNTER: 0 (start)
increment called
COUNTER: 1 (one)
increment called
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
aa COUNTER at start of break_it: 0
bb COUNTER in loop top: 0
bb COUNTER in loop bottom: 0
bb COUNTER in loop top: 0
increment called
bb COUNTER in loop bottom: 1
bb COUNTER in loop top: 1
bb COUNTER in loop bottom: 1
aa COUNTER at end of break_it: 0
COUNTER: 0 (I'd expect one)

请注意这些值如何bb COUNTER显示名为的变量在函数$COUNTER中递增break_it,只是这实际上是该变量的副本,而不是脚本其余部分中可用的变量。

最后,您可能需要通读 bash 手册的命令执行环境部分,特别是(强调我的):

当要执行除内置或 shell 函数之外的简单命令时,它在单独的执行环境中调用其中包括以下内容。除非另有说明,这些值都是从 shell 继承的。

  • shell 的打开文件,以及通过重定向到命令指定的任何修改和添加
  • 当前工作目录
  • 文件创建模式掩码
  • 标记为导出的 shell 变量和函数,以及为命令导出的变量,传递到环境中(请参阅环境)
  • shell 捕获的陷阱将重置为从 shell 的父级继承的值,并且 shell 忽略的陷阱将被忽略

在这个单独的环境中调用的命令不会影响 shell的执行环境。

最后一句话是问题的关键:在单独的环境中调用的命令不能影响父环境,这就是为什么您不能按照您想要的方式增加变量。

但是,您可以,因为您的 shell (bash) 支持流程替代,将您的功能更改为此,它将起作用:

 function break_it {
    while read LL
    do
        increment_if_yes $LL
    done < <(printf 'maybe\nyes\nno\n')
}

如果我现在运行您的原始脚本,但对break_it函数进行了如上所示的修改,我会得到:

$ foo.sh 
COUNTER: 0 (start)
COUNTER: 1 (one)
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
COUNTER: 1 (I'd expect one)

相关内容