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


125

我有以下简单的脚本,在其中运行循环并希望维护COUNTER。我无法弄清楚为什么计数器没有更新。是由于创建了subshel​​l造成的吗?我该如何解决呢?

#!/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.com"$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


您不需要将while循环放入子shell。只需删除while循环周围的括号,就足够了。否则,如果必须将其循环到子外壳中,则在完成后将计数器转储一次到临时文件中,然后将该文件还原到子外壳外。我将为您准备最后的程序。
Znik

Answers:


156

首先,您没有增加计数器。更改COUNTER=$((COUNTER))COUNTER=$((COUNTER + 1))COUNTER=$[COUNTER + 1]会增加它。

其次,在您推测时将子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

30
$ [...]已弃用。
chepner,2012年

1
@chepner是否有$[...]不推荐使用的引用?有替代解决方案吗?
blong

9
$[...]在使用bash$((...))通过了POSIX外壳。我不确定它是否曾被正式弃用,但我在bash手册页中找不到任何提及,它似乎仅受向后兼容性的支持。
chepner 2014年

此外,$(...)的优先级高于...
Lennart Rolland 2014年

7
@blong下面是一个$ SO问题[...] VS $((...)),讨论和引用弃用:stackoverflow.com/questions/2415724/...
食人魔Psalm33

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

测试重击:Centos,SuSE,RH


1
@kroonwijk在方括号之前必须有一个空格(正式来说是“分隔单词”)。否则Bash无法看到上一个表达式的结尾。
EdwardGarson

1
问题是关于使用管道的一段时间,因此在创建子外壳的位置,您的答案是正确的,但是您没有使用管道,因此它没有回答问题
chrisweb 18-10-10

2
根据chepner对另一个答案的评论,$[ ]不赞成使用该语法。stackoverflow.com/questions/10515964/...
马克Haferkamp

这不能解决主要问题,主循环位于子
外壳

42
COUNTER=$((COUNTER+1)) 

在现代编程中是相当笨拙的构造。

(( COUNTER++ ))

看起来更“现代”。您也可以使用

let COUNTER++

如果您认为这样可以提高可读性。有时,Bash给出了太多的工作方式-我认为是Perl哲学-当也许Python“只有一种正确的方式”可能更合适。如果有的话,那是一个值得商!的说法!无论如何,我建议的目的(在这种情况下)不仅是增加变量,而且(通用规则)还是编写其他人可以理解和支持的代码。整合对于实现这一目标大有帮助。

高温超导


这没有解决原始问题,即如何在结束(子过程)循环之后如何在计数器中获取更新的值
Luis Vazquez

16

尝试使用

COUNTER=$((COUNTER+1))

代替

COUNTER=$((COUNTER))

8
或者只是let "COUNTER++"
nullpotent

2
抱歉,是错字。实际上((COUNTER + 1))
Sparsh Gupta'5

8
@AaronDigulla:((( COUNTER++ ))无美元符号)
已暂停,直至另行通知。

2
我不知道为什么,但是我看到我的脚本在使用(( COUNTER++ ))时反复失败,但是当我切换到COUNTER=$((COUNTER + 1))它时起作用。GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
史蒂文·卢

也许您的哈希爆炸行以/ bin / sh而不是/ bin / bash的形式运行bash?
2014年

12

我认为这个awk调用等效于您的grep|grep|awk|awk管道:请对其进行测试。您的最后一个awk命令似乎什么都没有改变。

COUNTER的问题在于while循环在子shell中运行,因此,当子shell退出时,对变量的任何更改都将消失。您需要在同一子shell中访问COUNTER的值。或接受@DennisWilliamson的建议,使用流程替代,并完全避免使用子外壳。

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”之后不打印任何内容。
格伦·杰克曼

这对问题脚本做了很好的改进,但不能解决子
外壳


11

可以while使用进程替换来避免在循环周围创建子外壳,而不是使用临时文件。

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[@]}"(而不是星号)。
暂停,直到另行通知。

勘误表。在bash中(不是我之前写错的在perl中),退出代码是一个表,那么您可以单独检查管道链中的所有退出代码。在测试之前,必须先复制此表,否则在执行第一个命令后,您将丢失所有值。
Znik

这是对我有用的解决方案,并且不需要使用外部文件来存储变量值,这对我来说很常见。
Luis Vazquez


3

这就是您需要做的所有事情:

$((COUNTER++))

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

bash算术表达式等同于Java和C语言中的对应表达式。[9] 优先级和关联性与C中的相同。表6-2显示了受支持的算术运算符。尽管其中一些是(或包含)特殊字符,但由于它们在$((...))语法之内,因此无需反斜杠转义它们。

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

如果要将值增加或减少1,则++和-运算符很有用。[11] 它们的工作方式与Java和C中的相同,例如value ++将value递增1。这称为post-increment;还有一个预增量:++ 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 对或错,这是唯一可靠运行的版本。
LS

此表单的重要之处在于,您可以在一个步骤中使用增量。i=1; while true; do echo $((i++)); sleep .1; done
布鲁诺·布罗诺斯基

1
@LS:if (( needsComma++ > 0 )); thenif (( needsComma++ )); then
暂停,直到另行通知。

在bash中使用“ echo $(((i ++)))”,我总是得到“ /opt/xyz/init.sh:第29行:i:找不到命令”我在做什么错了?
mmo

这没有解决有关使计数器值超出循环范围的问题。
Luis Vazquez

1

这是一个简单的例子

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done

1
简单的例子,但并不适用。
Znik

0

似乎您没有更新counter脚本,请使用counter++


抱歉打错了,我实际上在无法正常工作的脚本中使用了((COUNTER + 1))
Sparsh Gupta

无论它是由value + 1还是由value ++增加。子外壳程序结束后,计数器值将丢失,并恢复为此脚本启动时设置的初始0值。
Znik

0

有两种情况导致该表达式((var++))对我失败:

  1. 如果我将bash设置为严格模式set -euo pipefail),并且我的增量从零开始(0)。

  2. 从一(1)开始很好,但零会导致在评估“ ++”时增量返回“ 1”,这是严格模式下的非零返回码失败。

我可以使用((var+=1))var=$((var+1))逃避此行为


0

源脚本的subshel​​l有问题。第一个示例,您可能不需要subshel​​l。但是我们不知道“更多动作”下隐藏了什么。最受欢迎的答案是隐藏的错误,它将增加I / O,并且不能与subshel​​l一起使用,因为它可以恢复内部循环。

不要强行添加“ \”符号,它会通知bash解释器有关行继续的信息。希望对您或任何人有帮助。但是我认为该脚本应该完全转换为AWK脚本,或者使用regexp或perl重写为python,但是多年以来perl的普及度下降了。最好用python来做。

没有子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://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
#(  #unneeded bracket
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
# ) unneeded bracket

echo $COUNTER # output = 0

如果确实需要带子外壳的版本

#!/bin/bash

TEMPFILE=/tmp/$$.tmp  #I've got it from the most popular answer
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://example.com"$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 > $TEMPFILE  #store counter only once, do it after loop, you will save I/O
)

COUNTER=$(cat $TEMPFILE)  #restore counter
unlink $TEMPFILE
echo $COUNTER # output = 0
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.