Lesson 03 of 8 · Modules
Functions & arguments
Structure scripts like small programs: functions with local variables and return codes, positional arguments, option parsing with getopts, a proper usage message, and messages on the right stream.
Functions: name a block of steps
#!/usr/bin/env bash
log() { # a tiny logger: timestamp + message, to stderr
printf '%s %s\n' "$(date +%T)" "$*" >&2
}
backup_dir() {
local src="$1" dest="$2" # local: invisible outside the function
[[ -d "$src" ]] || { log "no such directory: $src"; return 1; }
tar -czf "$dest" -C "$src" .
log "backed up $src to $dest"
}
backup_dir /etc/nginx "/tmp/nginx-$(date +%F).tgz" || exit 1
A function is a mini-recipe inside the big recipe: "make the sauce" can be written once and used in every dish. Arguments are the ingredients you hand it ("make the sauce with tomatoes"), and the return code says whether it worked ("sauce ready" or "burnt").
- Functions receive arguments as
$1,$2, … just like scripts. return Nsets the function's exit status (0 = success). To return data, print it and capture it:size=$(get_size /var/log).- Always declare function variables with
local.
Script arguments
#!/usr/bin/env bash
echo "script: $0"
echo "first: ${1:-<none>}"
echo "count: $#"
for arg in "$@"; do echo "arg: $arg"; done
$ ./args.sh "hello world" two
script: ./args.sh
first: hello world
count: 2
arg: hello world
arg: two
A usage message and option parsing with getopts
#!/usr/bin/env bash
# rotate.sh: compress logs older than N days in a directory
usage() {
cat >&2 <<EOF
Usage: $(basename "$0") [-d days] [-n] DIRECTORY
-d days age threshold (default: 7)
-n dry run: only print what would be compressed
-h this help
EOF
exit 2
}
days=7
dry_run=false
while getopts ":d:nh" opt; do
case "$opt" in
d) days="$OPTARG" ;;
n) dry_run=true ;;
h) usage ;;
:) echo "option -$OPTARG needs a value" >&2; usage ;;
\?) echo "unknown option -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1)) # drop the parsed options; $1 is now DIRECTORY
dir="${1:-}"
[[ -n "$dir" && -d "$dir" ]] || usage
[[ "$days" =~ ^[0-9]+$ ]] || { echo "-d must be a number" >&2; exit 2; }
find "$dir" -type f -name '*.log' -mtime +"$days" -print0 |
while IFS= read -r -d '' f; do
if $dry_run; then echo "would compress $f"; else gzip -- "$f"; fi
done
$ ./rotate.sh -n -d 30 /var/log/myapp
would compress /var/log/myapp/app-2026-08-01.log
Key details:
":d:nh": the leading:lets you handle errors yourself;d:means-dtakes a value.shift $((OPTIND - 1))removes processed options.- Validate inputs (is it a directory? is it a number?) and exit with a clear message.
find -print0withread -d ''handles file names containing spaces or newlines.gzip -- "$f":--ends options, so a file named-rfcan't be misread as options.
stdout for data, stderr for messages
echo "processing $f" >&2 # diagnostics → stderr
echo "$f,$size" # data → stdout (may be piped into another tool)
Then ./report.sh > report.csv captures clean data while progress messages still appear on screen.
Try it: a small CLI
Write userinfo.sh that:
- Accepts
-u USER(required) and-v(verbose). - Has functions
usage,log(to stderr, only when-v) anduser_exists(returns 0/1 usingid -u "$1" &>/dev/null). - Prints the user's UID, home directory and shell as
key=valuelines on stdout (hint:getent passwd "$user" | cut -d: -f3,6,7). - Exits 2 on bad usage and 1 if the user doesn't exist.
Test: ./userinfo.sh, ./userinfo.sh -u root -v, ./userinfo.sh -u nobody-here, and ./userinfo.sh -u root > out.txt (messages still on screen, data in the file).
Going deeper: script structure
- Put everything in functions and end the file with
main "$@". The script becomes easy to read top-down, and functions can be tested (lesson 07). getoptshandles short options only. For long options (--days), parse"$@"with awhile/caseloop, or switch to Python'sargparsewhen the interface grows.- Return distinct exit codes for distinct failures (2 = usage, 3 = dependency missing…) and document them. Callers such as cron, CI or systemd can react differently.
"$*"joins all arguments into one string (useful for log messages);"$@"keeps them separate (use it for passing arguments on).
Recap
- Functions:
name() { … },localvariables,returnfor status, print +$(…)for data. - Arguments:
$1…,"$@",$#,shift,${1:-default}. - getopts for options, a usage() function, and validated inputs.
- stdout = data, stderr = messages; use
--and-print0for untrusted file names.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.