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)
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)
这个更简单的例子可能会有所帮助:
\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#!/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运行此打印:
\nCOUNTER: 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
,只是这实际上是该变量的副本,而不是脚本其余部分中可用的变量。
最后,您可能需要通读 bash 手册的命令执行环境部分,特别是(强调我的):
\n\n\n当要执行除内置函数或 shell 函数之外的简单命令时,将在由以下内容组成的单独执行环境中调用该命令。除非另有说明,否则这些值是从 shell 继承的。
\n\n
\n- shell\xe2\x80\x99s 打开文件,以及通过重定向到命令指定的任何修改和添加
\n- 当前工作目录
\n- 文件创建模式掩码
\n- 标记为导出的 shell 变量和函数,以及为命令导出的变量,传递到环境中(请参阅\n环境)
\n- shell 捕获的陷阱将重置为从 shell\xe2\x80\x99s 父级继承的值,并且 shell 忽略的陷阱将被忽略
\n在此单独环境中调用的命令不能影响\n shell\xe2\x80\x99s 执行环境。
\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
函数进行了如上所示的修改,我会得到:
$ 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