Ways of iterating in bash

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the bash category.

Last Updated: 2024-04-19

Depending on what you wish to do, there are various ways to iterate in shell.

(All of these assume one iteration for each item in the list.)

Case 1: Iterating array entries

# Notice there are no commas in the bash array
debuggers=(binding.pry binding.remote_pry debugger)

# 1. Notice that the `$` comes OUTSIDE the debugger variable
# 2. Be aware that `for debugger in debuggers` would fail because it is an
# array.
for debugger in "${debuggers[@]}"
do
  echo "$debugger"
done

Case 2: Iterating words separated by spaces

Subcase 2.a: When not held inside a variable

for number in 1 2 3 4 5; do
  echo $number
done

Subcase 2.b When held in a variable

numbers="1 2 3 4 5"
# If you are using `zsh`, this option MUST be set, otherwise only a single
# iteration will occur. This is because ZSH, unlike bash, does not split when
# expanding variables
setopt shwordsplit
# Note the necessity of NOT wrapping $numbers in quotes
for number in $numbers; do
  echo $number
done
# Unset this option to restore normal behavior.
unsetopt shwordsplit

This would output:

1
2
3
4
5

By constrast, here is what happens without the shwordsplit bits in zsh

1 2 3 4 5

And while we're here, if you want an uncomplicated way to put something loopable in a string variable, a good solution is to put newline characters into the string. For example

tables="products\nusers\norders"
for $table in $tables; do
  echo $table
done

will print:

products
users
orders

Case 3: Iterating on lines of a file

cat Gemfile | while read -r line; do
  echo "$line"
done

What NOT to do:

# This contains one entry for every WORD on each line - i.e. far too many
iterations.
for file in $(cat Gemfile); do
  check_for_debuggers "$file"
done

Case 4: Iterating on a number range

for i in {0..3}
do
  echo "Number: $i"
done

Case 5: Iterating based on function output

This one preserves exit statuses by not running in subshells

changed_files() {
  ...
}

main() {
  for file in $(changed_files); do
    stop_if_debuggers "$file"
  done

  echo "I should not be executed"
  exit 0
}

By contrast, if we did the following, then the while loop is in a subshell, therefore the overall program will not respect exit codes from this subshell and halt as expected.

changed_files | while read -r file; do
  stop_if_debuggers $file
done

Case 6: Iterating based on a few lines you paste into a string

I wanted to upload part of my local .env file that was in the past buffer to Heroku without manually invoking the CLI line heroku config:set X once per item.

More abstractly, I wanted to iterate based on lines within my paste buffer. How was this done?

while read line
do
 heroku config:set $line --remote staging
done <<<'X_STRIPE_KEY=pk_test_51..
X_STRIPE_SECRET=sk_test_51H...
X_STRIPE_WEBHOOK_SIGNING_SECRET=whsec_9U...'

Notice:

  1. while read line system was used. This puts the variable in $line.
  2. The input with multiple lines was fed in with <<<'my string'
  3. The ending quotation mark must be at the end of the last entry's line (i.e. 9U...') If, instead, it were on the next line, then I would get a blank fourth entry when iterating.

Resources