Bash 循环中计数器递增不起作用

165

我有以下简单的脚本,在循环中运行并想要维护一个COUNTER。我无法弄清楚为什么计数器没有更新。这是由于正在创建的子shell引起的吗?我该如何潜在地解决这个问题?

#!/bin/bash

WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' | awk -F ', ' '{print $2,$4,$0}' | awk '{print "http://domain.example"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' | awk -F '&end=1' '{print $1"&end=1"}' |
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
)

echo $COUNTER # output = 0

1
相关链接:https://dev59.com/3WYr5IYBdhLWcg3wYZKD - Gabriel Devillers
1
你不需要将while循环放入子shell中。只需删除while循环周围的括号即可。或者如果你必须将循环放入子shell中,那么在while do done之后,将计数器转储到临时文件中,并在子shell外部恢复此文件。我会在答案中为您准备最终程序。 - Znik
13个回答

184

首先,你没有增加计数器的值。将COUNTER=$((COUNTER))更改为COUNTER=$((COUNTER + 1))COUNTER=$[COUNTER + 1]将增加其值。

其次,将子shell变量回传给调用者比较棘手,正如你所猜测的那样。子shell中的变量在子shell外部不可用。这些变量是子进程本地的变量。

解决方法之一是使用一个临时文件来存储中间值:

TEMPFILE=/tmp/$$.tmp
echo 0 > $TEMPFILE

# Loop goes here
  # Fetch the value and increase it
  COUNTER=$[$(cat $TEMPFILE) + 1]

  # Store the new value
  echo $COUNTER > $TEMPFILE

# Loop done, script done, delete the file
unlink $TEMPFILE

1
@chepner,你有证据表明$[...]已经被弃用了吗?有没有其他的解决方案? - blong
10
在 POSIX shell 采用 $((...)) 之前,bash 使用了 $[...]。我不确定它是否曾经被正式弃用,但我在 bash 的手册页中找不到任何提到它的内容,似乎只支持向后兼容。 - chepner
此外,$(...) 优于 ... - Mr. Developerdude
8
@blong 这是一个有关$ [...] vs $((...))的SO问题,讨论并提到了弃用:https://dev59.com/VHE95IYBdhLWcg3wMrAB - Ogre Psalm33
这个模板很糟糕。它为每个循环和每个匹配的源文件都转储临时文件。我们为什么要创建额外的I/O?只需在子shell内部循环后一次性转储计数器即可。在子shell之外,您可以恢复计数器一次,而无需进行不必要的I/O。这很重要,因为并非所有文件系统都具有有效的写入取消功能。 - Znik
我认为使用进程替代的答案更为恰当,而在每个循环中使用临时文件来存储变量值似乎是一种非常繁琐的解决方案。 - Luis Vazquez

110
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

已测试的Bash:Centos,SuSE,RH


1
@kroonwijk 在方括号之前需要有一个空格(从正式的角度来说,这是为了“分隔单词”)。否则,Bash无法看到先前表达式的结尾。 - EdwardG
1
问题是关于带有管道的while循环,因此会创建一个子shell,在您的答案中是正确的,但您没有使用管道,因此并没有回答问题。 - chrisweb
2
根据cheperner在另一个答案中的评论,$[ ]语法已经被弃用。https://stackoverflow.com/questions/10515964/counter-increment-in-bash-loop-not-working#comment13600255_10516135 - Mark Haferkamp
1
这段程序相关的内容的翻译是:这并没有解决主要问题,主循环放置在子shell下面。 - Znik
无限循环?和for()循环? - Peter Krauss
这个例子与原问题不同,因为创建了一个子shell,变量没有被保留。此外,在循环内部增加变量的结构在新的算术运算符构造$(( ))中已经过时(如果不是废弃的)。 - Luis Vazquez

60
COUNTER=$((COUNTER+1)) 

在现代编程中,这是一个相当笨拙的结构。

(( COUNTER++ ))

看起来更加“现代化”。你也可以使用

let COUNTER++

如果您认为这样可以提高可读性。有时,Bash 提供了太多的做事方式——我想这是 Perl 的哲学——而 Python 的“只有一种正确的方法”可能更为适宜。如果有争议的陈述,那么这就是一个!无论如何,我建议(在这种情况下)的目标不仅是增加变量,而是(普遍规则)编写其他人可以理解和支持的代码。符合规范可以实现这一点。

希望对你有所帮助


3
这并没有回答原问题,即如何在(子进程)循环结束后获取计数器中的更新值。 - Luis Vazquez

17
Try to use
COUNTER=$((COUNTER+1))

取代

COUNTER=$((COUNTER))

2
抱歉,那是一个打字错误。实际上应该是 ((COUNTER+1))。 - Sparsh Gupta
11
@AaronDigulla 写的 (( COUNTER++ ))(没有美元符号)可以翻译为“自增变量COUNTER”。 - Dennis Williamson
2
我不确定为什么,但当我使用(( COUNTER++ ))时,我的一个脚本一直失败,但当我改用COUNTER=$((COUNTER + 1))时,它就可以工作了。GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu) - Steven Lu
也许你的哈希铭文行将bash作为/bin/sh而不是/bin/bash运行? - Max
1
请删除您的回答,因为它只会产生信息噪音 :) 主要问题是,在子shell中增加了计数器。 - Znik

12
count=0   
base=1
(( count += base ))

12

不必使用临时文件,在while循环周围创建子shell,可以通过使用进程替换来避免。

while ...
do
   ...
done < <(grep ...)

顺便提一下,你应该能够将所有的grep、grep、awk、awk、awk转换为单个的awk

从Bash 4.2开始,有一个lastpipe选项,

在当前shell上下文中运行管道的最后一个命令。如果启用作业控制,则lastpipe选项无效。

bash -c 'echo foo | while read -r s; do c=3; done; echo "$c"'

bash -c 'shopt -s lastpipe; echo foo | while read -r s; do c=3; done; echo "$c"'
3

进程替换在循环内部递增计数器并在完成后在外部使用时非常有用。但是,进程替换的问题在于我找不到一种方法来获取执行命令的状态码,而使用管道则可以通过使用${PIPESTATUS [*]}来实现。 - chrisweb
@chrisweb:我添加了关于lastpipe的信息。顺便说一下,你应该使用"${PIPESTATUS[@]}"(而不是星号)。 - Dennis Williamson
在bash中(不是我之前错误地写成的perl),errata退出代码是一个表格,因此您可以单独检查管道链中的所有退出代码。在测试之前,您必须首先复制此表格,否则在第一条命令之后,您将丢失所有值。 - Znik
这是对我有效的解决方案,而且没有使用外部文件来存储变量的值,这在我看来太过平凡了。 - Luis Vazquez

12

我认为这个单独的 awk 命令与你的 grep|grep|awk|awk 管道是等效的:请测试一下。你的最后一个 awk 命令似乎根本没有改变任何东西。

COUNTER 的问题在于 while 循环在子 shell 中运行,因此对变量的任何更改都会在子 shell 退出时消失。你需要在同一个子 shell 中访问 COUNTER 的值。或者采用 @DennisWilliamson 的建议,使用进程替换,并避免使用子 shell。

awk '
  /GET \/log_/ && /upstream timed out/ {
    split($0, a, ", ")
    split(a[2] FS a[4] FS $0, b)
    print "http://example.com" b[5] "&ip=" b[2] "&date=" b[7] "&time=" b[8] "&end=1"
  }
' | {
    while read WFY_URL
    do
        echo $WFY_URL #Some more action
        (( COUNTER++ ))
    done
    echo $COUNTER
}

1
谢谢,最后一个awk基本上会删除end=1之后的所有内容,并在末尾放置一个新的end=1(这样下次我们就可以删除它之后追加的所有内容)。 - Sparsh Gupta
1
@SparshGupta,前面的awk在“end=1”之后没有打印任何内容。 - glenn jackman
这对问题脚本进行了很好的改进,但并没有解决子shell内部计数器增加的问题。 - Znik

8

极简主义

counter=0
((counter++))
echo $counter

简单的一个 :-). 感谢 @geekzspot - Hussain K
2
由于存在子shell,所以“例如问题中的命令不起作用”。 - Znik

7
有两种情况导致表达式((var++))对我失败:
  1. 如果我将bash设置为严格模式(set -euo pipefail)并且从零开始递增(0)。

  2. 从1开始是可以的,但是从零开始递增会导致递增返回“1”,在严格模式下这是一个非零返回代码失败。

我可以使用((var+=1))var=$((var+1))来避免这种行为。

我遇到了同样的问题。在互联网搜索后,我找到了这个答案。这是由于“++”运算符产生了令人困惑和意外的行为。set -e; x=-2; while [ "$x" -le 2 ]; do (( x++ )) || echo "error incrementing x to $x"; done;结果是 _错误:将x增加到1_。 - Bill Jetzer
请注意,递减运算符“--”也包含了这个“特性”。该运算符在从零递减时会返回成功。将循环改为向后运行,您将得到“错误:将x递减至-1”的提示。 - Bill Jetzer
2
根据 Ilkka Virta(回答 bug-bash@gnu.org 的问题)向我解释,这不是 inc/dec 运算符,而是 ((...)) 结构。如果表达式的结果为零,则 ((...)) 返回一个状态1,这使其对条件表达式非常有用:if (( 100-100 )); then echo true; else echo false; fi 如果计数器从零开始,则可以使用 ((++x)),它返回表达式求值之后的值,而 ((x++)) 返回递增前 x 的值,导致“假/错误”返回值。 - Bill Jetzer

3

这就是你需要做的全部:

$((COUNTER++))

这是《学习bash Shell》第三版的摘录,第147页和第148页:

bash算术表达式与Java和C语言中的相应表达式相同[9]。优先级和结合性与C语言相同。表6-2列出了支持的算术运算符。尽管其中一些(或包含)特殊字符,但无需对它们进行反斜杠转义,因为它们在$((...))语法内。

..........................

当您想将值增加或减少1时,++和-运算符非常有用。 它们的工作方式与Java和C相同,例如,value ++将值增加1。 这被称为后递增; 还有一个前递增:++ value。 差异在以下示例中变得明显:

$ i=0
$ echo $i
0
$ echo $((i++))
0
$ echo $i
1
$ echo $((++i))
2
$ echo $i
2

请参见http://www.safaribooksonline.com/a/learning-the-bash/7572399/


这是我需要的版本,因为我在一个 if 语句的条件中使用它:if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi 不管对错,这是唯一可靠工作的版本。 - Mr. Lance E Sloan
这个表单的重要之处在于你可以在一个步骤中使用增量。i=1; while true; do echo $((i++)); sleep .1; done - Bruno Bronosky
1
@LS: 如果 (( needsComma++ > 0 )); 则为真,或者如果 (( needsComma++ )); 则为真。 - Dennis Williamson
在Bash中使用“echo $((i++))”,我总是得到“/opt/xyz/init.sh: line 29: i: command not found”的错误。我做错了什么? - mmo
1
这并没有解决如何在循环外获取计数器值的问题。 - Luis Vazquez

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接