Table of Contents
- Mastering Variables and Parameter Expansion
- Advanced Conditional Logic
- Efficient Loops for Scalable Automation
- Functions: Reusability & Modularity
- Process Substitution: Beyond Pipes
- Traps: Cleanup & Error Handling
- Advanced Redirection: Taming Output
- Scheduling with Cron & Systemd Timers
- Debugging Like a Pro
- Real-World Example: Automated Backup Script
- Conclusion
- References
1. Mastering Variables and Parameter Expansion
Variables are the building blocks of Bash scripts, but most users only scratch the surface of their capabilities. Parameter expansion unlocks dynamic manipulation of variable values, making scripts more flexible and robust.
Key Techniques:
-
Default Values: Set fallback values if a variable is unset or empty:
# Syntax: ${VAR:-default} BACKUP_DIR="${1:-/var/backups}" # Use first argument, or /var/backups if missing -
Substring Removal: Trim prefixes/suffixes from variables (no need for
sed!):FILE="report_2024-05-20.log" echo "${FILE#report_}" # Removes prefix "report_": "2024-05-20.log" echo "${FILE%.log}" # Removes suffix ".log": "report_2024-05-20" -
String Replacement: Replace text within variables:
PATH="/usr/local/bin:/usr/bin" echo "${PATH/bin/BIN}" # Replace first "bin" with "BIN": "/usr/local/BIN:/usr/bin" echo "${PATH//bin/BIN}" # Replace all "bin" with "BIN": "/usr/local/BIN:/usr/BIN" -
Length of Variable: Get the length of a string (useful for validation):
PASSWORD="secure123" if [ ${#PASSWORD} -lt 8 ]; then echo "Password too short!" fi
Pro Tip:
Use local variables in functions to avoid polluting the global scope:
greet() {
local name="$1" # "local" limits scope to the function
echo "Hello, $name!"
}
2. Advanced Conditional Logic
Bash conditionals (if, case) are more powerful than you think. Move beyond simple file checks and learn to handle complex logic.
[[ ... ]] vs. [ ... ]
The extended test command [[ ... ]] (Bash-specific) supports features like pattern matching and regex, unlike the POSIX-compliant [ ... ]:
FILE="data.csv"
if [[ $FILE == *.csv ]]; then # Pattern matching (no quotes needed)
echo "CSV file detected"
fi
if [[ $FILE =~ ^data_[0-9]{4}.csv$ ]]; then # Regex match
echo "Valid data file: $FILE"
fi
case Statements for Multiple Conditions
Simplify complex if-elif chains with case:
case "$1" in
start) systemctl start myservice ;;
stop) systemctl stop myservice ;;
restart) systemctl restart myservice ;;
*) echo "Usage: $0 {start|stop|restart}" ;;
esac
File/Directory Checks
Test file types, permissions, and timestamps with these operators (works in [ ... ] or [[ ... ]]):
if [[ -d "/tmp" ]]; then echo "/tmp is a directory"; fi
if [[ -f "/etc/passwd" ]]; then echo "File exists"; fi
if [[ -x "/usr/bin/git" ]]; then echo "Git is executable"; fi
if [[ "$file1" -nt "$file2" ]]; then echo "$file1 is newer"; fi # Newer than
3. Efficient Loops for Scalable Automation
Loops let you repeat tasks, but optimizing them can drastically improve script performance.
Loop Over Arrays
Arrays are underused in Bash but ideal for lists of items:
SERVERS=("web01" "web02" "db01")
for server in "${SERVERS[@]}"; do # "${array[@]}" preserves spaces in elements
ssh "$server" "uptime"
done
Process Substitution in Loops
Avoid temporary files by looping over command output directly:
# Loop over lines from a command (e.g., list of log files modified in the last 7 days)
while IFS= read -r logfile; do
gzip "$logfile" # Compress old logs
done < <(find /var/log -name "*.log" -mtime +7) # <(...) is process substitution
until Loops for Retries
Use until to repeat a command until it succeeds (e.g., waiting for a service to start):
until systemctl is-active --quiet myservice; do
echo "Waiting for myservice..."
sleep 5
done
echo "myservice is running!"
4. Functions: Reusability & Modularity
Functions turn repetitive code into reusable blocks, making scripts easier to maintain and debug.
Key Function Features:
-
Return Values: Use
returnfor exit codes (0 = success) or command substitution for output:# Return exit code is_root() { [[ $EUID -eq 0 ]] # Returns 0 (true) if root, 1 (false) otherwise } # Return output via command substitution get_timestamp() { date +"%Y%m%d_%H%M%S" # Call with: timestamp=$(get_timestamp) } -
Argument Handling: Access arguments with
$1,$2, etc., or$@for all arguments:backup_files() { local dest="$1" shift # Remove first argument (dest), so "$@" is now the list of files tar -czf "$dest/backup_$(get_timestamp).tar.gz" "$@" } # Usage: backup_files /tmp/backups /home/user/docs /var/log
Pro Tip:
Use function libraries to share code across scripts. Create a common_functions.sh file and source it:
# common_functions.sh
log() {
echo "[$(date +%H:%M:%S)] $*" # Log with timestamp
}
# In your script:
source ./common_functions.sh
log "Starting backup..."
5. Process Substitution: Beyond Pipes
Process substitution (<(command) or >(command)) treats the output of a command as a temporary file, avoiding clunky temporary files in scripts.
Use Cases:
-
Compare Two Command Outputs:
# Diff the output of two commands (no temp files!) diff <(ls /tmp) <(ls /var/tmp) -
Feed Input to Multiple Commands:
# Send log data to both a file and stdout tail -f /var/log/syslog | tee >(grep "ERROR" > error.log) -
Pass Multiple Inputs to a Command:
# Merge two sorted files without temporary storage sort -m <(sort file1.txt) <(sort file2.txt)
6. Traps: Cleanup & Error Handling
Traps let you run commands automatically on signals (e.g., script exit, Ctrl+C), ensuring cleanup (e.g., deleting temp files) even if the script fails.
Common Use Cases:
-
Cleanup Temp Files on Exit:
TEMP_DIR=$(mktemp -d) trap 'rm -rf "$TEMP_DIR"' EXIT # Delete TEMP_DIR when script exits (success or failure) # ... rest of script ... -
Handle Ctrl+C Interrupts:
trap 'echo "Aborted!"; exit 1' INT # Run on Ctrl+C (SIGINT) -
Debugging with Traps:
trap 'echo "Error at line $LINENO"' ERR # Log line number on error
7. Advanced Redirection: Taming Output
Most users know >, >>, and 2>&1, but advanced redirection helps manage logs and silence noise.
Key Techniques:
-
Redirect All Output to a Log File:
exec > /var/log/script.log 2>&1 # From this line onward, stdout/stderr go to log -
Silence Command Output:
command >/dev/null 2>&1 # Suppress stdout and stderr -
Log to File and Stdout:
# Use tee to write to both log and stdout (stderr too with process substitution) command 2>&1 | tee -a script.log
8. Scheduling with Cron & Systemd Timers
Automation isn’t complete without scheduling. Use cron for simple jobs or systemd timers for more control.
Cron Basics
Cron runs jobs at fixed intervals. Edit crontab with crontab -e:
# Syntax: minute hour day month weekday command
0 3 * * * /home/user/scripts/backup.sh # Run daily at 3 AM
Pitfall: Cron uses a minimal PATH. Always use absolute paths or define PATH in the crontab:
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
0 3 * * * /home/user/scripts/backup.sh
Systemd Timers (Modern Alternative)
Systemd timers offer more flexibility (e.g., calendar events, dependencies). Create a .timer and .service file:
-
Service File (
backup.servicein/etc/systemd/system/):[Unit] Description=Run backup script [Service] Type=oneshot ExecStart=/home/user/scripts/backup.sh User=user -
Timer File (
backup.timer):[Unit] Description=Daily backup timer [Timer] OnCalendar=*-*-* 03:00:00 # Daily at 3 AM Persistent=true # Run missed jobs on startup [Install] WantedBy=timers.target
Enable and start the timer:
sudo systemctl enable --now backup.timer
9. Debugging Like a Pro
Debugging Bash scripts can be frustrating, but these tools and techniques will save you hours.
set Options
Enable debugging flags at the start of your script:
set -x # Print commands and arguments as they execute (verbose mode)
set -e # Exit immediately if any command fails
set -u # Treat unset variables as errors (avoids "undefined variable" bugs)
set -o pipefail # Exit if any command in a pipeline fails (not just the last one)
Use set +x to disable debugging temporarily in a script.
shellcheck: Static Analysis
The shellcheck tool (install with sudo apt install shellcheck) scans scripts for errors, bad practices, and portability issues:
shellcheck my_script.sh
Example output:
In my_script.sh line 5:
echo "Backup dir: $BACKUP_DIR"
^------------^ SC2154: BACKUP_DIR is referenced but not assigned.
Traps for Debugging
Log the call stack on error to identify where things went wrong:
trap 'echo "Error in $0 at line $LINENO"; exit 1' ERR
10. Real-World Example: Automated Backup Script
Let’s tie it all together with a robust backup script using the tips above:
#!/bin/bash
set -euo pipefail # Exit on error, unset variables, or pipeline failure
# Load common functions
source "$(dirname "${BASH_SOURCE[0]}")/common_functions.sh"
# Configuration
DEST_DIR="${1:-/var/backups}"
SRC_DIRS=(/home/user/docs /var/log)
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT # Cleanup temp dir on exit
# Validate inputs
if [[ ! -d "$DEST_DIR" ]]; then
log "Error: Destination $DEST_DIR does not exist."
exit 1
fi
# Backup logic
log "Starting backup to $DEST_DIR..."
backup_files "$DEST_DIR" "${SRC_DIRS[@]}" # Use backup_files function from common_functions
log "Backup completed successfully!"
11. Conclusion
Bash is more than just a command prompt—it’s a powerful automation tool. By mastering variables, parameter expansion, functions, traps, and scheduling, you’ll write scripts that are efficient, maintainable, and resilient. The key is practice: experiment with these tips, refactor old scripts, and use tools like shellcheck to refine your craft.
Automation is about working smarter, not harder. With these pro tips, you’ll spend less time on repetitive tasks and more time innovating.