Bash loops
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
doneOr equivalently, it can be a one-liner:
while test-commands; do consequent-commands; doneThe 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++))
doneThe 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++))
doneNote 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
doneThe 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
10for 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
doneOr it can be written as a one-liner:
for variable_name in list_of_items; do commands; doneThe 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
doneThe 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
9If 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
doneThen make this code executable and run it with some arguments:
$ chmod +x for_loop.sh
$ ./for_loop.sh 1 2 3
1
2
3Bash 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
doneHere, 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
expr2must evaluate to a non-zero value for the loop to run, unlike thewhileloop.
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
10All 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
10If 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; doneThis 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
Arraybreak 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
# 6The 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
# 10As 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}
doneYou 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]}"
doneThe 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++))
doneHere, $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.

Playing the game and winning using binary search
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.