Table of Contents
- What is Shell Scripting?
- Core Concepts of Shell Scripting
- Strategies for Effective Shell Scripting
- Practical Examples
- Best Practices
- Conclusion
- References
What is Shell Scripting?
A shell script is a text file containing a sequence of commands executed by a shell interpreter (e.g., Bash, Zsh, or Dash). The shell acts as an intermediary between the user and the operating system kernel, translating human-readable commands into system calls.
Shell scripts are lightweight, platform-agnostic (across Unix-like systems), and require no compilation—making them ideal for:
- Automating file management (e.g., renaming, archiving).
- System monitoring (e.g., CPU/memory usage alerts).
- Deployment pipelines (e.g., code testing, server provisioning).
- Repetitive tasks (e.g., log rotation, data backups).
Core Concepts of Shell Scripting
To harness the power of shell scripting, you must first master its foundational building blocks.
Variables and Data Types
Variables store data for reuse. Unlike compiled languages, shell scripts are dynamically typed (no explicit type declarations).
Syntax:
# Declare a variable (no spaces around =)
name="Alice"
age=30
# Access a variable with $
echo "Name: $name, Age: $age"
# Read user input into a variable
read -p "Enter your favorite color: " color
echo "Your favorite color is $color"
Key Types:
- Strings: Enclosed in quotes (e.g.,
greeting="Hello, World!"). Use double quotes (" ") for variable expansion; single quotes (' ') for literal strings. - Integers: Numeric values (e.g.,
count=10). Arithmetic operations require$(( ... )):total=$((age + 5)) # Adds 5 to $age - Arrays: Ordered collections (Bash-specific):
fruits=("apple" "banana" "cherry") echo "First fruit: ${fruits[0]}" # Access by index echo "All fruits: ${fruits[@]}" # Print all elements
Control Structures
Control structures dictate the flow of execution (conditionals, case statements).
if-else Statements
Check conditions (e.g., file existence, numeric comparisons).
file="data.txt"
if [ -f "$file" ]; then # -f checks if $file is a regular file
echo "$file exists."
elif [ -d "$file" ]; then # -d checks if $file is a directory
echo "$file is a directory."
else
echo "$file does not exist."
fi
Common condition flags:
-f <file>: File exists and is regular.-d <dir>: Directory exists.-z <str>: String is empty.-n <str>: String is non-empty.$a -gt $b:$a>$b(numeric comparison).
case Statements
Simplify multi-condition checks (alternative to nested if-else).
read -p "Enter a day (1-7): " day
case $day in
1) echo "Monday" ;;
2) echo "Tuesday" ;;
3|4|5) echo "Weekday" ;; # Match 3, 4, or 5
6|7) echo "Weekend" ;;
*) echo "Invalid day" ;; # Default case
esac
Loops
Loops iterate over data (e.g., files, lists, ranges).
for Loops
Iterate over a list of items.
# Loop over a static list
for fruit in apple banana cherry; do
echo "I like $fruit"
done
# Loop over files in a directory
for file in *.txt; do
echo "Processing $file"
done
# Loop over a range (Bash)
for i in {1..5}; do
echo "Count: $i"
done
while Loops
Run commands as long as a condition is true.
count=1
while [ $count -le 5 ]; do # Run until count > 5
echo "Loop iteration: $count"
count=$((count + 1)) # Increment count
done
Functions
Functions encapsulate reusable code blocks, improving readability and maintainability.
Syntax:
# Define a function
greet() {
local name=$1 # $1 = first argument, local to the function
echo "Hello, $name!"
}
# Call the function
greet "Bob" # Output: Hello, Bob!
Key Features:
- Parameters: Accessed via
$1,$2, … (positional arguments). - Return Values: Use
returnfor exit codes (0 = success, non-zero = error). For data, echo output and capture with$(...):add() { echo $(( $1 + $2 )) # Echo result } sum=$(add 5 3) # Capture output: sum=8
Input/Output Redirection
Shell scripts interact with input/output (I/O) streams to read from files, write to files, or chain commands.
Streams:
stdin(0): Standard input (default: keyboard).stdout(1): Standard output (default: terminal).stderr(2): Standard error (default: terminal).
Redirection Operators:
>: Overwrite a file withstdout(e.g.,echo "Hi" > output.txt).>>: Appendstdoutto a file (e.g.,echo "Line 2" >> output.txt).2>: Redirectstderrto a file (e.g.,command 2> errors.log).&>: Redirect bothstdoutandstderr(e.g.,command &> combined.log).|: Pipestdoutof one command tostdinof another (e.g.,ls -l | grep ".txt").
Strategies for Effective Shell Scripting
Writing a script that “works” is easy; writing one that is reliable, maintainable, and scalable requires strategy.
Modular Design
Break scripts into small, focused functions. This improves readability, reusability, and testability.
Example: Modular Backup Script
#!/bin/bash
# Function: Log messages with timestamps
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
}
# Function: Validate backup source exists
validate_source() {
local source=$1
if [ ! -d "$source" ]; then
log "Error: Source directory $source does not exist."
exit 1 # Exit with error code 1
fi
}
# Function: Perform backup with rsync
perform_backup() {
local source=$1
local dest=$2
log "Starting backup from $source to $dest..."
rsync -av --delete "$source"/ "$dest"/ # -av: archive/verbose, --delete: mirror source
if [ $? -eq 0 ]; then # $? = exit code of last command (0 = success)
log "Backup completed successfully."
else
log "Backup failed."
exit 1
fi
}
# Main execution
SOURCE="/home/user/documents"
DEST="/mnt/backup/documents"
validate_source "$SOURCE"
perform_backup "$SOURCE" "$DEST"
Error Handling
Unhandled errors can crash scripts or cause data loss. Proactively manage errors with:
set -euo pipefail
Enable strict error checking at the start of your script:
-e: Exit immediately if any command fails.-u: Treat undefined variables as errors.-o pipefail: Exit if any command in a pipeline fails (not just the last one).
#!/bin/bash
set -euo pipefail # Strict mode
trap for Cleanup
Use trap to run commands on script exit (e.g., delete temporary files).
#!/bin/bash
set -euo pipefail
# Create a temporary directory
TMP_DIR=$(mktemp -d)
log "Created temp dir: $TMP_DIR"
# Cleanup: Delete temp dir on exit (success or failure)
trap 'rm -rf "$TMP_DIR"; log "Cleaned up temp dir."' EXIT
# ... rest of script ...
Idempotency
Scripts should be idempotent—running them multiple times produces the same result as running them once. Avoid side effects like duplicate files or failed commands on re-runs.
Example: Idempotent Directory Creation
# Instead of: mkdir "logs" (fails if dir exists)
mkdir -p "logs" # -p: Create parent dirs; no error if dir exists
Logging
Logs are critical for debugging and auditing. Always log key actions, errors, and timestamps.
Example: Logging to File and Terminal
#!/bin/bash
LOG_FILE="script.log"
# Log to both terminal and file (tee -a = append)
echo "Starting script..." | tee -a "$LOG_FILE"
# ... rest of script ...
Practical Examples
Let’s apply these concepts and strategies to real-world scenarios.
Example 1: File Organizer
Goal: Automatically sort files in a directory by extension (e.g., .txt → Documents, .jpg → Images).
#!/bin/bash
set -euo pipefail
# Define source directory (default: current dir)
SOURCE_DIR="${1:-.}" # Use first argument or . (current dir)
# Define extension-to-folder mappings
declare -A FOLDERS=(
["txt"]="Documents"
["pdf"]="Documents"
["jpg"]="Images"
["png"]="Images"
["sh"]="Scripts"
["zip"]="Archives"
["tar.gz"]="Archives"
)
# Create target folders if missing
for folder in "${FOLDERS[@]}"; do
mkdir -p "$SOURCE_DIR/$folder"
done
# Organize files
find "$SOURCE_DIR" -maxdepth 1 -type f | while read -r file; do
# Skip the script itself
if [ "$(basename "$file")" = "$(basename "$0")" ]; then
continue
fi
# Extract file extension (handle multi-part extensions like .tar.gz)
ext=$(basename "$file" | sed -E 's/^.*\.([^.]+)$/\1/' | tr '[:upper:]' '[:lower:]')
# Skip files with no extension
if [ "$ext" = "$(basename "$file")" ]; then
echo "Skipping file with no extension: $file"
continue
fi
# Move file to target folder
target_folder="${FOLDERS[$ext]:-Other}" # Default to "Other" if extension not mapped
mv -v "$file" "$SOURCE_DIR/$target_folder/"
done
echo "File organization complete!"
Usage:
Save as organize_files.sh, make executable with chmod +x organize_files.sh, and run with ./organize_files.sh [source_dir] (e.g., ./organize_files.sh ~/Downloads).
Example 2: System Monitoring Dashboard
Goal: Monitor CPU, memory, and disk usage; alert if thresholds are exceeded.
#!/bin/bash
set -euo pipefail
# Thresholds (adjust as needed)
CPU_THRESHOLD=80 # %
MEM_THRESHOLD=80 # %
DISK_THRESHOLD=85 # %
# Get metrics
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}') # User + System CPU
MEM_USAGE=$(free -m | awk '/Mem:/ {print $3/$2 * 100}') # Used memory %
DISK_USAGE=$(df -h / | awk '/\// {print $5}' | sed 's/%//') # Root disk usage %
# Function: Check threshold and alert
check_alert() {
local metric=$1
local value=$2
local threshold=$3
if (( $(echo "$value > $threshold" | bc -l) )); then # bc for floating-point comparison
echo "ALERT: $metric usage is $(printf "%.1f" "$value")% (Threshold: $threshold%)"
else
echo "OK: $metric usage is $(printf "%.1f" "$value")%"
fi
}
# Run checks
echo "=== System Monitoring Report ==="
check_alert "CPU" "$CPU_USAGE" "$CPU_THRESHOLD"
check_alert "Memory" "$MEM_USAGE" "$MEM_THRESHOLD"
check_alert "Disk (/)" "$DISK_USAGE" "$DISK_THRESHOLD"
Usage:
Save as system_monitor.sh, make executable, and run. For continuous monitoring, add to cron (e.g., run every 5 minutes).
Example 3: Automated Backup with Rsync
Goal: Back up a directory to a remote server or external drive, with logging and error handling.
#!/bin/bash
set -euo pipefail
# Configuration
SOURCE="/home/user/photos"
DEST="[email protected]:/mnt/backup/photos"
LOG_FILE="/var/log/photo_backup.log"
EXCLUDE_LIST=("*.tmp" "cache/") # Files/dirs to exclude
# Log function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Validate source
if [ ! -d "$SOURCE" ]; then
log "ERROR: Source directory $SOURCE does not exist."
exit 1
fi
# Build rsync exclude arguments
EXCLUDE_ARGS=()
for item in "${EXCLUDE_LIST[@]}"; do
EXCLUDE_ARGS+=(--exclude "$item")
done
# Run rsync
log "Starting backup from $SOURCE to $DEST..."
rsync -avz "${EXCLUDE_ARGS[@]}" "$SOURCE"/ "$DEST"/ >> "$LOG_FILE" 2>&1
# Check rsync exit code
if [ $? -eq 0 ]; then
log "Backup completed successfully."
else
log "ERROR: Backup failed. Check $LOG_FILE for details."
exit 1
fi
Usage:
Save as photo_backup.sh, configure SOURCE, DEST, and EXCLUDE_LIST, then run. For passwordless remote access, set up SSH keys.
Best Practices
To elevate your scripts from “working” to “production-ready,” follow these best practices:
-
Start with a Shebang: Always specify the shell interpreter:
#!/bin/bash # Use Bash-specific features #!/bin/sh # Use POSIX-compliant shell (portable) -
Enable Strict Mode: Use
set -euo pipefailto catch errors early:-e: Exit on command failure.-u: Treat undefined variables as errors.-o pipefail: Exit if any pipeline command fails.
-
Quote Variables: Always quote variables to avoid word splitting (e.g.,
rm "$file"instead ofrm $file). -
Use Functions for Repeating Code: Reduce redundancy and improve readability.
-
Document with Comments: Explain why, not just what. Include usage instructions at the top: