Skip to main content

Lesson: Loops, Arguments & Functions

What you'll learn

  • How to repeat work with for and while loops.
  • How to read arguments passed to your script: $1, $2, $#, and "$@".
  • How to write functions to organize and reuse code, and how they return values.
  • How to use basic arrays to hold lists of items.

By the end you can write scripts that process many items, accept input from the command line, and stay organized as they grow.

The lesson

So far each script ran top to bottom once. Real automation repeats — over every file, every server, every line of a log — and takes input so the same script handles different jobs. That is loops, arguments, and functions.

Run everything on the Jumpbox (10.100.100.254, user ubuntu).

1. The for loop

A for loop walks through a list, binding each item to a variable in turn:

for name in alice bob carol; do
  echo "Hello, $name"
done

The flow:

  list:  alice   bob   carol
           |       |      |
           v       v      v
         name=   name=  name=    <- body runs once per item

The list can come from a glob (file pattern), command substitution, or a number range:

for f in /etc/*.conf; do          # every .conf file
  echo "Found config: $f"
done

for n in $(seq 1 5); do           # 1 2 3 4 5
  echo "Attempt $n"
done

for i in {1..3}; do               # brace expansion: 1 2 3
  echo "Round $i"
done

Always quote "$f" when you use the loop variable — filenames can contain spaces. To process the lines of a file safely, prefer a while read loop (next).

2. The while loop

A while loop repeats as long as a command keeps succeeding (exit 0):

count=1
while [[ $count -le 3 ]]; do
  echo "count is $count"
  count=$((count + 1))     # arithmetic with $(( ))
done

$(( ... )) does integer math. The most reliable way to read a file or command output line by line is:

while read -r line; do
  echo "log line: $line"
done < /var/log/syslog

This handles spaces and odd characters far better than a for loop over file contents. You can also pipe into it: df -h | while read -r line; do ...; done.

To stop early use break; to skip to the next iteration use continue:

for f in *.txt; do
  [[ -s $f ]] || continue       # skip empty files
  echo "processing $f"
done

3. Script arguments

When you run ./deploy.sh web01 prod, the words after the script name become positional parameters:

  ./deploy.sh   web01   prod
       $0        $1      $2
  • $0 — the script's own name.
  • $1, $2, ... — the first, second, ... argument.
  • $# — the count of arguments.
  • "$@"all arguments, each kept as a separate quoted word.
#!/usr/bin/env bash
echo "Script: $0"
echo "First arg: $1"
echo "Number of args: $#"

if [[ $# -lt 1 ]]; then
  echo "Usage: $0 <hostname> [environment]" >&2
  exit 1
fi

for arg in "$@"; do
  echo "Got argument: $arg"
done

Use "$@" (with quotes), not $* or unquoted $@. Quoted "$@" preserves arguments that contain spaces; the others mangle them. Checking $# up front and printing a usage line is the mark of a polished script.

4. Functions

A function is a named block of commands you can call repeatedly. It keeps scripts readable and avoids copy-paste:

#!/usr/bin/env bash

log() {
  echo "[$(date +%T)] $*"
}

check_dir() {
  local dir="$1"          # 'local' keeps the variable inside the function
  if [[ -d $dir ]]; then
    log "OK: $dir exists"
  else
    log "MISSING: $dir"
  fi
}

check_dir /etc
check_dir /nope

Key points:

  • Define a function before you call it.
  • Inside a function, arguments are $1, $2, "$@" — just like a script. The function above reads its argument as $1.
  • Use local for variables so they do not leak out and clobber others. This matters as scripts grow.
  • $* and "$@" inside a function refer to the function's own arguments.

5. Return values from functions

Functions return an exit code (0–255), not a string, via return. That code feeds straight into if:

is_running() {
  systemctl is-active --quiet "$1"   # exits 0 if the service is active
}

if is_running ssh; then
  echo "ssh is up"
else
  echo "ssh is down"
fi

To get data (a string or number) out of a function, have it echo the value and capture it with command substitution:

disk_pct() {
  df --output=pcent "$1" | tail -1 | tr -dc '0-9'
}

used=$(disk_pct /)
echo "Root filesystem is ${used}% full"

So: return for success/failure, echo + $( ) for actual values. Mixing these up is a classic beginner bug.

6. Basic arrays

An array holds an ordered list of values in one variable:

services=(ssh cron systemd-journald)

echo "${services[0]}"        # ssh           (first element, index 0)
echo "${services[@]}"        # all elements
echo "${#services[@]}"       # 3             (count of elements)

services+=(nginx)            # append an element

Loop over an array with quoted "${services[@]}":

for svc in "${services[@]}"; do
  if systemctl is-active --quiet "$svc"; then
    echo "$svc: up"
  else
    echo "$svc: DOWN"
  fi
done

Arrays are perfect for "a list of things to check" — services, hosts, directories — which is exactly what your health-check assignment needs.

7. Putting it together

#!/usr/bin/env bash
# Check several services passed as arguments; exit non-zero if any is down.
if [[ $# -eq 0 ]]; then
  echo "Usage: $0 <service> [service...]" >&2
  exit 1
fi

failed=0
for svc in "$@"; do
  if systemctl is-active --quiet "$svc"; then
    echo "$svc: up"
  else
    echo "$svc: DOWN" >&2
    failed=$((failed + 1))
  fi
done

echo "$failed service(s) down"
[[ $failed -eq 0 ]]      # last command's exit code becomes the script's

This takes a variable list of arguments, loops over them, counts failures, and sets a meaningful exit code — clean, reusable, and ready for Git.

Dig deeper

Search terms

  • bash for loop over files example
  • bash while read line file safely
  • bash positional parameters $1 $@ $#
  • bash quote "$@" vs $* difference
  • bash function return value vs echo
  • bash array loop ${array[@]}

Check yourself

  1. When looping over the lines of a file, why is while read -r line preferred over a for loop?
  2. What is the difference between $#, $1, and "$@"?
  3. Why should you write "$@" with quotes instead of $@ or $*?
  4. How does a function "return" a number versus an actual string value?
  5. Given services=(ssh cron nginx), how do you print the number of elements?