Chapter 14Lesson 03~70 minutes

Loops, Functions, Arguments, and Exit Codes

Compose reusable Bash operations, iterate over structured inputs, preserve positional arguments, and return statuses that callers and automation can interpret reliably.

FunctionsIterationExit contracts

Learning objectives

By the end of this lesson

  • Iterate safely with for, while, and until.
  • Design functions with local state, explicit arguments, output, and return status.
  • Use positional parameters, "$@", $#, and shift correctly.
  • Choose meaningful exit statuses for usage, validation, and operational failures.
  • Build a batch checker that continues, summarizes, and reports partial failure.

1. Loops repeat commands; functions define operational units

A loop controls repetition, while a function gives a group of commands a name and a local scope. Combining them lets a script apply one reviewed operation to many inputs. The design question is not merely “how do I loop?” It is “what is one unit of work, what inputs does it receive, what output does it produce, and how does failure propagate?”

Batch-script control and status flow
flowchart TD
  A["Receive positional arguments"] --> V{"Arguments valid?"}
  V -- no --> U["Print usage and return 2"]
  V -- yes --> L["Iterate over each input"]
  L --> F["Call one unit-of-work function"]
  F --> S{"Function succeeded?"}
  S -- yes --> OK["Record success"]
  S -- no --> BAD["Record failure and continue or stop by policy"]
  OK --> M{"More inputs?"}
  BAD --> M
  M -- yes --> L
  M -- no --> R["Print summary and return aggregate status"]

Keep functions cohesive. A function that validates, downloads, edits system state, restarts a service, and formats a report is difficult to test and impossible to reuse safely. Separate observation, decision, mutation, and presentation when the operation matters.

2. Positional parameters preserve the caller’s argument vector

Inside a script or function, $0 identifies the script or shell context, $1 through $9 access early positional parameters, ${10} and beyond require braces, $# is the count, and "$@" expands all arguments while preserving boundaries. shift removes processed positional parameters.

print_arguments() {
  local index=0 argument
  printf 'count=%d\n' "$#"

  for argument in "$@"; do
    (( index += 1 ))
    printf 'argument[%d]=<%s>\n' "$index" "$argument"
  done
}

print_arguments 'one' 'two words' '*.log' ''

consume_prefix() {
  local prefix=$1
  shift
  printf 'prefix=%s remaining=%d\n' "$prefix" "$#"
  printf 'remaining=<%s>\n' "$@"
}

consume_prefix INFO alpha 'beta gamma' delta

Never use unquoted $* or $@ to forward arbitrary arguments. "$@" is the standard forwarding form because each original argument remains separate, including empty arguments.

3. Functions return status and may write output

Bash functions do not return arbitrary strings through return. They return an integer status from 0 through 255. Data can be written to standard output, assigned through a caller-provided variable, or stored in a controlled global, but each choice has consequences.

file_size_bytes() {
  if (( $# != 1 )); then
    printf 'file_size_bytes requires one path\n' >&2
    return 2
  fi

  local path=$1
  if [[ ! -f $path ]]; then
    printf 'Not a regular file: %s\n' "$path" >&2
    return 3
  fi

  stat --format='%s' -- "$path"
}

if size=$(file_size_bytes '/etc/hosts'); then
  printf '/etc/hosts size=%s bytes\n' "$size"
else
  status=$?
  printf 'Size lookup failed with status %d\n' "$status" >&2
fi

Command substitution captures standard output, so diagnostic messages must go to standard error or they contaminate the returned data. When output may contain trailing newlines or large binary data, use a file, array, or caller variable instead of command substitution.

4. Select a loop according to input and termination

LoopUse whenTypical hazard
for item in ...You already have a finite argument or array listGenerating the list through unquoted command substitution
while conditionRepeat while a command succeedsPipeline subshells losing variable changes
until conditionRetry until a condition succeedsUnbounded retries without delay or deadline
for ((...))Integer counters and bounded arithmetic iterationOff-by-one boundaries
# Iterate over caller-provided paths.
for path in "$@"; do
  printf 'path=%s\n' "$path"
done

# Read text without treating backslashes specially.
while IFS= read -r line; do
  printf 'line=<%s>\n' "$line"
done < /etc/os-release

# Bounded retry loop. Chapter 15 develops production retry policy further.
attempt=1
until getent hosts example.invalid >/dev/null 2>&1; do
  if (( attempt >= 3 )); then
    printf 'Name resolution did not succeed after %d attempts\n' "$attempt" >&2
    break
  fi
  sleep 1
  (( attempt += 1 ))
done

When a while read loop is the last element of a pipeline, it may execute in a subshell, so assignments may not survive after the loop. Redirect a file into the loop or use process substitution when the outer shell must retain state.

5. Exit status is an API for callers

Status zero means success; nonzero means some form of failure or negative result. Define a small status contract rather than returning whichever command happened to run last. Status 2 is commonly used for command-line usage errors. Status 126 often means a command was found but could not execute, 127 means command not found, and statuses above 128 commonly represent termination by a signal, but scripts should document the values they intentionally emit.

main() {
  if (( $# == 0 )); then
    printf 'Usage: %s FILE...\n' "${0##*/}" >&2
    return 2
  fi

  local failures=0 path
  for path in "$@"; do
    if [[ -r $path ]]; then
      printf 'OK %s\n' "$path"
    else
      printf 'FAIL %s\n' "$path" >&2
      (( failures += 1 ))
    fi
  done

  (( failures == 0 )) || return 1
}

main "$@"

A batch script must decide whether to stop on first failure or continue and return an aggregate result. Continuing can provide a complete report; stopping can prevent harmful dependent actions. Make the policy explicit.

6. Hands-on lab: build a batch file-health checker

The checker accepts one or more paths, delegates each check to a function, keeps per-result counts, and returns nonzero when any input fails policy.

lab="$HOME/devops-academy/linux/chapter14/lesson03"
mkdir -p "$lab"
cd "$lab"

cat > check-files.sh <<'SCRIPT'
#!/usr/bin/env bash

check_file() {
  if (( $# != 1 )); then
    printf 'check_file requires one path\n' >&2
    return 2
  fi

  local path=$1

  if [[ ! -e $path ]]; then
    printf 'MISSING\t%s\n' "$path"
    return 1
  fi
  if [[ ! -f $path ]]; then
    printf 'NOT_FILE\t%s\n' "$path"
    return 1
  fi
  if [[ ! -r $path ]]; then
    printf 'UNREADABLE\t%s\n' "$path"
    return 1
  fi
  if [[ ! -s $path ]]; then
    printf 'EMPTY\t%s\n' "$path"
    return 1
  fi

  printf 'OK\t%s\t%s bytes\n' "$path" "$(stat -c %s -- "$path")"
}

main() {
  if (( $# == 0 )); then
    printf 'Usage: %s FILE...\n' "${0##*/}" >&2
    return 2
  fi

  local checked=0 passed=0 failed=0 path
  for path in "$@"; do
    (( checked += 1 ))
    if check_file "$path"; then
      (( passed += 1 ))
    else
      (( failed += 1 ))
    fi
  done

  printf 'SUMMARY checked=%d passed=%d failed=%d\n' \
    "$checked" "$passed" "$failed"
  (( failed == 0 ))
}

main "$@"
SCRIPT

chmod u+x check-files.sh
printf 'healthy\n' > healthy.txt
: > empty.txt

./check-files.sh healthy.txt empty.txt missing.txt || status=$?
printf 'aggregate_status=%d\n' "${status:-0}"
bash -n check-files.sh

Verification checklist

7. Common loop and function mistakes

“for f in $(find ...) is a file loop.”

Command substitution and word splitting corrupt filenames. Use arrays, find -print0, or a redirected while read loop.

“return sends a string to the caller.”

return sets a numeric status. Standard output is a separate data channel.

“The last command’s status is good enough.”

Refactoring can silently change it. Return the status that belongs to the function’s documented contract.

“A retry loop can continue until it works.”

Production retries need a bound, delay, deadline, diagnostics, and a distinction between transient and permanent failures.

8. Knowledge check

Question 1. Why should a wrapper function forward arguments as command "$@"?

Question 2. How does a function return data and status at the same time?

Question 3. What is the advantage of an aggregate batch status?

9. Summary

Functions turn operational steps into named units with arguments, local state, output, and status. Loops apply those units to structured input. Preserve arguments with "$@", choose loop termination deliberately, keep diagnostics separate from returned data, and define exit statuses as a stable interface for humans, CI jobs, and parent scripts.

10. Further reading

Next lesson

Arrays, Command Substitution, and Input Handling

Preserve argument and record boundaries with arrays, delimiters, read loops, and controlled substitutions.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.