Advanced Linux Shell Scripting
Advanced shell scripting takes you beyond basic commands and into the realm of automation, system administration, and powerful data processing. This guide will help you write sophisticated scripts that can handle complex tasks efficiently.
If you’re new to shell scripting, you might want to start with our shell scripting basics guide first.
Functions and Modular Scripting
Functions make your scripts reusable, organized, and easier to maintain.
Creating and Using Functions
#!/bin/bash
# Function definition
log_message() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local level=$1
local message=$2
echo "[$timestamp] [$level] $message" | tee -a /var/log/my_script.log
}
# Function with return value
check_file_exists() {
local file_path=$1
if [[ -f "$file_path" ]]; then
return 0 # Success
else
return 1 # Failure
fi
}
# Function with parameters
backup_file() {
local source_file=$1
local backup_dir=${2:-"/tmp/backups"} # Default backup directory
# Create backup directory if it doesn't exist
mkdir -p "$backup_dir"
# Create backup with timestamp
local timestamp=$(date '+%Y%m%d_%H%M%S')
local backup_file="${backup_dir}/$(basename "$source_file")_$timestamp"
if cp "$source_file" "$backup_file"; then
log_message "INFO" "Backup created: $backup_file"
return 0
else
log_message "ERROR" "Failed to backup $source_file"
return 1
fi
}
# Usage
log_message "INFO" "Script started"
if check_file_exists "/etc/passwd"; then
log_message "INFO" "System files exist"
else
log_message "ERROR" "System files missing"
fi
backup_file "/etc/hosts" "/home/user/backups"Advanced Function Techniques
#!/bin/bash
# Function with variable arguments
process_files() {
local count=0
# Iterate over all arguments
for file in "$@"; do
if [[ -f "$file" ]]; then
echo "Processing: $file"
# Perform operations on file
((count++))
else
echo "Warning: $file not found" >&2
fi
done
echo "Processed $count files"
}
# Function with local and global variables
counter=0
increment_counter() {
local increment=${1:-1} # Default increment of 1
counter=$((counter + increment))
echo "Counter is now: $counter"
}
# Function that returns complex data
get_system_info() {
local info=""
info+="Hostname: $(hostname)\n"
info+="Kernel: $(uname -r)\n"
info+="Uptime: $(uptime -p)\n"
info+="Memory Usage: $(free -h | grep '^Mem:' | awk '{print $3 "/" $2}')\n"
echo -e "$info"
}
# Function libraries
source_library() {
local lib_file=$1
if [[ -f "$lib_file" ]]; then
source "$lib_file"
else
echo "Library file $lib_file not found" >&2
return 1
fi
}Advanced Text Processing
Master text processing tools like awk, sed, and grep for powerful data manipulation.
Advanced Awk Usage
#!/bin/bash
# Process log files with awk
analyze_web_log() {
local log_file=$1
echo "=== Web Log Analysis ==="
# Count requests by hour
awk '{print $4}' "$log_file" | \
sed 's/\[//' | cut -d: -f1 | \
sort | uniq -c | sort -nr
# Top 10 IP addresses
echo -e "\n=== Top 10 IP Addresses ==="
awk '{print $1}' "$log_file" | \
sort | uniq -c | sort -nr | head -10
# HTTP status code distribution
echo -e "\n=== HTTP Status Codes ==="
awk '{print $9}' "$log_file" | \
sort | uniq -c | sort -nr
# Calculate average response size
echo -e "\n=== Average Response Size ==="
awk '{sum+=$10; count++} END {print "Average:", sum/count " bytes"}' "$log_file"
}
# Complex data transformation
transform_csv() {
local input_file=$1
local output_file=$2
awk -F',' 'BEGIN {OFS=","}
NR == 1 {
# Print header
print "ID", "Name", "Score", "Grade"
next
}
NR > 1 {
# Calculate grade based on score
grade = ($3 >= 90) ? "A" : ($3 >= 80) ? "B" : ($3 >= 70) ? "C" : "F"
# Clean up name (remove extra spaces)
gsub(/^[ \t]+|[ \t]+$/, "", $2)
print $1, $2, $3, grade
}' "$input_file" > "$output_file"
}
# Multi-file processing
process_multiple_files() {
local pattern=$1
for file in $pattern; do
if [[ -f "$file" ]]; then
echo "Processing $file..."
# Extract specific information
awk -F':' '$3 >= 1000 && $3 <= 65535 {print $1 ":" $6}' /etc/passwd
# Calculate statistics
awk '{bytes+=$5; files++} END {
print "Total files:", files
print "Total bytes:", bytes
print "Average:", bytes/files
}' "$file"
fi
done
}Advanced Sed Techniques
#!/bin/bash
# Batch text replacement
update_config_files() {
local directory=$1
local old_value=$2
local new_value=$3
find "$directory" -name "*.conf" -type f | while read file; do
echo "Updating $file..."
# Backup original file
cp "$file" "$file.backup"
# Perform multiple sed operations
sed -i \
-e "s/old_value/$new_value/g" \
-e 's/^[[:space:]]*//' \
-e '/^#/d' \
-e '/^$/d' \
"$file"
done
}
# Log file processing
clean_log_file() {
local log_file=$1
local temp_file=$(mktemp)
# Process log file with multiple sed commands
sed -n \
-e '1p' \
-e '/ERROR/{
s/^/\n/
p
}' \
-e '/WARNING/w warnings.log' \
-e '/CRITICAL/w critical.log' \
"$log_file" > "$temp_file"
mv "$temp_file" "$log_file"
}
# Format text files
format_text_file() {
local input_file=$1
local output_file=$2
sed '
# Remove leading whitespace
s/^[[:space:]]*//
# Remove trailing whitespace
s/[[:space:]]*$//
# Replace multiple spaces with single space
s/[[:space:]]\+/ /g
# Add line numbers
=
# Combine line number with content
N
s/\n/ /
' "$input_file" > "$output_file"
}Process Management and Monitoring
Advanced scripts often need to manage processes and monitor system resources.
Process Management Functions
#!/bin/bash
# Check if process is running
is_process_running() {
local process_name=$1
pgrep -f "$process_name" > /dev/null
return $?
}
# Start process with PID tracking
start_daemon() {
local command=$1
local pid_file=$2
if is_process_running "$command"; then
echo "Process is already running"
return 1
fi
# Start process in background
nohup $command > /dev/null 2>&1 &
local pid=$!
# Save PID to file
echo $pid > "$pid_file"
echo "Started process with PID: $pid"
return 0
}
# Stop process gracefully
stop_daemon() {
local pid_file=$1
if [[ ! -f "$pid_file" ]]; then
echo "PID file not found"
return 1
fi
local pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
echo "Stopping process $pid..."
kill -TERM "$pid"
# Wait for process to stop
local timeout=30
while kill -0 "$pid" 2>/dev/null && [[ $timeout -gt 0 ]]; do
sleep 1
((timeout--))
done
# Force kill if still running
if kill -0 "$pid" 2>/dev/null; then
echo "Force killing process $pid..."
kill -KILL "$pid"
fi
rm "$pid_file"
echo "Process stopped"
else
echo "Process not running"
rm -f "$pid_file"
fi
}
# Monitor system resources
monitor_resources() {
local threshold_cpu=${1:-80}
local threshold_mem=${2:-80}
local duration=${3:-60}
local interval=${4:-5}
echo "Monitoring system resources for $duration seconds..."
local end_time=$(($(date +%s) + duration))
while [[ $(date +%s) -lt $end_time ]]; do
local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | sed 's/%us,//')
local mem_usage=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100.0}')
echo "$(date '+%H:%M:%S') - CPU: ${cpu_usage}% MEM: ${mem_usage}%"
# Alert if thresholds exceeded
if (( $(echo "$cpu_usage > $threshold_cpu" | bc -l) )); then
echo "WARNING: High CPU usage: ${cpu_usage}%"
fi
if [[ $mem_usage -gt $threshold_mem ]]; then
echo "WARNING: High memory usage: ${mem_usage}%"
fi
sleep $interval
done
}Log Analysis and Monitoring
#!/bin/bash
# Analyze system logs
analyze_system_logs() {
local log_file=${1:-"/var/log/syslog"}
local time_range=${2:-"last hour"}
echo "=== System Log Analysis ==="
# Recent errors
echo -e "\n=== Recent Errors ==="
grep -i "error\|fail\|critical" "$log_file" | tail -10
# Login attempts
echo -e "\n=== Recent Login Attempts ==="
grep -i "login\|ssh\|auth" "$log_file" | tail -10
# Service status changes
echo -e "\n=== Service Status Changes ==="
grep -i "start\|stop\|restart" "$log_file" | tail -10
# Resource warnings
echo -e "\n=== Resource Warnings ==="
grep -i "memory\|disk\|cpu" "$log_file" | tail -5
}
# Real-time log monitoring
monitor_logs() {
local log_file=$1
local pattern=$2
if [[ -z "$pattern" ]]; then
tail -f "$log_file"
else
tail -f "$log_file" | grep --line-buffered "$pattern"
fi
}
# Generate system report
generate_system_report() {
local report_file="system_report_$(date +%Y%m%d_%H%M%S).txt"
{
echo "=== System Report ==="
echo "Generated: $(date)"
echo ""
echo "=== System Information ==="
uname -a
echo ""
echo "=== Disk Usage ==="
df -h
echo ""
echo "=== Memory Usage ==="
free -h
echo ""
echo "=== Top Processes ==="
ps aux --sort=-%cpu | head -10
echo ""
echo "=== Network Connections ==="
netstat -tuln | head -10
echo ""
echo "=== System Load ==="
uptime
echo ""
echo "=== Recent Errors ==="
tail -20 /var/log/syslog | grep -i error
} > "$report_file"
echo "Report saved to: $report_file"
}Advanced File Operations
Complex file operations including batch processing, archiving, and synchronization.
File System Operations
#!/bin/bash
# Batch file operations
batch_process_files() {
local source_dir=$1
local operation=$2
case $operation in
"rename")
# Add timestamp to all files
find "$source_dir" -type f | while read file; do
local dir=$(dirname "$file")
local name=$(basename "$file")
local ext="${name##*.}"
local base="${name%.*}"
local timestamp=$(date +%Y%m%d_%H%M%S)
mv "$file" "$dir/${base}_${timestamp}.${ext}"
done
;;
"compress")
# Compress all log files
find "$source_dir" -name "*.log" -type f -exec gzip {} \;
;;
"cleanup")
# Remove files older than 30 days
find "$source_dir" -type f -mtime +30 -delete
;;
"organize")
# Organize files by extension
find "$source_dir" -type f | while read file; do
local ext="${file##*.}"
local target_dir="$source_dir/$ext"
mkdir -p "$target_dir"
mv "$file" "$target_dir/"
done
;;
esac
}
# Synchronize directories
sync_directories() {
local source=$1
local destination=$2
# Create destination if it doesn't exist
mkdir -p "$destination"
# Use rsync for efficient synchronization
rsync -av --delete --progress "$source/" "$destination/"
# Create sync log
echo "Sync completed at $(date)" >> "$destination/sync.log"
}
# Advanced backup system
advanced_backup() {
local source_dir=$1
local backup_dir=$2
local backup_type=${3:-"incremental"}
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_path="$backup_dir/backup_$timestamp"
case $backup_type in
"full")
# Full backup
tar -czf "$backup_path.tar.gz" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
;;
"incremental")
# Incremental backup (requires previous backup)
local last_backup=$(ls -t "$backup_dir"/backup_*.tar.gz | head -1)
if [[ -n "$last_backup" ]]; then
tar -czf "$backup_path.tar.gz" --newer "$last_backup" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
else
tar -czf "$backup_path.tar.gz" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
fi
;;
"differential")
# Differential backup (since last full backup)
local last_full=$(ls -t "$backup_dir"/backup_*.tar.gz | grep -v incremental | head -1)
if [[ -n "$last_full" ]]; then
tar -czf "$backup_path.tar.gz" --newer "$last_full" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
else
tar -czf "$backup_path.tar.gz" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
fi
;;
esac
echo "Backup completed: $backup_path.tar.gz"
}Error Handling and Validation
Robust error handling makes your scripts reliable and user-friendly.
Comprehensive Error Handling
#!/bin/bash
# Error handling setup
set -euo pipefail # Exit on error, undefined variables, and pipe failures
IFS=$'\n\t' # Set Internal Field Separator
# Custom error handler
error_handler() {
local line_number=$1
local exit_code=$2
echo "Error occurred in script at line $line_number (exit code: $exit_code)" >&2
echo "Command failed: ${BASH_COMMAND}" >&2
# Cleanup actions
cleanup_on_error
exit $exit_code
}
# Set up error trap
trap 'error_handler ${LINENO} $?' ERR
# Cleanup function
cleanup_on_error() {
echo "Performing cleanup..."
# Remove temporary files
rm -f /tmp/script_$$_*
# Kill background processes
jobs -p | xargs -r kill
}
# Input validation
validate_input() {
local input=$1
local type=$2
case $type in
"email")
[[ "$input" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
;;
"number")
[[ "$input" =~ ^[0-9]+$ ]]
;;
"directory")
[[ -d "$input" ]]
;;
"file")
[[ -f "$input" ]]
;;
*)
echo "Unknown validation type: $type" >&2
return 1
;;
esac
}
# Safe file operations
safe_file_operation() {
local operation=$1
local file=$2
# Validate file path
if [[ "$file" =~ \.\. ]]; then
echo "Error: Path traversal attempt detected" >&2
return 1
fi
# Ensure file is within allowed directory
local allowed_dir="/home/user/data"
if [[ ! "$file" == "$allowed_dir"* ]]; then
echo "Error: File outside allowed directory" >&2
return 1
fi
case $operation in
"read")
if [[ -r "$file" ]]; then
cat "$file"
else
echo "Error: File not readable" >&2
return 1
fi
;;
"write")
if [[ -w "$(dirname "$file")" ]]; then
# Create backup before writing
if [[ -f "$file" ]]; then
cp "$file" "$file.backup"
fi
return 0
else
echo "Error: Directory not writable" >&2
return 1
fi
;;
esac
}
# Main script with error handling
main() {
local input_file=$1
# Validate input
if ! validate_input "$input_file" "file"; then
echo "Error: Invalid input file" >&2
exit 1
fi
# Perform safe operations
safe_file_operation "read" "$input_file"
echo "Script completed successfully"
}
# Check if script is being sourced or executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fiIntegration with Other Tools
Shell scripts can integrate with databases, APIs, and other services.
Database Integration
#!/bin/bash
# MySQL database operations
mysql_query() {
local host=$1
local user=$2
local password=$3
local database=$4
local query=$5
mysql -h "$host" -u "$user" -p"$password" "$database" -e "$query"
}
# PostgreSQL operations
postgres_query() {
local host=$1
local user=$2
local database=$3
local query=$4
PGPASSWORD=$5 psql -h "$host" -U "$user" -d "$database" -c "$query"
}
# SQLite operations
sqlite_query() {
local database=$1
local query=$2
sqlite3 "$database" "$query"
}
# Example usage: Database backup
backup_database() {
local db_type=$1
local connection_params=$2
local output_file=$3
case $db_type in
"mysql")
mysql_dump "$connection_params" > "$output_file"
;;
"postgresql")
pg_dump "$connection_params" > "$output_file"
;;
"sqlite")
cp "$connection_params" "$output_file"
;;
esac
gzip "$output_file"
}API Integration
#!/bin/bash
# REST API calls
api_call() {
local method=$1
local url=$2
local data=$3
local headers=$4
local curl_cmd="curl -s -w '\n%{http_code}'"
curl_cmd+=" -X $method"
if [[ -n "$headers" ]]; then
curl_cmd+=" $headers"
fi
if [[ -n "$data" ]]; then
curl_cmd+=" -d '$data'"
fi
curl_cmd+=" '$url'"
local result=$(eval $curl_cmd)
local status_code=$(echo "$result" | tail -n1)
local response=$(echo "$result" | head -n -1)
if [[ $status_code -eq 200 ]]; then
echo "$response"
else
echo "API Error: HTTP $status_code" >&2
echo "$response" >&2
return 1
fi
}
# JSON processing with jq
parse_json_response() {
local response=$1
local field=$2
echo "$response" | jq -r ".$field"
}
# Example: Weather API
get_weather() {
local city=$1
local api_key=$2
local url="http://api.openweathermap.org/data/2.5/weather"
local params="q=$city&appid=$api_key&units=metric"
local response=$(curl -s "$url?$params")
if echo "$response" | jq . >/dev/null 2>&1; then
local temp=$(echo "$response" | jq -r '.main.temp')
local description=$(echo "$response" | jq -r '.weather[0].description')
echo "Weather in $city: $temp°C, $description"
else
echo "Error: Invalid response from weather API" >&2
return 1
fi
}Performance Optimization
Optimize your scripts for better performance and resource usage.
Optimization Techniques
#!/bin/bash
# Use arrays instead of string operations
fast_processing() {
local file=$1
# Read file into array (faster for multiple operations)
local lines=()
mapfile -t lines < "$file"
# Process with array operations
for line in "${lines[@]}"; do
# Process each line
echo "$line" | tr '[:lower:]' '[:upper:]'
done
}
# Use built-in commands instead of external processes
efficient_text_processing() {
local input=$1
# Use parameter expansion instead of external commands
input=${input##*/} # basename equivalent
input=${input%.*} # remove extension
echo "$input"
}
# Parallel processing
parallel_processing() {
local max_jobs=${1:-4}
# Process files in parallel
find . -type f -print0 | xargs -0 -P "$max_jobs" -I {} process_file {}
}
# Memory-efficient processing
streaming_processing() {
local input_file=$1
# Process line by line (memory efficient for large files)
while IFS= read -r line; do
# Process line
echo "$line" | grep "pattern"
done < "$input_file"
}
# Use bc for math operations instead of external commands
math_operations() {
local num1=$1
local num2=$2
# Use built-in arithmetic for simple operations
local sum=$((num1 + num2))
local product=$((num1 * num2))
# Use bc for complex operations
local sqrt=$(echo "scale=4; sqrt($num1)" | bc)
echo "Sum: $sum, Product: $product, Sqrt: $sqrt"
}Testing and Debugging
Professional scripting includes comprehensive testing and debugging practices.
Debugging Tools
#!/bin/bash
# Enable debugging
set -x # Print each command before execution
# Debug function
debug() {
if [[ "${DEBUG:-0}" == "1" ]]; then
echo "[DEBUG] $*" >&2
fi
}
# Performance profiling
profile_script() {
local start_time=$(date +%s.%N)
# Run the main script logic
"$@"
local end_time=$(date +%s.%N)
local runtime=$(echo "$end_time - $start_time" | bc)
echo "Script execution time: ${runtime}s"
}
# Unit testing framework
assert_equals() {
local expected=$1
local actual=$2
local message=$3
if [[ "$expected" == "$actual" ]]; then
echo "PASS: ${message:-'Test passed'}"
return 0
else
echo "FAIL: ${message:-'Test failed'} - Expected: $expected, Got: $actual"
return 1
fi
}
# Test suite
run_tests() {
local failed=0
# Test function
assert_equals "hello" "$(echo 'hello')" "Echo command"
((failed+=$?))
assert_equals "2" "$((1+1))" "Math operation"
((failed+=$?))
echo "Tests completed. Failures: $failed"
return $failed
}This advanced shell scripting guide equips you with powerful techniques for building sophisticated automation tools. Practice these concepts with real-world scenarios to become proficient in shell scripting.
For more Linux tutorials, explore our guide on command line basics and learn about window managers like i3 for a complete Linux experience.
The Advanced Bash-Scripting Guide is an excellent comprehensive resource, and the Bash Reference Manual provides detailed technical documentation for advanced users.