Argument handling separates throwaway scripts from real tools. A few patterns cover everything you need.
Positional Arguments
#!/usr/bin/env bash
set -euo pipefail
src="$1"
dest="$2"
echo "from $src to $dest"
Use defaults to make optional positionals safe under set -u:
src="${1:-}"
dest="${2:-/tmp}"
# or fail with a message
src="${1:?usage: $0 SRC [DEST]}"
$@ vs $*
myfn() {
for a in "$@"; do echo "[$a]"; done
}
myfn "one two" three
# [one two]
# [three] ← spaces preserved, args separate
# vs $*:
for a in "$*"; do echo "[$a]"; done
# [one two three] ← all collapsed
Always use "$@" (with quotes) to pass arguments through unchanged. $* is rarely what you want.
shift
shift drops $1, renumbers the rest. Useful for consuming options:
cmd="$1"; shift # remaining args belong to cmd
case "$cmd" in
build) do_build "$@" ;;
deploy) do_deploy "$@" ;;
*) echo "unknown: $cmd"; exit 2 ;;
esac
getopts (Short Flags)
getopts is a Bash builtin for POSIX-style single-letter flags:
verbose=false
file=""
count=1
while getopts ":vf:c:h" opt; do
case "$opt" in
v) verbose=true ;;
f) file="$OPTARG" ;;
c) count="$OPTARG" ;;
h) usage; exit 0 ;;
\?) echo "Invalid option: -$OPTARG" >&2; usage; exit 2 ;;
:) echo "Option -$OPTARG requires an argument" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))
echo "verbose=$verbose file=$file count=$count remaining=$*"
Notes:
- The optstring
":vf:c:h"means: leading:activates "silent error reporting";vandhtake no argument;f:andc:take an argument. OPTARGholds the value;OPTINDis the next argument index.- After the loop,
shift $((OPTIND - 1))drops parsed flags so$@is the positional remainder.
Long Options (--verbose)
getopts doesn't support long options. Write a small loop:
verbose=false
file=""
count=1
positional=()
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
verbose=true
shift
;;
-f|--file)
file="$2"
shift 2
;;
--file=*)
file="${1#*=}"
shift
;;
-c|--count)
count="$2"
shift 2
;;
-h|--help)
usage; exit 0
;;
--)
shift
positional+=("$@")
break
;;
-*)
echo "Unknown option: $1" >&2
exit 2
;;
*)
positional+=("$1")
shift
;;
esac
done
set -- "${positional[@]}"
echo "remaining: $*"
This handles --file foo, --file=foo, -f foo, mixing flags with positional arguments, and the conventional -- end-of-options marker.
A Usage Function
usage() {
cat <<EOF
Usage: $0 [options] SRC DEST
Options:
-v, --verbose Verbose output
-f, --file FILE Use FILE as input
-c, --count N Repeat N times (default 1)
-h, --help Show this help
EOF
}
Always print to stderr when usage results from an error; to stdout when from --help.
Subcommands (git-style)
cmd="${1:-}"
[[ -n "$cmd" ]] || { usage; exit 2; }
shift
case "$cmd" in
build) build "$@" ;;
deploy) deploy "$@" ;;
status) status "$@" ;;
help|-h|--help) usage ;;
*) echo "unknown command: $cmd" >&2; exit 2 ;;
esac
Each subcommand can have its own argument parsing — same patterns recursively.
Reading from stdin
if [[ ! -t 0 ]]; then
# stdin is a pipe, read it
while IFS= read -r line; do
process "$line"
done
else
# stdin is a terminal — use args instead
for arg in "$@"; do
process "$arg"
done
fi
Tools that work both as filters and with arguments are extra useful.
Environment Variables for Config
For deployment-like scripts, environment variables often beat flags:
: "${LOG_LEVEL:=info}" # default if unset
: "${RETRIES:=3}"
: "${TIMEOUT:=30}"
: "${API_TOKEN:?must be set}"
echo "log=$LOG_LEVEL retries=$RETRIES"
: "${VAR:=default}" sets a default; : "${VAR:?msg}" errors if unset. Both are no-ops besides their side effect on the variable.
Cert Mapping
| Cert | Scope |
|---|---|
| RHCSA / LFCS | Writing parameterised admin scripts |
| AWS / Azure | Build / deploy CLI helpers |
The next lesson covers running scripts on a schedule with cron and systemd timers.