Script Structure & Argument Parsing¶
A well-structured script with proper argument parsing and a --help flag is the difference between a tool someone else can use and one that only you understand.
Learning Objectives¶
- Write a complete, production-ready script header
- Use
set -euo pipefailand understand what each flag does - Parse arguments with
getoptsand manually withcase - Write a
usage()function and display it with--help - Handle required vs optional arguments correctly
The Standard Script Header¶
Every production script starts with this pattern:
#!/usr/bin/env bash
set -euo pipefail
# --- constants ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "$0")"
# --- usage ---
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] <required_arg>
Description of what the script does.
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-o, --output DIR Write output to DIR (default: ./output)
Examples:
$SCRIPT_NAME input.txt
$SCRIPT_NAME -v -o /tmp input.txt
EOF
}
# --- defaults ---
VERBOSE=false
OUTPUT_DIR="./output"
set -euo pipefail Explained¶
| Flag | Meaning | Why it matters |
|---|---|---|
-e |
Exit on error | A failing command won't silently continue |
-u |
Error on unset variables | Typos in variable names become errors, not empty strings |
-o pipefail |
Pipeline fails if any stage fails | false | true returns non-zero, not zero |
set -euo pipefail
# With -e: this exits the script instead of continuing
cp /nonexistent/file /tmp/ # script stops here
# With -u: this is an error instead of an empty string
echo "$TYPO_VAR" # error: TYPO_VAR: unbound variable
# With pipefail: this returns a non-zero exit code
false | true
echo $? # 1, not 0
-e can be surprising
Some commands legitimately return non-zero (e.g., grep returns 1 when no match is found). Guard these with || true or if: grep "pattern" file || true.
Argument Parsing with case¶
For simple scripts, manual case parsing is the clearest approach:
#!/usr/bin/env bash
set -euo pipefail
VERBOSE=false
OUTPUT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-o|--output)
OUTPUT="${2:?--output requires an argument}"
shift 2
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
break
;;
esac
done
# Remaining args are positional
if [[ $# -eq 0 ]]; then
echo "Error: required argument missing" >&2
usage >&2
exit 1
fi
INPUT="$1"
getopts — POSIX Argument Parsing¶
getopts handles short options only (single letters):
while getopts ":hvo:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) VERBOSE=true ;;
o) OUTPUT="$OPTARG" ;;
:) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
?) echo "Unknown option: -$OPTARG" >&2; exit 1 ;;
esac
done
shift $(( OPTIND - 1 ))
getopts vs manual case parsing
Use getopts for POSIX portability and when only short options are needed. Use manual case parsing when you need long options (--verbose, --output) without adding external dependencies.
The Complete Script Template¶
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "$0")"
VERBOSE=false
usage() {
cat << EOF
Usage: $SCRIPT_NAME [-h] [-v] <input>
EOF
}
log() { "$VERBOSE" && echo "[$SCRIPT_NAME] $*" || true; }
die() { echo "ERROR: $*" >&2; exit 1; }
warn() { echo "WARN: $*" >&2; }
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) VERBOSE=true; shift ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
[[ $# -ge 1 ]] || die "Missing required argument. Run with --help."
INPUT="$1"
}
main() {
parse_args "$@"
log "Processing: $INPUT"
echo "Done."
}
main "$@"
Practice Exercises¶
Main (write a short script)¶
Write a script ~/scripts/template.sh that uses the full template above and accepts:
- -v / --verbose flag
- -o DIR / --output DIR option (defaults to /tmp)
- One required positional argument (a filename to process)
The script should print the filename's line count when run normally, and additional details when verbose.
Stretch¶
- Add a
--dry-runflag to your script that prints what would be done without doing it. - Research
BASH_SOURCE[0]vs$0. Why isBASH_SOURCE[0]preferred for finding the script's own directory?
Interview Questions¶
- What does
set -euo pipefaildo?
Show answer
-e exits immediately when any command returns non-zero. -u treats unset variables as errors. -o pipefail makes a pipeline fail if any command in it fails (not just the last). Together they catch most silent failure modes in bash scripts.
- Why use
#!/usr/bin/env bashinstead of#!/bin/bash?
Show answer
#!/bin/bash hardcodes the path to bash. On some systems (macOS with Homebrew, NixOS, some containers), bash is in a different location. #!/usr/bin/env bash searches $PATH for bash, making the script more portable across different environments.
- How do you handle
grepreturning exit code 1 (no matches) whenset -eis active?
Show answer
Use grep "pattern" file || true to prevent the non-zero exit from stopping the script. Or use if grep "pattern" file; then ...; fi — the if construct does not trigger -e on failure. Or temporarily disable with set +e; grep ...; set -e.