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

Joz*_*cek 5 bash shell-script variable

请有人解释一下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"
Run Code Online (Sandbox Code Playgroud)

我希望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)
Run Code Online (Sandbox Code Playgroud)

mar*_*uso 7

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" )
Run Code Online (Sandbox Code Playgroud)


ter*_*don 5

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

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

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

\n

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

\n
#!/bin/bash\n\nCOUNTER=0\n\nfunction increment {\n  echo "increment called"\n    ((COUNTER++))\n}\n\nfunction report {\n    echo "COUNTER: $COUNTER ($1)"\n}\n\nfunction reset_counter {\n    COUNTER=0\n}\n\nfunction increment_if_yes {\n    answer=$1\n    if [ "$answer" == "yes" ]\n    then\n        increment\n    fi\n}\n\nfunction break_it {\n  echo "aa COUNTER at start of break_it: $COUNTER"\n    echo -e "maybe\\nyes\\nno" | \\\n    while read LL\n    do\n        echo "bb COUNTER in loop top: $COUNTER"\n        increment_if_yes $LL\n        echo "bb COUNTER in loop bottom: $COUNTER"\n    done\n    echo "aa COUNTER at end of break_it: $COUNTER"\n}\n\nreport start # counter should be 0\nincrement\nreport one\nincrement_if_yes yes\nreport two\nincrement_if_yes no\nreport "still two"\n\nreset_counter\nreport reset\n\nbreak_it\nreport "I\'d expect one"\n
Run Code Online (Sandbox Code Playgroud)\n

运行此打印:

\n
COUNTER: 0 (start)\nincrement called\nCOUNTER: 1 (one)\nincrement called\nCOUNTER: 2 (two)\nCOUNTER: 2 (still two)\nCOUNTER: 0 (reset)\naa COUNTER at start of break_it: 0\nbb COUNTER in loop top: 0\nbb COUNTER in loop bottom: 0\nbb COUNTER in loop top: 0\nincrement called\nbb COUNTER in loop bottom: 1\nbb COUNTER in loop top: 1\nbb COUNTER in loop bottom: 1\naa COUNTER at end of break_it: 0\nCOUNTER: 0 (I\'d expect one)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
\n

当要执行除内置函数或 shell 函数之外的简单命令时,将在由以下内容组成的单独执行环境中调用该命令。除非另有说明,否则这些值是从 shell 继承的。

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

在此单独环境中调用的命令不能影响\n shell\xe2\x80\x99s 执行环境。

\n
\n

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

\n

但是,由于您的 shell (bash) 支持进程替换,您可以将您的函数更改为此,它将起作用:

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

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

\n
$ foo.sh \nCOUNTER: 0 (start)\nCOUNTER: 1 (one)\nCOUNTER: 2 (two)\nCOUNTER: 2 (still two)\nCOUNTER: 0 (reset)\nCOUNTER: 1 (I\'d expect one)\n
Run Code Online (Sandbox Code Playgroud)\n