请有人解释一下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)