Using Loops In Bash
We’re Earthly. We make building software simpler and therefore faster. This article is about looping in bash. If you are looking for to move beyond bash scripts for building and deploying then check us out.
Like any other programming language, Bash supports loops. The loops are used to repeatedly execute a set of commands based on some condition. Along with conditionals, they’re the most common way to control the flow of a program.
Once you’ve mastered variables and conditionals, you’re ready to learn loops. In this article, you’ll learn the different types of loops provided by Bash and see some examples of using them to accomplish various tasks.
while
Loop
Bash provides three types of loops: while
, until
, and for
.
The while
loop is used to execute commands as long as a condition is true. The general syntax for a while
loop is as follows:
#!/bin/bash
while test-commands
do
consequent-commands
done
Or equivalently, it can be a one-liner:
while test-commands; do consequent-commands; done
The test-commands
can be any command that exits with a success or failure status. An exit status of 0
is considered a success, and any non-zero status is considered a failure. Since each Linux command exits with a status code, you can use any command as a condition for the while
loop.
Before executing the commands in the body of the while
loop, the condition is checked. The loop is executed if the test-commands
exits with success. If the test-commands
exits with a failure, the loop doesn’t run. The return code of the while
loop is the return code of the last command in the body of the while
loop.
Just like conditionals, you can use the test
, [
, and [[
commands in the condition of the while
loop. For example, the following while
loop prints off all the numbers from 1 to 10:
#!/bin/bash
i=1
while [ $i -le 10 ]
do
echo $i
((i++))
done
The condition above is [ $i -le 10 ]
. The [
command evaluates whether $i
is less than or equal to 10, and if it is, the command exits with a status of 0
. Since i
is initiated with the value of 1
, the condition is true, so the loop is executed and prints 1
. In the loop’s body, $i
is incremented, and in the next iteration, the condition is rechecked. This goes on until $i
becomes 11
, at which point, the [
command exits with a status of 1
, and the loop exits.
Being able to use [
and [[
means you can have more complex conditions by combining two or more conditions, or performing tests related to variables and files. You can find a full list of tests in the Bash Reference Manual.
You can also use arithmetic expressions to evaluate numerical comparisons in the while
loop condition. Below is the while
loop from above, rewritten using the arithmetic expression:
#!/bin/bash
i=1
while ((i <= 10))
do
echo $i
((i++))
done
Note that this only works for arithmetic expressions.
until
Loop
The until
loop is a rarely used cousin of the while
loop. Its syntax is similar to the while
loop:
#!/bin/bash
until test-commands
do
consequent-commands
done
The only difference between the while
loop and the until
loop is that the until
loop will run as long as test-commands
has a non-zero exit status. You can think of it as an inverted while
loop. Here’s a sample:
i=1
$ until [ $i -gt 10 ]; do echo $i; ((i++)); done
$ 1
2
3
4
5
6
7
8
9
10
for
Loop
for
loops in Bash are used to iterate over a list of items. The general syntax of the for
loop is as follows:
#!/bin/bash
for variable_name in list_of_items
do
commands
done
Or it can be written as a one-liner:
for variable_name in list_of_items; do commands; done
The variable_name
is the name of the variable that you can access in the body of the loop that represents the current item from the list as the list is being looped through. The list_of_items
is a list of space- or newline-separated items, or a command that returns a list of items.
To begin, the list is expanded according to the expansion rules, and for each item in the list, the commands
will run. At every iteration, the current item can be accessed using the variable_name
.
Here’s an example of a for
loop:
#!/bin/bash
for word in hello world
do
echo $word
done
The output is as follows:
hello
world
As you can see, the list contains two items, hello
and world
. In the first iteration, word
is bound to hello
, and hello
is printed. In the second iteration, word
is bound to world
, so world
is printed.
It’s possible to generate a range of numbers or characters using the sequence expression syntax. The syntax of the sequence expression is {start..end}
where start
and end
are integers or characters. This expression creates a range of numbers or characters between start
and end
that are both inclusive. You can also optionally specify an interval with the syntax {start..end..interval}
.
The for
loop can iterate over a range of numbers or characters created using the sequence expression:
for i in {1..10..2}; do echo $i; done
$ 1
3
5
7
9
If you’re using the for
loop in a script, you can omit the in list_of_items
part. In that case, the loop will iterate over the arguments passed to the scripts. This is the same as running for variable_name in "$@"
. You can find out more about $@
in “Understanding Bash Variables”.
To test this, create a file named for_loop.sh
with the following code:
#!/bin/bash
for i
do
echo $i
done
Then make this code executable and run it with some arguments:
chmod +x for_loop.sh
$ ./for_loop.sh 1 2 3
$ 1
2
3
Bash also has an alternate form of the for
loop that is similar to the for
loop in languages like C or C++. The syntax is as follows:
#!/bin/bash
for (( expr1 ; expr2 ; expr3 ))
do
commands
done
Here, expr1
, expr2
, and expr3
are arithmetic expressions. Before the loop starts, expr1
is evaluated. Then expr2
is evaluated, and if it evaluates to a non-zero value, the loop runs. After every iteration, expr3
is evaluated. The loop runs until expr2
evaluates to zero.
Note that
expr2
must evaluate to a non-zero value for the loop to run, unlike thewhile
loop.
Here’s an example of this type of for
loop:
for (( i=1 ; i<=10; i++ )); do echo $i; done
$ 1
2
3
4
5
6
7
8
9
10
All three expressions are optional, and if any of them are omitted, it will evaluate to 1
. However, semicolons are required.
Here’s a sample:
i=1
$ for (( ; i<=10; i++ )); do echo $i; done
$ 1
2
3
4
5
6
7
8
9
10
If you’re familiar with for
loops in Python, you’re probably used to using them for iterating over arrays. You can also do the same in Bash, albeit with a bit of a caveat.
Say you are defining an array like the following:
WORDS=('Hello' 'World' 'This' 'Is' 'An' 'Array') $
Then you’d want to run a for
loop like this:
for i in ${WORDS}; do echo $i; done $
This prints only Hello
and does not iterate over the array.
This is because ${WORDS}
is the same as ${WORDS[0]}
(ie the first element of the array). To iterate over the array, you need to expand the array using either ${WORDS[@]}
or ${WORDS[*]}
. Both versions are the same unless it’s double-quoted. You can learn more about this difference in the Bash Reference Manual.
for i in ${WORDS[@]}; do echo $i; done
$ Hello
World
This
Is
An
Array
break
and continue
Statements
The break
and continue
statements help modify the default flow of the loop. The break
statement immediately stops the loop, and the continue
statement skips the rest of the loop and jumps to the next iteration. These statements can be used in the body of the while
, for
, and until
loops.
In the following example, when i
equals 6
, the break
statement is encountered, and the loop stops so the output is only up to 6
:
#!/bin/bash
for (( i=1; i<=10; i++ ))
do
echo $i
if [ $i -eq 6 ]
then
break
fi
done
# Output:
# 1
# 2
# 3
# 4
# 5
# 6
The following example shows the continue
statement in action. When i
is equal to 6
, the continue
statement causes Bash to skip the echo
command and jump to the next iteration.
#!/bin/bash
for (( i=1; i<=10; i++ ))
do
if [ $i -eq 6 ]
then
continue
fi
echo $i
done
# Output:
# 1
# 2
# 3
# 4
# 5
# 7
# 8
# 9
# 10
As you can see, 6
is skipped in the output.
Examples Using Loops
Now that you know the three types of loops, it’s time to see them in action. Below are a few practical use cases of the various loops:
Renaming Files
If you have to rename several files at once, loops can be a valuable resource. By utilizing for
loops, you can iterate over a list of files and rename them. In the following example, the script renames all .jpeg
files in a directory to .jpg
:
#!/bin/bash
for i in *.jpeg
do
mv $i ${i//jpeg/jpg}
done
You can read “Bash String Manipulation” to learn more about how ${i//jpeg/jpg}
works.
Waiting for Successful Execution of a Command
Sometimes, you need to wait for another command to finish executing before you can continue with your script. For instance, if your script needs a running server, you can utilize a while
loop to wait for the server to start. How exactly you check for the execution of the command depends on the command and how it communicates the status of its execution.
Following is an example where the script waits for some command to finish. It’s assumed that this command creates a file named result.txt
to indicate its execution:
#!/bin/bash
while [ ! -f result.txt ]
do
echo "Waiting for result.txt to be created..."
sleep 1
done
echo "File created. Exiting"
Save the above code in a file and run it. You’ll see that it prints “Waiting for result.txt to be created…” every second. In another terminal, create result.txt
by running touch result.txt
, and the script will terminate.
Counting Files in a Directory
The following example is slightly complex. It counts the number of directories and files with different extensions and presents the number of files corresponding to each extension:
#!/bin/bash
declare -A counter
for i in *
do
if [[ -d "$i" ]]
then
((counter["dir"]++))
elif [[ -f "$i" ]]
then
extension=${i##*.}
((counter[${extension}]++))
fi
done
for i in "${!counter[@]}"
do
echo "$i = ${counter[$i]}"
done
The script uses declare- A
to declare an associative array named counter
. The for
loop then iterates over all the files in the current directory. The test [[ -d "$i" ]]
checks if the file is a directory. If it is, the counter["dir"]
value is incremented. The "dir"
key will automatically be created, and the value will be set to 0
the first time it is accessed.
Then the [[ -f "$i" ]]
test checks if it is a file or not. If it is, it extracts the extension using the shell parameter expansion and increments the corresponding counter in the array.
The second for
loop iterates over the keys of the array. Since counter
is an associative array, ${counter[@]}
will expand to only the values of the array. Then ${!counter[@]}
is used, which expands to the list of indices. The indices, along with the values, are then printed.
Here’s a sample output:
dir = 54
java = 1
log = 49
pdf = 65
json = 11
out = 1
png = 44
journal = 1
zip = 25
js = 1
txt = 5
jpeg = 7
Guessing Game
While the other examples may seem a little boring, this one is a bit more fun: a game. The script will generate a secret random number between 1 and 100, and you have to try and guess it in five tries. Here’s the script:
#!/bin/bash
number=$(($RANDOM % 100 + 1))
count=1
while [ ${count} -le 5 ]
do
read -r -p "Enter your guess: (${count} / 5): " guess
if [ ${guess} -eq ${number} ]
then
echo "You win"
break
elif [ ${guess} -lt ${number} ]
then
echo "You're thinking too small."
else
echo "You're thinking too large."
fi
((count++))
done
Here, $RANDOM
is used to generate a random number. The $count
variable stores the number of guesses made by the user. A while
loop is used to run the script as long as $count
is less than or equal to 5. In the body of the while
loop, the read
command is used to get the user’s input. If it’s equal to the secret number, the user wins, and the loop ends. Otherwise, a hint is given based on whether the user’s guess is greater or less than the secret number.
Conclusion
Loops are one of the most fundamental concepts in Bash, or any programming language for that matter. From iterating over items to manipulating files, any fairly advanced script is certain to employ loops. In fact, the Bash shell itself is a big while
loop.
This article gave you an overview of loops in Bash, using while
, until
, and for
loops. If you want to learn more about loops, you can consult the Bash manual or if you want to learn more about Bash, you can check out the next article in this series.
If you love building things in Bash, you’re sure to love Earthly. Supercharge your build system with the clean and approachable syntax, and repeatable builds, of Earthly.
Earthly makes CI/CD super simple
Fast, repeatable CI/CD with an instantly familiar syntax – like Dockerfile and Makefile had a baby.