Table of Contents
- Introduction
- The Basics of Bash Scripting
2.1 Variables and Data Types
2.2 Control Structures: Loops and Conditionals
2.3 Functions: Reusable Code Blocks - Key Concepts for Automation
3.1 Command Substitution: Capturing Output
3.2 Input/Output Redirection and Pipes
3.3 Process Management
3.4 Handling Signals - Advanced Bash Techniques
4.1 Arrays and Associative Arrays
4.2 Regular Expressions and Pattern Matching
4.3 Here Documents and Here Strings
4.4 Parameter Expansion
4.5 Error Handling and Debugging - Real-World Automation Examples
5.1 Automated Backup Script
5.2 Log Rotation and Management
5.3 System Health Monitoring
5.4 User Account Provisioning - Best Practices for Bash Scripting
- Enhancing Bash with Complementary Tools
- Conclusion
- References
The Basics of Bash Scripting
Before diving into automation, let’s cover the foundational building blocks of Bash scripting.
Variables and Data Types
Variables store data for reuse. Bash is dynamically typed, so you don’t declare types explicitly.
Syntax:
# Assign a value (no spaces around =)
NAME="Alice"
AGE=30
# Access a variable with $
echo "Name: $NAME, Age: $AGE"
# Environment variables (predefined or set with export)
echo "Home directory: $HOME"
export PATH="$PATH:/usr/local/bin" # Add custom directory to PATH
Key Notes:
- Use quotes (
" ") to preserve spaces in strings:GREETING="Hello World" - Avoid spaces around
=(e.g.,NAME = "Alice"will throw an error). - Use
readonlyto prevent accidental modification:readonly PI=3.14
Control Structures: Loops and Conditionals
Control structures let you automate decision-making and repetition.
Conditionals (if, else, elif, case)
Check conditions (file existence, exit codes, comparisons) to execute code selectively.
Example: File Existence Check
FILE="/tmp/data.txt"
if [ -f "$FILE" ]; then # -f checks if file exists and is a regular file
echo "$FILE exists."
elif [ -d "$FILE" ]; then # -d checks if directory
echo "$FILE is a directory."
else
echo "$FILE does not exist."
fi
Example: Case Statement (Multiple Conditions)
DAY=$(date +%A) # Get current day (e.g., "Monday")
case $DAY in
Monday|Wednesday|Friday)
echo "Gym day!"
;;
Tuesday|Thursday)
echo "Work from home."
;;
Saturday|Sunday)
echo "Weekend!"
;;
*) # Default case
echo "Invalid day."
;;
esac
Loops (for, while, until)
Repeat commands for lists, files, or until a condition is met.
Example: For Loop (Iterate Over Files)
# List all .txt files in the current directory
for FILE in *.txt; do
echo "Processing $FILE..."
# Add logic here (e.g., compress, move)
done
Example: While Loop (Read Lines from a File)
# Read user list and print names
while IFS= read -r USER; do # IFS= preserves leading/trailing whitespace
echo "User: $USER"
done < "users.txt" # Input file
Functions: Reusable Code Blocks
Functions group commands into reusable units, improving readability and reducing redundancy.
Syntax:
# Define a function
greet() {
local NAME=$1 # $1 = first argument
echo "Hello, $NAME!"
}
# Call the function
greet "Bob" # Output: Hello, Bob!
Example: Logging Function
log() {
local TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$TIMESTAMP] $1" # $1 = log message
}
log "Script started" # Output: [2024-05-20 14:30:00] Script started
Key Concepts for Automation
These concepts are critical for building powerful automation scripts.
Command Substitution: Capturing Output
Store the output of a command in a variable using $(command) (preferred) or backticks `command`.
Example:
# Get current date as YYYY-MM-DD
TODAY=$(date +"%Y-%m-%d")
echo "Today is $TODAY" # Output: Today is 2024-05-20
# Count lines in a file
LINE_COUNT=$(wc -l < "data.txt")
echo "Lines in data.txt: $LINE_COUNT"
Input/Output Redirection and Pipes
Redirect command input/output or chain commands with pipes (|).
| Operator | Purpose | Example |
|---|---|---|
> | Overwrite file with output | ls > file_list.txt |
>> | Append output to file | echo "New line" >> notes.txt |
< | Read input from file | sort < unsorted.txt |
2> | Redirect errors to file | command_that_fails 2> error.log |
&> | Redirect both output and errors | script.sh &> combined.log |
| ` | ` | Pipe output of one command to another |
Process Management
Control background/foreground processes and handle job scheduling.
Example: Run a Command in the Background
# Start a long-running process (e.g., backup) in the background
rsync -av /source /destination &
# List background jobs
jobs
# Bring job 1 to foreground
fg %1
Kill a Process:
# Kill by PID (find PID with ps aux | grep "process_name")
kill 1234
# Force kill (if unresponsive)
kill -9 1234
Handling Signals
Use trap to catch signals (e.g., Ctrl+C = SIGINT) and clean up resources (temp files, connections).
Example: Clean Up Temp Files on Exit
# Define cleanup function
cleanup() {
echo "Cleaning up temp files..."
rm -f /tmp/temp_*.txt
exit 0
}
# Trap SIGINT (Ctrl+C) and SIGTERM (kill command)
trap cleanup SIGINT SIGTERM
# Simulate long-running task
echo "Working..."
sleep 30 # Press Ctrl+C during sleep to trigger cleanup
Advanced Bash Techniques
These techniques unlock Bash’s full potential for complex automation.
Arrays and Associative Arrays
Arrays store lists of values; associative arrays (Bash 4+) store key-value pairs.
Example: Regular Array
FRUITS=("Apple" "Banana" "Cherry")
# Access element (indexes start at 0)
echo "First fruit: ${FRUITS[0]}" # Apple
# Loop through array
for FRUIT in "${FRUITS[@]}"; do
echo "Fruit: $FRUIT"
done
Example: Associative Array (Key-Value Pairs)
declare -A USER_AGES # Declare associative array
USER_AGES["Alice"]=30
USER_AGES["Bob"]=25
echo "Alice's age: ${USER_AGES["Alice"]}" # 30
# Loop through keys
for NAME in "${!USER_AGES[@]}"; do
echo "$NAME is ${USER_AGES[$NAME]} years old"
done
Regular Expressions and Pattern Matching
Use [[ string =~ regex ]] to test strings against regular expressions.
Example: Validate Email Format
EMAIL="[email protected]"
EMAIL_REGEX="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if [[ $EMAIL =~ $EMAIL_REGEX ]]; then
echo "Valid email: $EMAIL"
else
echo "Invalid email: $EMAIL"
fi
Here Documents and Here Strings
Pass multi-line input to commands without external files.
Here Document (<<EOF):
# Create a config file with multi-line content
cat > config.ini <<EOF
[Server]
Host=localhost
Port=8080
Timeout=30
EOF
Here String (<<<):
# Pass a string as input to a command
grep "error" <<< "log line 1: success; log line 2: error" # Output: log line 2: error
Parameter Expansion
Manipulate variables dynamically (substring extraction, replacement, default values).
Common Expansions:
| Syntax | Purpose | Example |
|---|---|---|
${var:-default} | Use default if var is unset | ${NAME:-Guest} (uses “Guest” if NAME is unset) |
${var#pattern} | Remove shortest prefix matching pattern | ${FILE#*.} (get file extension: “txt” for “file.txt”) |
${var##pattern} | Remove longest prefix matching pattern | ${PATH##*/} (get last directory in PATH) |
${var%pattern} | Remove shortest suffix matching pattern | ${FILE%.txt} (remove “.txt” from “file.txt”) |
${var//search/replace} | Replace all search with replace | ${TEXT//hello/hi} (replace “hello” with “hi” in TEXT) |
Error Handling and Debugging
Write robust scripts with error checking and debugging tools.
Key Options:
set -e: Exit immediately if any command fails.set -u: Treat undefined variables as errors.set -o pipefail: Exit if any command in a pipe fails (not just the last one).set -x: Print commands and arguments as they execute (debug mode).
Example: Strict Error Checking
#!/bin/bash
set -euo pipefail # Exit on error, undefined var, or pipe failure
# This will fail if "input.txt" doesn't exist (thanks to set -e)
cat input.txt
Debugging with set -x:
#!/bin/bash
set -x # Enable debugging
NAME="Alice"
echo "Hello $NAME" # Output: + echo 'Hello Alice' followed by Hello Alice
Real-World Automation Examples
Let’s apply these concepts to practical automation tasks.
Automated Backup Script
Goal: Backup a directory to a remote server with logging and error alerts.
#!/bin/bash
set -euo pipefail
# Configuration
SOURCE_DIR="/home/user/documents"
DESTINATION="[email protected]:/backups"
BACKUP_NAME="docs_backup_$(date +%Y%m%d).tar.gz"
LOG_FILE="/var/log/backup.log"
EMAIL="[email protected]"
# Log function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# Start backup
log "Starting backup of $SOURCE_DIR..."
# Create tar archive and send to remote server via rsync
tar -czf - "$SOURCE_DIR" | rsync -avz -e ssh --progress - "$DESTINATION/$BACKUP_NAME"
if [ $? -eq 0 ]; then # Check if rsync succeeded
log "Backup completed successfully: $BACKUP_NAME"
echo "Backup succeeded: $BACKUP_NAME" | mail -s "Backup Success" "$EMAIL"
else
log "Backup FAILED"
echo "Backup failed for $SOURCE_DIR" | mail -s "Backup FAILED" "$EMAIL"
exit 1
fi
Log Rotation and Management
Goal: Compress old logs, keep only 7 days of logs, and notify on failure.
#!/bin/bash
set -euo pipefail
LOG_DIR="/var/log/myapp"
DAYS_TO_KEEP=7
LOG_FILE="$LOG_DIR/rotation.log"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
log "Starting log rotation..."
# Compress logs older than 1 day (not today's log)
find "$LOG_DIR" -name "app.log.*" -mtime +1 -exec gzip {} \;
# Delete logs older than 7 days
find "$LOG_DIR" -name "app.log.*.gz" -mtime +$DAYS_TO_KEEP -delete
log "Log rotation completed. Kept last $DAYS_TO_KEEP days of logs."
System Health Monitoring
Goal: Check CPU, memory, and disk usage; alert if thresholds are exceeded.
#!/bin/bash
set -euo pipefail
# Thresholds (percent)
CPU_THRESHOLD=80
MEM_THRESHOLD=80
DISK_THRESHOLD=90
ALERT_EMAIL="[email protected]"
# Check CPU usage (using top for current load)
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d. -f1)
# Check memory usage (using free)
MEM_USAGE=$(free | grep Mem | awk '{print $3/$2 * 100}' | cut -d. -f1)
# Check disk usage (root partition)
DISK_USAGE=$(df -h / | grep / | awk '{print $5}' | sed 's/%//g')
ALERT_MSG=""
if [ "$CPU_USAGE" -gt "$CPU_THRESHOLD" ]; then
ALERT_MSG+="High CPU usage: $CPU_USAGE% (Threshold: $CPU_THRESHOLD%)\n"
fi
if [ "$MEM_USAGE" -gt "$MEM_THRESHOLD" ]; then
ALERT_MSG+="High Memory usage: $MEM_USAGE% (Threshold: $MEM_THRESHOLD%)\n"
fi
if [ "$DISK_USAGE" -gt "$DISK_THRESHOLD" ]; then
ALERT_MSG+="High Disk usage: $DISK_USAGE% (Threshold: $DISK_THRESHOLD%)\n"
fi
if [ -n "$ALERT_MSG" ]; then
echo -e "System Alert:\n$ALERT_MSG" | mail -s "High Resource Usage" "$ALERT_EMAIL"
fi
User Account Provisioning
Goal: Automatically create user accounts, set up home directories, and assign permissions.
#!/bin/bash
set -euo pipefail
# User list (format: username:group:password_hash)
USER_LIST=(
"alice:developers:\$6\$salt\$hash" # Use openssl passwd -6 to generate hash
"bob:operations:\$6\$salt\$hash"
)
for USER in "${USER_LIST[@]}"; do
# Split user data (IFS=: to split on colon)
IFS=':' read -r USERNAME GROUP PASSWORD_HASH <<< "$USER"
# Check if user exists
if id "$USERNAME" &>/dev/null; then
echo "User $USERNAME already exists. Skipping."
continue
fi
# Create group if it doesn't exist
if ! getent group "$GROUP" &>/dev/null; then
groupadd "$GROUP"
echo "Created group: $GROUP"
fi
# Create user with home directory and group
useradd -m -g "$GROUP" -s /bin/bash "$USERNAME"
echo "Created user: $USERNAME"
# Set password (using pre-hashed password for security)
echo "$USERNAME:$PASSWORD_HASH" | chpasswd -e
echo "Set password for $USERNAME"
# Set permissions on home directory
chmod 700 "/home/$USERNAME"
echo "Configured home directory for $USERNAME"
done
Best Practices for Bash Scripting
- Use a Shebang: Start scripts with
#!/bin/bash(not#!/bin/sh, which may be a minimal shell). - Enable Strict Mode:
set -euo pipefailto catch errors early. - Quote Variables: Always quote variables (
"$VAR") to prevent word splitting (e.g.,FILE="my file.txt"; cat "$FILE"works, butcat $FILEfails). - Modularize with Functions: Break logic into reusable functions for readability.
- Validate Inputs: Check if files exist, arguments are provided, etc.
- Avoid Hardcoded Secrets: Use environment variables or secure vaults instead of plaintext passwords.
- Test with
shellcheck: Runshellcheck script.shto catch syntax and logic errors. - Document with Comments: Explain why (not just what) the code does.
Enhancing Bash with Complementary Tools
Bash works best with other tools to extend functionality:
jq: Parse JSON (e.g.,curl API_URL | jq '.data[0].name').awk/sed: Advanced text processing (e.g.,awk -F ',' '{print $2}' data.csvto extract the second column).cron: Schedule scripts (e.g., run a backup daily at 2 AM:0 2 * * * /path/to/backup.sh).ansible: For large-scale infrastructure automation (use Bash for ad-hoc tasks, Ansible for orchestration).
Conclusion
Bash is a timeless tool for Linux automation, offering flexibility, speed, and direct access to the OS. By mastering variables, loops, conditionals, and advanced techniques like arrays and error handling, you can automate repetitive tasks, manage systems, and streamline workflows.
Combine Bash with tools like jq, cron, and ansible to tackle even the most complex automation challenges. Start small—write a backup script or log cleaner—and gradually build up to more ambitious projects.
The Linux ecosystem thrives on open-source collaboration, so don’t hesitate to learn from community scripts and contribute your own!