Loops

Loop, as the name implied, is execute a set of code repeatedly. Under one clear condition, BASH allows looping to happen. Here, we will look into several types of looping mechanism.

for

For looping mechanism is a single feed known listing loop mechanism. It requires a known list as a feed to its loop.

Pattern

There are various way to write the for loop. However, as a good practice, you write it:

for variable in (list of items); do
        ...
        echo "Use $variable for each elements in the list."
done

Good Practice:

  1. keep to do the same line as for. It is the same as open bracket for most programming languages.
  2. keep done as its own. It is the same as close bracket for most programming languages.


Basic Use

You can feed a list of items after in keyword. For loop will read each of the element and pass it via the variable ($fruit) for you to use.

#!/bin/bash
for fruit in banana papaya pineapple; do
        echo "I love $fruit."
done

NOTE:

  • Avoid applying quote to the list of items. Doing so will turn the whole thing into 1 single string element.


Feed An Array

Another common use is feeding the loop with an array. For loop will iterate through the list, get the element and save it inside your variable for your operations.

#!/bin/bash
list=(
        "banana"
        "papaya"
        "pineapple"
)

for fruit in "${list[@]}"; do
        echo "I love $fruit"
done


Search Directory

To search files in directory, you can use the wildcard and pattern searches. However, if there is no match (0 element), for loop will print the given pattern as a result. Therefore, you'll need to check the file existence before using the element. Here is an example for searching mp3 files in the current directory:

#!/bin/bash
for file in ./*.mp3; do
        [ -e "$file" ] || continue   # skip this if $file is ./*.mp3, which doesn't make sense.
        ...
done


C-Style Countable Loop

BASH specific feature - In C programming, we are used to make looping by a countable number within a specified range. This is doable in BASH looping as well. It complies to the following format:

for ((initial; condition to run loop; counter action)); do
        ...
done

Here's is an example for looping from 0 to 10:

for ((i=0; i<=10; i++)); do
        echo "current number: %i"
done

NOTE:

  • As a good practice:
    1. a space after the for keyword
    2. no space after the open parenthesis
    3. no space before the closing parenthesis
    4. one space after each semicolon statement separator (;)
  • You don't need the dollar sign to denote the index variable.


Loop through Character for String

BASH specific feature - Unlike array looping, to loop through a string for each character can be very painful, yet achievable using for loop. The idea is to use the c-style counter loop against the length of the string. Here's the format, with all the usable elements:

total_char="${#string}"
for ((i=0; i<"$total_char"; i++)); do
        char=${string:$i:1}
        last_char="$((${#string} - 1))"        # OR last_char="$((total_char - 1))"
        ...
done

Keep in mind that this type of looping is resource intensive and you should use it sparingly smartly. For example, if you don't need the $total_char, don't set it.

While

While mechanism is a conditional iteration. It looks for a conditions to run the iterations or it meets its breaking condition (with a break keyword).

Pattern

There are various way to write the while loop. However, as a good practice, you write it:

while [[ condition ]]; do
        ...
done

NOTE:

  1. keep to do the same line as while. It is the same as open bracket for most programming languages.
  2. keep done as its own. It is the same as close bracket for most programming languages.
  3. the conditions can use both BASH new test conditions or POSIX test conditions, depending on portability requirement. Here, we'll use the BASH new test conditions.


Basic Incremental/Decremental Use

You can feed a condition to the while loop. The loop will repeat itself until the condition is no longer satisfied.

#!/bin/bash
i=0
while [[ "$i" -le 10 ]]; do
        echo "I love repeating work."
        ...
        ((i++))
done


Infinity Loop and Break Triggers

You can also create an infinity loop with while mechanism. The loop will repeat itself until it hits the breaking condition. To break the loop, we use the keyword break.

#!/bin/bash
i=0

while true; do
        if [[ "$i" -gt 10 ]]; then
                break
        fi

        echo "This is $i"
        ...

        ((i++))
done


Input Feeding Loop

While mechanism is also commonly used for reading contents by piping input. Here's is an example:

#!/bin/bash
while read ip name aliases; do
        echo "This is: { $name }; IP: { $ip }; Aliases: { $alias }"
done < /etc/hosts

You need the read command to populate the line into the list of variables you requested. This command splits the line component using the $IFS variable.

Lastly, at the done keyword, use the input pipe to provide a file path you wish to read.


Read File Line By Line

Using the input feeding loop approach, we can also read a file line-by-line. Here is an example:

#!/bin/bash
old_IFS="$IFS"
while IFS='' read -r line || [ -n "$line" ]; do
        echo "$line"
        ...
done < "/path/to/file"
IFS="$old_IFS" && unset old_IFS

You need read -r command to read the line as it is without special interpretation like backslash, configure your $IFS to none for avoiding parsing complication and lastly, [ -n "$line" ] to avoid last line being ignored if it it doesn't ends with newline (\n).


Read File Line By Line with User Interaction

BASH specific feature - you can also read file line-by-line while prompting user interaction. This requires the use of stdin redirect using exec. Here is an example:

#!/bin/bash
exec 3<"./posix.sh"
old_IFS="$IFS"
while IFS='' read -r -u 3 line || [ -n "$line" ]; do
        read -p "> $line (Press ENTER to continue)"
        ...
done
IFS="$old_IFS" && unset old_IFS

The difference is that instead of feeding the file directly into the while loop, you feed it into a redirect channel (in the example, it is 3). This frees up the stdin for user prompting usage like "Press ENTER to continue".


Loop Through Characters for String

Looping through characters for string using while loop is possible as well. Basically it requires you to duplicate the string into a temporary variable since the loop consumes the string character by character. Here, we use the string manipulation to extract the first character:

  1. Get the first character (taking the string and subtract the remainder) - $char
  2. Update index if available - "$((index + 1))"
#!/bin/bash
string="a quick brown fox jumps over the lazy dog"

total=${#string}
index=0
while [ $index -lt $total ]; do
        char="${string:${index}:1}"
        last_char_index="$((total - 1))"        # OR last_char_index="$((${#string} - 1))"
        ...

        index="$((index + 1))"
done
unset index

This loop is lighter than the for loop variant. However, you still need to consider optimizing into suitable efficiency. Example: if you don't need $total or $last_char_index, don't use them.

Until

until mechanism is similar to while mechanism except the condition works in an opposite manner. It looks for a conditions to break the iterations or it meets its breaking condition (with a break keyword). The pattern is:

Pattern

There are various way to write the while loop. However, as a good practice, you write it:

until [[ condition ]]; do
        ...
done

NOTE:

  1. keep to do the same line as until. It is the same as open bracket for most programming languages.
  2. keep done as its own. It is the same as close bracket for most programming languages.
  3. the conditions can use both BASH new test conditions or POSIX test conditions, depending on portability requirement. Here, we'll use the BASH new test conditions.

An example:

#!/bin/bash
i=0
until [[ "$i" -ge 10 ]]; do
  echo "the index is now: $i"
 ((i++))
done

Select

select mechanism is a closed loop for making user to select a fixed list of options. It is a kind of infinity loop and only breaks when meeting with the break keyword.

Pattern

There are various way to write the while loop. However, as a good practice, you write it like:

select variable in item1 item2 ...; do
        if [[ "$variable" == "exit keyword" ]]; then
                break
        fi
        ...
done

NOTE:

  1. keep to do the same line as select. It is the same as open bracket for most programming languages.
  2. keep done as its own. It is the same as close bracket for most programming languages.
  3. the break condition can be both BASH new test conditions or POSIX test conditions, depending on portability requirement. Here, we'll use the BASH new test conditions.
  4. you must plan your break triggers (be it timeout or user interaction) explicitly before further developing the loop functionalities.


Basic Select

Select is useful for gather user input within your fixed value compounds. This saves a lot of resources through reducing user input validation. Here is an example of basic number listing along with exit.

#!/bin/bash
select item in "one" "two" "three" "four" "five" "exit"; do
        echo "You have chosen $item."
        if [[ "$item" == "exit" ]]; then
                break
        fi
done

Running this code produces:

holloway:Desktop$ ./demo.sh 
1) one
2) two
3) three
4) four
5) five
6) exit
#? 1
You have chosen one.
#? 2
You have chosen two.
#? 3
You have chosen three.
#? 4
You have chosen four.
#? 5
You have chosen five.
#? 6
You have chosen exit.
holloway:Desktop$

Notice that upon select 6, you get to break the loop and complete the program. Otherwise, it will print the positioned value.


List Select

Select mechanism allows the use of array as well (instead of a long string). Here's an example based on the basic version:

#!/bin/bash
list=(
        "one"
        "two"
        "three"
        "four"
        "five"
)
list+=("exit")
select item in "${list[@]}"; do
        echo "You have chosen $item."
        if [[ "$item" == "exit" ]]; then
                break
        fi
done

This produces the same result as the basic select.


Changing Prompt Text with $PS3

Notice that the prompt for user input is #?? That is the default PS3 value. You can change your prompt messages by changing the text. Here is an example:

#!/bin/bash
list=(
        "one"
        "two"
        "three"
        "four"
        "five"
)
list+=("exit")
PS3="Please select number (1-${#list[@]}): "
select item in "${list[@]}"; do
        echo "You have chosen $item."
        if [[ "$item" == "exit" ]]; then
                break
        fi
done

This will prompt a different message instead:

holloway:Desktop$ ./demo.sh 
1) one
2) two
3) three
4) four
5) five
6) exit
Please select number (1-6): 1
You have chosen one.
Please select number (1-6): 2
You have chosen two.
Please select number (1-6): 3
You have chosen three.
Please select number (1-6): 4
You have chosen four.
Please select number (1-6): 5
You have chosen five.
Please select number (1-6): 6
You have chosen exit.
holloway:Desktop$ 

Nested Loop

It is possible to have a loop inside of another loop. However, as a best practice to avoid cyclomatic complexity, you should always makes good use of your 3-tabs warning.

If you need more than 3-tabs, you are likely going to produce a highly complex codes. Try break it down using function or simplify the process.

That's all about looping in BASH. Feel free to move up to the next section.