Stage 3 · Build
Shell Scripting & Automation
Bash Script Structure
Functions, traps, exit codes, arrays, and argument parsing with getopts.
Script Skeleton
#!/usr/bin/env bash
set -euo pipefail
# Script metadata
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly VERSION="1.0.0"
# Global variables
LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"
VERBOSE=false
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] <ARGUMENT>
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-V, --version Show version
-o, --output Output file
Example:
$SCRIPT_NAME -v -o result.txt input.txt
EOF
}
log() {
local level=$1; shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}
main() {
# Script logic goes here
log INFO "Starting $SCRIPT_NAME v$VERSION"
# ...
log INFO "Completed successfully"
}
main "$@"Every production script should have: shebang, strict mode, usage function, logging function, and a main function. This structure makes scripts maintainable and debuggable.
Functions
# Function definition
function greet() {
local name=$1
echo "Hello, $name"
}
# Shorthand definition
greet() {
local name=$1
echo "Hello, $name"
}
# Return values vs output
get_value() {
echo "result" # Capture with: result=$(get_value)
return 0 # Return code only (0-255)
}
# Local variables (always use local!)
calculate() {
local input=$1
local result=$((input * 2))
echo $result
}
# Function with named arguments
deploy() {
local env=""
local version=""
local dry_run=false
while [[ $# -gt 0 ]]; do
case $1 in
-e|--env) env=$2; shift 2 ;;
-v|--version) version=$2; shift 2 ;;
-n|--dry-run) dry_run=true; shift ;;
*) echo "Unknown: $1"; return 1 ;;
esac
done
echo "Deploying $version to $env (dry_run=$dry_run)"
}Always use local variables in functions. Return codes indicate success/failure; use echo for actual return values.
Traps and Cleanup
# Cleanup function
cleanup() {
local exit_code=$?
rm -f "$TEMP_FILE" 2>/dev/null
rm -rf "$TEMP_DIR" 2>/dev/null
echo "Cleaned up (exit code: $exit_code)"
exit $exit_code
}
# Set traps
trap cleanup EXIT INT TERM HUP
# Create temp files
TEMP_FILE=$(mktemp)
TEMP_DIR=$(mktrap -d)
# trap ERR for error handling
trap 'echo "Error on line $LINENO"; exit 1' ERR
# Ignore signals
trap '' INT # Ignore Ctrl+C
trap - INT # Reset INT handling
# Debug trap
trap 'echo "Executing: $BASH_COMMAND"' DEBUGThe EXIT trap runs when the script exits for any reason. Use it for cleanup of temp files, locks, and temporary mounts.
Exit Codes
# Standard exit codes
exit 0 # Success
exit 1 # General error
exit 2 # Misuse of shell command
exit 126 # Command cannot execute (permission denied)
exit 127 # Command not found
exit 128+N # Fatal error signal N (e.g., 130 = SIGINT = Ctrl+C)
exit 255 # Exit code out of range
# Custom exit codes
readonly ERR_CONFIG=1
readonly ERR_PERMISSION=2
readonly ERR_NETWORK=3
check_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Config file not found: $CONFIG_FILE" >&2
return $ERR_CONFIG
fi
}
# Trap exit codes
trap 'echo "Failed with exit code $?"; exit $?' ERRUse descriptive exit codes for different failure modes. Document them in the usage message for operators.
Arrays
# Indexed arrays
declare -a fruits
fruits=("apple" "banana" "cherry")
# Sparse arrays
declare -a sparse
sparse[0]="zero"
sparse[5]="five"
# Array operations
echo "${fruits[0]}" # First element
echo "${fruits[@]}" # All elements
echo "${#fruits[@]}" # Length
fruits+=("date") # Append
unset 'fruits[1]' # Remove element
# Iterate
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
# Associative arrays (Bash 4+)
declare -A config
config[host]="localhost"
config[port]="5432"
config[db]="mydb"
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
doneUse indexed arrays for lists and associative arrays for key-value data. Always quote ${array[@]} to preserve element boundaries.
Argument Parsing with getopts
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $(basename "$0") [-h] [-v] [-o output] [-e env] file"
exit 1
}
VERBOSE=false
OUTPUT=""
ENV="production"
FILES=()
while getopts "hvo:e:" opt; do
case $opt in
h) usage ;;
v) VERBOSE=true ;;
o) OUTPUT=$OPTARG ;;
e) ENV=$OPTARG ;;
?) usage ;;
esac
done
shift $((OPTIND - 1))
# Remaining arguments
if [[ $# -eq 0 ]]; then
echo "Error: No input file specified" >&2
usage
fi
FILES=("$@")
echo "Output: $OUTPUT"
echo "Env: $ENV"
echo "Files: ${FILES[*]}"getopts provides POSIX-compatible option parsing. For long options (--verbose), use getopt or manual parsing with a while loop.
set -e exits on error, -u exits on undefined variable, -o pipefail catches pipe errors. This catches most bugs early.
Unquoted variables cause word splitting and glob expansion. Use $VAR for scalar values and "${array[@]}" for arrays.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.