1. Overview
Implementing a counter is a common technique in almost any programming language. In this tutorial, we are going to see how to implement a counter in Bash script. Moreover, we’ll discuss some common pitfalls and how to solve the problems correctly.
2. Incrementing an Integer in Bash
Usually, when we need a counter, we want to increment its value in a loop. Therefore, implementing a counter won’t be a problem if we know how to increment an integer in Bash.
2.1. Using the let Command
The let command is a built-in Bash command to evaluate arithmetic expressions.
Let’s create a simple shell script let_cmd.sh to show how we can increment an integer variable using the let command:
$ cat let_cmd.sh
#!/bin/bash
COUNTER=0
printf "Initial value of COUNTER=%d\n" $COUNTER
let COUNTER=COUNTER+1
printf "After 'let COUNTER=COUNTER+1', COUNTER=%d\n" $COUNTER
let COUNTER++
printf "After 'let COUNTER++', COUNTER=%d\n" $COUNTER
If we run the let_cmd.sh script, we’ll get the output:
$ ./let_cmd.sh
Initial value of COUNTER=0
After 'let COUNTER=COUNTER+1', COUNTER=1
After 'let COUNTER++', COUNTER=2
The code is pretty straightforward, and we see the let command incremented the integer variable COUNT.
2.2. Using the Bash Arithmetic Expansion
We can use (( math expression )) to evaluate a math expression in Bash.
Similarly, we create a short script math_exp.sh to show how to use the arithmetic expansion to evaluate a math expression:
$ cat math_exp.sh
#!/bin/bash
COUNTER=0
printf "Initial value of COUNTER=%d\n" $COUNTER
COUNTER=$(( COUNTER + 1 ))
printf "After 'COUNTER=\$(( COUNTER + 1 ))', COUNTER=%d\n" $COUNTER
(( COUNTER++ ))
printf "After '(( COUNTER++ ))', COUNTER=%d\n" $COUNTER
If we execute the script, we’ll get the output:
$ ./math_exp.sh
Initial value of COUNTER=0
After 'COUNTER=$(( COUNTER + 1 ))', COUNTER=1
After '(( COUNTER++ ))', COUNTER=2
As the code shows, if we want to get the result of the calculation, we can prepend a dollar ‘*$*‘ sign to the (( … )).
3. Implementing a Counter
Now, it’s time to implement a counter in a shell script.
Let’s say we would like to create a shell script counter.sh to count the number of lines in a command’s output.
To make it easier to verify, we’ll use the “seq 5” in the test:
$ cat counter.sh
#!/bin/bash
COUNTER=0
for OUTPUT in $(seq 5)
do
let COUNTER++
done
printf "The value of the counter is COUNTER=%d\n" $COUNTER
If we execute the script, the counter will have the value of five:
$ ./counter.sh
The value of the counter is COUNTER=5
Great, our counter works!
4. The Subshell Pitfall
We’ve seen how to create a counter and increment its value in a for loop. So far, so good. When we read the output of a command and do the counting, we often use a while loop.
Let’s do the same counting with a while loop:
$ cat pipe_count.sh
#!/bin/bash
COUNTER=0
seq 5 | while read OUTPUT
do
let COUNTER++
done
printf "The value of the counter is COUNTER=%d\n" $COUNTER
In the script above, we only changed the for loop into a while loop, and piped the output of the “seq 5” command to the while loop.
Let’s see if the counter worked as well:
$ ./pipe_count.sh
The value of the counter is COUNTER=0
Oops! Why has that happened? The let command didn’t work in a while loop? Let’s print some debug messages after each loop step:
#!/bin/bash
COUNTER=0
seq 5 | while read OUTPUT
do
let COUNTER++
printf "[DEBUG] After a loop step COUNTER=%d\n" $COUNTER
done
printf "The value of the counter is COUNTER=%d\n" $COUNTERbbvg
Now, let’s rerun the script:
$ ./pipe_count.sh
[DEBUG] After a loop step COUNTER=1
[DEBUG] After a loop step COUNTER=2
[DEBUG] After a loop step COUNTER=3
[DEBUG] After a loop step COUNTER=4
[DEBUG] After a loop step COUNTER=5
The value of the counter is COUNTER=0
The debug output shows the let command worked properly in a while loop. However, the COUNTER was somehow “reset” to 0 after the loop.
The cause of this weird problem is the pipe. When we use a pipe, as in command1 | command2, command2 will be executed in a subshell. The changes that happen in a subshell won’t affect the current shell, even if it’s the same variable.
Back to our pipe_count.sh script, since we pipe the output of the seq 5 to a while loop, the while loop runs in a subshell. Therefore, when we check the variable COUNTER after the loop, it still has the value 0.
Next, let’s see how to solve this problem.
4.1. Using a Process Substitution
To solve the subshell problem, we can redirect the process substitution of the seq command to the while loop:
$ cat ps_count.sh
#!/bin/bash
COUNTER=0
while read OUTPUT
do
let COUNTER++
done < <(seq 5)
printf "The value of the counter is COUNTER=%d\n" $COUNTER
In this way, the while loop is not running in a subshell, and the counter will eventually report the correct value:
$ ./ps_count.sh
The value of the counter is COUNTER=5
4.2. Using a Named Pipe
Alternatively, we can also use a named pipe to solve this problem:
$ cat ./fifo_count.sh
#!/bin/bash
COUNTER=0
NAMED_PIPE="./myFifo"
if [[ ! -p "$NAMED_PIPE" ]]; then
mkfifo $NAMED_PIPE
fi
seq 5 > "$NAMED_PIPE" &
while read OUTPUT
do
let COUNTER++
done < "$NAMED_PIPE"
rm "$NAMED_PIPE"
printf "The value of the counter is COUNTER=%d\n" $COUNTER
In the script above, we create a named pipe and feed it with the seq 5 command. Later, the while loop reads the named pipe to get the output of the seq command.
The variable COUNTER will have the expected value after the while loop:
$ ./fifo_count.sh
The value of the counter is COUNTER=5
5. Conclusion
In this article, we’ve learned how to implement a counter in Bash script. Further, we have discussed the subshell pitfall and the solutions to the problem.