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.
Learning objectives
By the end of this lesson
- Iterate safely with
for,while, anduntil. - Design functions with local state, explicit arguments, output, and return status.
- Use positional parameters,
"$@",$#, andshiftcorrectly. - 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?”
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' deltaNever 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
fiCommand 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
for item in ...You already have a finite argument or array listGenerating the list through unquoted command substitutionwhile conditionRepeat while a command succeedsPipeline subshells losing variable changesuntil conditionRetry until a condition succeedsUnbounded retries without delay or deadlinefor ((...))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 ))
doneWhen 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.shVerification 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
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.