Linux: Bash Scripting Advanced
Basic scripts can handle simple tasks, but production-grade scripts need more robust code — functions to split logic, error handling to prevent "silent failures," and parameter parsing to make scripts more usable.
📋 Prerequisites: You should already know
- Lesson 20: Bash Scripting Basics
1. What You'll Learn
- Function definition and return values
- Indexed and associative arrays
- set -e and trap error handling
- getopts command-line parameters
- set -x debugging techniques
2. A Story of Xiaoming's Script Running Wild on the Server
(1) The Pain Point: Script Continues Running After Errors
Xiaoming wrote a deployment script:
#!/bin/bash
cd /var/www/myapp
git pull origin main
npm install # ❌ This failed
npm run build # But the script kept going
pm2 restart app # Restarted with old code and broken build
(2) Adding One Line Makes It Safe
Bob said: "Add set -e at the top — it stops on any error."
#!/bin/bash
set -e # Stop on error
cd /var/www/myapp
git pull origin main
npm install
npm run build
pm2 restart app
(3) The Benefit: Safety First
With set -e, if npm install fails, the script exits immediately instead of restarting the service with broken code.
3. Knowledge Points
(1) Functions
# Define a function
greet() {
echo "Hello, $1!"
echo "Today is $(date)"
}
# Alternative syntax
function greet {
echo "Hello, $1!"
}
# Call it
greet "Alice"
# Local variables
myfunc() {
local x=10 # Local variable, doesn't affect outer scope
echo "Inside: $x"
}
x=5
myfunc # Output: Inside: 10
echo "Outside: $x" # Output: Outside: 5
(2) Arrays
# Indexed array
fruits=("apple" "banana" "orange")
fruits[3]="grape" # Add element
echo ${fruits[0]} # apple
echo ${fruits[@]} # All elements
echo ${#fruits[@]} # Array length
# Iterate over array
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Associative array (like a dictionary)
declare -A config
config[host]="localhost"
config[port]="8080"
config[user]="admin"
echo ${config[host]} # localhost
(3) Error Handling
# set -e: Exit on error
set -e
# set -u: Error on undefined variable
set -u
# set -o pipefail: Pipeline fails if any command fails
set -o pipefail
# Common combination
set -euo pipefail
> 💡 Tip: `set -euo pipefail` is the "safety trio" for production scripts — `-e` stops on error, `-u` catches undefined variables, `-o pipefail` makes pipeline failures propagate. All non-trivial scripts should include this line.
# trap: Execute cleanup on signal or exit
trap 'echo "Script interrupted"; exit 1' INT TERM
trap 'rm -f /tmp/tempfile; echo "Cleanup done"' EXIT
> 💡 Tip: `trap` can automatically execute cleanup when a script exits — whether it ends normally or is interrupted by Ctrl+C, `trap EXIT` is triggered. Common uses: deleting temporary files, releasing file locks, restoring configs. Write `trap 'rm -f $TMPFILE' EXIT` at the top and you won't need manual cleanup at every exit point.
# Custom error handler
error_handler() {
echo "❌ Error on line $1"
echo "Command: $2"
exit 1
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
(4) getopts — Parameter Parsing
# getopts syntax
while getopts "f:o:hv" opt; do
case $opt in
f) INPUT_FILE="$OPTARG" ;; # -f filename
o) OUTPUT_DIR="$OPTARG" ;; # -o output
h) show_help; exit 0 ;; # -h help
v) VERBOSE=true ;; # -v verbose
*) echo "Unknown option"; exit 1 ;;
esac
done
> ℹ️ Note: `getopts` is bash's built-in parameter parser — in the option string `"f:o:hv"`, a colon `:` means the option requires an argument (e.g., `-f filename`), no colon means it's a flag option (e.g., `-v`). It doesn't support long options (like `--help`); for those, use the external `getopt` command or manual parsing.
(5) Debugging Techniques
# Debug mode: show each command as it executes
set -x
# Turn off debugging
set +x
# Enable debugging for a specific section
echo "Normal execution"
set -x
echo "This section is traced"
for i in 1 2 3; do echo "$i"; done
set +x
echo "Back to normal"
# Or debug at script startup
# bash -x script.sh
(6) ▶ Example: Function Encapsulation
#!/bin/bash
# functions-demo.sh - Function examples
log() {
local level="${1:-INFO}"
local message="$2"
echo "[$(date '+%H:%M:%S')] [$level] $message"
}
die() {
log "ERROR" "$1"
exit 1
}
check_dependency() {
if ! command -v "$1" &> /dev/null; then
die "Required command '$1' not found"
fi
}
# Using the functions
log "INFO" "Starting dependency check..."
check_dependency "git"
check_dependency "node"
log "INFO" "Dependency check passed"
(7) ▶ Example: Array Operations
#!/bin/bash
# array-demo.sh - Array examples
# Server list
servers=("web01" "web02" "db01" "cache01")
echo "Server count: ${#servers[@]}"
echo "--- Iterating servers ---"
for server in "${servers[@]}"; do
echo "Checking: $server"
done
echo "--- Associative array example ---"
declare -A disk_usage
disk_usage[/]="45%"
disk_usage[/home]="62%"
disk_usage[/var]="78%"
for mount in "${!disk_usage[@]}"; do
echo "$mount: ${disk_usage[$mount]}"
done
Output:
TEXT[10:23:01] [INFO] Starting dependency check... [10:23:01] [INFO] Dependency check passed Server count: 4 --- Iterating servers --- Checking: web01 Checking: web02 Checking: db01 Checking: cache01 --- Associative array example --- /: 45% /home: 62% /var: 78%
(8) ▶ Example: set -e / trap Error Handling
#!/bin/bash
# safe-script.sh - Robust script
set -euo pipefail
cleanup() {
echo "Cleaning temporary files..."
rm -f /tmp/temp_*.txt
echo "Done"
}
error_handler() {
echo "❌ Error occurred on line $1"
cleanup
exit 1
}
trap error_handler ERR
trap cleanup EXIT
echo "Starting execution..."
mkdir -p /tmp/test
touch /tmp/test/file.txt
cat /tmp/test/file.txt
# If cat fails on a non-existent file, the script stops and triggers ERR trap
Output:
TEXTStarting execution... Cleaning temporary files... Done
(9) ▶ Example: getopts Parameter Parsing
#!/bin/bash
# backup-tool.sh - Backup tool with parameter parsing
usage() {
echo "Usage: $0 -s <source_dir> -d <dest_dir> [-c] [-v]"
echo " -s Source directory to back up"
echo " -d Backup destination directory"
echo " -c Compress backup"
echo " -v Verbose output"
echo " -h Show help"
exit 0
}
# Default values
COMPRESS=false
VERBOSE=false
while getopts "s:d:cvh" opt; do
case $opt in
s) SOURCE="$OPTARG" ;;
d) DEST="$OPTARG" ;;
c) COMPRESS=true ;;
v) VERBOSE=true ;;
h) usage ;;
*) usage ;;
esac
done
# Validate required parameters
if [ -z "$SOURCE" ] || [ -z "$DEST" ]; then
echo "Error: Must specify -s and -d"
usage
fi
if [ ! -d "$SOURCE" ]; then
echo "Error: Source directory '$SOURCE' does not exist"
exit 1
fi
# Execute backup
mkdir -p "$DEST"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}"
if [ "$COMPRESS" = true ]; then
tar -czf "${DEST}/${BACKUP_NAME}.tar.gz" "$SOURCE"
echo "✅ Compressed backup complete: ${DEST}/${BACKUP_NAME}.tar.gz"
else
cp -r "$SOURCE" "${DEST}/${BACKUP_NAME}"
echo "✅ Backup complete: ${DEST}/${BACKUP_NAME}"
fi
if [ "$VERBOSE" = true ]; then
echo "Source: $SOURCE"
echo "Destination: $DEST"
echo "Size: $(du -sh "$SOURCE" | cut -f1)"
fi
Output:
TEXTUsage: ./backup-tool.sh -s <source_dir> -d <dest_dir> [-c] [-v] -s Source directory to back up -d Backup destination directory -c Compress backup -v Verbose output -h Show help ✅ Compressed backup complete: /backup/backup_20260707_102345.tar.gz Source: /var/www/myapp Destination: /backup Size: 12M
(10) ▶ Example: Debugging Techniques
#!/bin/bash
# debug-demo.sh - Debugging example
# Only enable tracing where debugging is needed
echo "Script started PID: $$"
for i in 1 2 3; do
set -x # Start tracing
echo "Loop iteration $i"
result=$((i * 10))
echo "Result: $result"
set +x # Stop tracing
done
echo "Script execution complete"
(11) ▶ Comprehensive Example: Production-Grade Deployment Script
#!/bin/bash
# deploy.sh - Production-grade deployment script
set -euo pipefail
APP_NAME="webapp"
APP_DIR="/var/www/$APP_NAME"
BACKUP_DIR="/backup/$APP_NAME"
LOG_FILE="/var/log/deploy.log"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
log() {
local level="$1"
local msg="$2"
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${msg}" | tee -a "$LOG_FILE"
}
info() { log "INFO" "$1"; }
success() { log "INFO" "${GREEN}$1${NC}"; }
error() { log "ERROR" "${RED}$1${NC}"; }
cleanup() {
info "Cleaning deployment temporary files..."
}
error_handler() {
error "Deployment failed at line $1"
error "Command: $BASH_COMMAND"
cleanup
exit 1
}
usage() {
echo "Usage: $0 [options]"
echo " -b Specify Git branch (default: main)"
echo " -e Specify environment (dev/staging/prod)"
echo " -n Skip backup"
echo " -h Show help"
exit 0
}
trap error_handler ERR
trap cleanup EXIT
# --- Parameter parsing ---
BRANCH="main"
ENV="prod"
SKIP_BACKUP=false
while getopts "b:e:nh" opt; do
case $opt in
b) BRANCH="$OPTARG" ;;
e) ENV="$OPTARG" ;;
n) SKIP_BACKUP=true ;;
h) usage ;;
*) usage ;;
esac
done
# --- Environment check ---
info "Starting deployment of $APP_NAME (branch: $BRANCH, environment: $ENV)"
if [ ! -d "$APP_DIR" ]; then
error "Application directory $APP_DIR does not exist"
exit 1
fi
# --- Backup ---
if [ "$SKIP_BACKUP" = false ]; then
info "Backing up current version..."
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
tar -czf "${BACKUP_DIR}/${TIMESTAMP}.tar.gz" \
--exclude=node_modules \
--exclude=.git \
"$APP_DIR"
success "Backup complete: ${BACKUP_DIR}/${TIMESTAMP}.tar.gz"
fi
# --- Deploy ---
cd "$APP_DIR"
info "Pulling latest code..."
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
info "Installing dependencies..."
npm ci --production
info "Building application..."
if [ "$ENV" = "production" ]; then
npm run build:prod
else
npm run build
fi
info "Restarting service..."
pm2 restart "$APP_NAME" || pm2 start npm --name "$APP_NAME" -- start
# --- Health check ---
info "Running health check..."
sleep 3
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health | grep -q "200"; then
success "✅ Deployment successful! Health check passed"
else
error "❌ Health check failed, please investigate manually"
exit 1
fi
❓ FAQ
Q: What's the difference between functions and aliases? A: Functions are more powerful than aliases: they support parameters, local variables, return values, and reuse. Aliases are just simple command substitutions. Use functions for complex logic, aliases for simple shortcuts.
Q: When does
set -enot work? A:set -edoesn't trigger in these cases: 1) In if/while/until condition expressions. 2) After&&or||. 3) In pipelines (addset -o pipefail). 4) When a command is negated with!.
Q: Which signals can trap catch? A: Common ones:
EXIT(script exits),ERR(command fails),INT(Ctrl+C),TERM(defaultkillsignal). Others includeHUP,QUIT,USR1,USR2.
Q: What's the difference between getopts and getopt? A:
getoptsis a bash built-in, simpler, doesn't support long options (like--help).getoptis an external command (fromutil-linuxpackage) that supports mixing long and short options. Prefergetoptsfor compatibility.
Q: How to create temporary files in a script? A: Use
mktemp:tempfile=$(mktemp)creates a temp file,tempdir=$(mktemp -d)creates a temp directory. Temp files are usually created in/tmp; remember to delete them when done.
📖 Summary
- Functions
func() { local vars; return N; }encapsulate logic - Arrays:
${arr[@]}iterate,${#arr[@]}length,declare -Aassociative arrays set -euo pipefailmakes scripts robust,trap ERR EXITcatches errors and cleans upgetoptsparses command-line parametersset -x/+xdebug,bash -x script.shruns in debug mode
📝 Exercises
- Basic (⭐): Refactor the Lesson 20 backup script using functions (log/die/check_dependency etc.), and add
set -euo pipefailandtraperror handling - Intermediate (⭐⭐): Add
-s(source directory) and-d(destination directory) parameters usinggetopts, create an associative array to store config, and iterate to output all config items - Challenge (⭐⭐⭐): Use
mktempto create a temporary directory that auto-cleans on completion, and write a production-grade deployment script — with parameter parsing, colored logging, error handling, and health check