163

I have the following simple script where I am running a loop and want to maintain a COUNTER. I am unable to figure out why the counter is not updating. Is it due to subshell that's getting created? How can I potentially fix this?

#!/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
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Sparsh Gupta
  • 2,163
  • 5
  • 19
  • 21
  • 1
    Related: https://stackoverflow.com/questions/13726764/while-loop-subshell-dilemma-in-bash – Gabriel Devillers Feb 26 '19 at 20:45
  • 1
    You don't need put while loop into subshell. Simply remove brackets around while loop, it is enough. Or else if you must put it loop into subshell, then after while do done, dump counter into temporary file once, and restore this file outside subshell. I will prepare final procedure to you in answer. – Znik Jan 22 '20 at 14:31

13 Answers13

183

First, you are not increasing the counter. Changing COUNTER=$((COUNTER)) into COUNTER=$((COUNTER + 1)) or COUNTER=$[COUNTER + 1] will increase it.

Second, it's trickier to back-propagate subshell variables to the callee as you surmise. Variables in a subshell are not available outside the subshell. These are variables local to the child process.

One way to solve it is using a temp file for storing the intermediate value:

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
bos
  • 6,437
  • 3
  • 30
  • 46
  • 1
    @chepner Do you have a reference that says `$[...]` is deprecated? Is there an alternative solution? – blong May 27 '14 at 14:25
  • 10
    `$[...]` was used by `bash` before `$((...))` was adopted by the POSIX shell. I'm not sure that it was ever formally deprecated, but I can find no mention of it in the `bash` man page, and it appears to only be supported for backwards compatibility. – chepner May 27 '14 at 15:13
  • Also, $(...) is preferred over `...` – Mr. Developerdude Aug 25 '14 at 02:49
  • 8
    @blong Here is a SO question on $[...] vs $((...)) that discusses and references the deprecation: http://stackoverflow.com/questions/2415724/bash-arithmetic-expression-vs-arithmetic-expression – Ogre Psalm33 Jun 27 '16 at 17:19
  • This template is bad. It dump temporary file for each loop, for each matched source. What for we create additional I/O ???? Simply dump counter once after loop inside subshell. do it *once*. outside subshell you can restore it counter once, without unneeded I/O. This is important, because not all filesystem has effective write cancellation. – Znik Jan 22 '20 at 14:34
  • I think the answer using process substitution is much more appropriate and the use of a temporary file to store the variable value in each loop seems a very cumbersome solution. – Luis Vazquez May 02 '20 at 19:28
110
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

TESTED BASH: Centos, SuSE, RH

John Conde
  • 217,595
  • 99
  • 455
  • 496
Jay Stan
  • 1,181
  • 1
  • 7
  • 2
  • 1
    @kroonwijk there needs to be a space before the square bracket (to 'delimit the words', formally speaking). Bash cannot otherwise see the end of the previous expression . – EdwardG Jan 27 '18 at 17:40
  • 1
    the questions was about a while with a pipe, so where a subshell is created, your answer is right but you don't use a pipe so it's not answering the question – chrisweb Oct 10 '18 at 14:05
  • 2
    Per chepner's comment on another answer, the `$[ ]` syntax is deprecated. https://stackoverflow.com/questions/10515964/counter-increment-in-bash-loop-not-working#comment13600255_10516135 – Mark Haferkamp Aug 08 '19 at 02:40
  • 1
    this does not resolve main question, main loop is placed under subshell – Znik Jan 22 '20 at 15:02
  • infinite loop? and a for() loop? – Peter Krauss Jan 19 '21 at 12:53
  • This example is different and doesn't answer the original question, where the variable is not preserved because a subshell is created. Also the construct used here to increment the variable inside the loop is equivalente but outdated (if not deprecated) with respect to the newer arithmetic operator construct $(( )) used in the question. – Luis Vazquez Jan 04 '22 at 18:57
59
COUNTER=$((COUNTER+1)) 

is quite a clumsy construct in modern programming.

(( COUNTER++ ))

looks more "modern". You can also use

let COUNTER++

if you think that improves readability. Sometimes, Bash gives too many ways of doing things - Perl philosophy I suppose - when perhaps the Python "there is only one right way to do it" might be more appropriate. That's a debatable statement if ever there was one! Anyway, I would suggest the aim (in this case) is not just to increment a variable but (general rule) to also write code that someone else can understand and support. Conformity goes a long way to achieving that.

HTH

Bill Parker
  • 765
  • 5
  • 4
  • 3
    This doesn't address the original question, which is how to get the updatedd value in counter AFTER ending the (sub-process) loop – Luis Vazquez May 02 '20 at 19:24
17

Try to use

COUNTER=$((COUNTER+1))

instead of

COUNTER=$((COUNTER))
dbf
  • 6,399
  • 2
  • 38
  • 65
12

Instead of using a temporary file, you can avoid creating a subshell around the while loop by using process substitution.

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

By the way, you should be able to transform all that grep, grep, awk, awk, awk into a single awk.

Starting with Bash 4.2, there is a lastpipe option that

runs the last command of a pipeline in the current shell context. The lastpipe option has no effect if job control is enabled.

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
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • process substitution is great if you want to increment a counter inside the loop and use it outside when done, the problem with process substitutions is that I found no way to also get the status code of the executed command, which is possible when using a pipe by using ${PIPESTATUS[*]} – chrisweb Oct 11 '18 at 08:23
  • @chrisweb: I added information about `lastpipe`. By the way, you should probably use `"${PIPESTATUS[@]}"` (at instead of asterisk). – Dennis Williamson Oct 11 '18 at 11:53
  • errata. in bash (not in perl as I've been write previously by mistake) exit code is a table, then you can check separatelly all exit codes in pipe chain. before testing first your step must be copy this table, otherwise after first command you'll lost all values. – Znik Feb 03 '20 at 12:47
  • This is the solution that worked for me and without using an external file to store the variable's value which is to much pedestrian in my opinion. – Luis Vazquez May 02 '20 at 19:20
12

I think this single awk call is equivalent to your grep|grep|awk|awk pipeline: please test it. Your last awk command appears to change nothing at all.

The problem with COUNTER is that the while loop is running in a subshell, so any changes to the variable vanish when the subshell exits. You need to access the value of COUNTER in that same subshell. Or take @DennisWilliamson's advice, use a process substitution, and avoid the subshell altogether.

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
}
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • 1
    Thanks, the last awk will basically remove everything after end=1 and put a new end=1 to the end (so that next time we can remove everything that gets appended after it). – Sparsh Gupta May 09 '12 at 17:35
  • 1
    @SparshGupta, the previous awk doesn't print anything after "end=1". – glenn jackman May 09 '12 at 18:46
  • This do very good improve for question script, but does not resolve problem with increasing counter inside subshell – Znik Jan 22 '20 at 15:06
12
count=0   
base=1
(( count += base ))
Community
  • 1
  • 1
pkm
  • 2,683
  • 2
  • 29
  • 44
8

minimalist

counter=0
((counter++))
echo $counter
geekzspot
  • 787
  • 7
  • 7
7

There were two conditions that caused the expression ((var++)) to fail for me:

  1. If I set bash to strict mode (set -euo pipefail) and if I start my increment at zero (0).

  2. Starting at one (1) is fine but zero causes the increment to return "1" when evaluating "++" which is a non-zero return code failure in strict mode.

I can either use ((var+=1)) or var=$((var+1)) to escape this behavior

Rishabh Agarwal
  • 1,988
  • 1
  • 16
  • 33
Augustus Hill
  • 71
  • 1
  • 1
  • I ran into this same issue. Internet search lead me to this answer. Puzzling and unexpected behavior from the "++" operator. `set -e;` `x=-2;` `while [ "$x" -le 2 ];` `do` `(( x++ )) || echo "error incrementing x to $x";` `done;` results in _error incrementing x to 1_ – Bill Jetzer Nov 23 '20 at 16:06
  • Note that the decrement operator `--` includes this "feature" as well. Operator returns success until decrementing from zero. Change the loop to run backward and you'll get _error decrementing x to -1_. – Bill Jetzer Nov 23 '20 at 16:11
  • 2
    As explained to me by Ilkka Virta (who answers questions addressed to bug-bash@gnu.org), this is not the inc/dec operators, but rather the `((...))` construct. If the result of the expression is zero, `((...))` returns a status of 1, which makes it useful for conditional expressions: `if (( 100-100 )); then echo true; else echo false; fi` If your counter starts at zero, you can use `((++x))`, which returns the value _after_ the expression is evaluated, whereas `((x++))` returns the value of x _before_ it's incremented, resulting in an "false/error" return value. – Bill Jetzer Nov 24 '20 at 14:16
3

This is all you need to do:

$((COUNTER++))

Here's an excerpt from Learning the bash Shell, 3rd Edition, pp. 147, 148:

bash arithmetic expressions are equivalent to their counterparts in the Java and C languages.[9] Precedence and associativity are the same as in C. Table 6-2 shows the arithmetic operators that are supported. Although some of these are (or contain) special characters, there is no need to backslash-escape them, because they are within the $((...)) syntax.

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

The ++ and - operators are useful when you want to increment or decrement a value by one.[11] They work the same as in Java and C, e.g., value++ increments value by 1. This is called post-increment; there is also a pre-increment: ++value. The difference becomes evident with an example:

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

See http://www.safaribooksonline.com/a/learning-the-bash/7572399/

  • This is the version of this I needed, because I was using it in the condition of an `if` statement: `if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi` Right or wrong, this is the only version that worked reliably. – Mr. Lance E Sloan Aug 13 '17 at 04:17
  • What's important about this form is that you can use an increment in a single step. `i=1; while true; do echo $((i++)); sleep .1; done` – Bruno Bronosky Jan 21 '18 at 21:12
  • 1
    @LS: `if (( needsComma++ > 0 )); then` or `if (( needsComma++ )); then` – Dennis Williamson Aug 08 '19 at 13:22
  • Using "echo $((i++))" in bash I always get "/opt/xyz/init.sh: line 29: i: command not found" What am I doing wrong? – mmo Dec 06 '19 at 18:29
  • 1
    This doesn't address the question about getting the counter value outside the loop. – Luis Vazquez May 02 '20 at 19:19
2

This is a simple example

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done
IgorAlves
  • 5,086
  • 10
  • 52
  • 83
1

Source script has some problem with subshell. First example, you probably do not need subshell. But We don't know what is hidden under "Some more action". The most popular answer has hidden bug, that will increase I/O, and won't work with subshell, because it restores couter inside loop.

Do not fortot add '\' sign, it will inform bash interpreter about line continuation. I hope it will help you or anybody. But in my opinion this script should be fully converted to AWK script, or else rewritten to python using regexp, or perl, but perl popularity over years is degraded. Better do it with python.

Corrected Version without subshell:

#!/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

Version with subshell if it is really needed

#!/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
Znik
  • 1,047
  • 12
  • 17
0

It seems that you didn't update the counter is the script, use counter++

yjshen
  • 6,583
  • 3
  • 31
  • 40
  • Apologies for the typo, I am actually using ((COUNTER+1)) in script which is not working – Sparsh Gupta May 09 '12 at 12:27
  • 1
    it is no matter it is incremetted by value+1, or by value++ . After subshell ends, counter value is lost, and revert to initial 0 value set at start on this script. – Znik Jan 22 '20 at 15:12