Shell Scripting¶
🎯 Learning Objectives
- Master Bash scripting fundamentals
- Learn variables, loops, and conditionals
- Understand functions and error handling
- Write production-ready scripts
Shell scripting is essential for DevOps automation. This chapter covers Bash scripting from basics to advanced patterns.
Interview Focus
Be ready to write scripts on the spot. Practice common patterns like file processing, error handling, and automation.
Basic Script Structure¶
#!/bin/bash
# Script description
# Author: Your Name
# Date: 2024
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Script body
echo "Hello, World!"
Shebang
#!/bin/bash tells the system which interpreter to use. Always include it.
Variables¶
# Declare variables
NAME="John"
AGE=30
# Use variables
echo "Name: $NAME"
echo "Age: $AGE"
# Command substitution
CURRENT_DIR=$(pwd)
DATE=$(date +%Y-%m-%d)
# Environment variables
echo "Home: $HOME"
echo "User: $USER"
Variable Naming
- Use uppercase for constants
- Use lowercase for variables
- No spaces around
= - Quote variables to prevent word splitting
Conditionals¶
# If statement
if [ "$AGE" -gt 18 ]; then
echo "Adult"
elif [ "$AGE" -gt 13 ]; then
echo "Teenager"
else
echo "Child"
fi
# File checks
if [ -f "file.txt" ]; then
echo "File exists"
fi
if [ -d "directory" ]; then
echo "Directory exists"
fi
# String comparison
if [ "$NAME" == "John" ]; then
echo "Hello John"
fi
File Test Operators:
- -f: File exists and is regular file
- -d: Directory exists
- -r: File is readable
- -w: File is writable
- -x: File is executable
- -s: File exists and is not empty
Loops¶
For Loop¶
# Iterate over list
for i in 1 2 3 4 5; do
echo "Number: $i"
done
# Iterate over files
for file in *.txt; do
echo "Processing: $file"
done
# C-style for loop
for ((i=1; i<=10; i++)); do
echo "Count: $i"
done
While Loop¶
# While condition
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
((count++))
done
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
Functions¶
# Define function
function greet() {
local name=$1
echo "Hello, $name!"
}
# Call function
greet "John"
# Function with return value
function add() {
local a=$1
local b=$2
echo $((a + b))
}
result=$(add 5 3)
echo "Sum: $result"
Best Practices
- Use
localfor function variables - Return values via
echoand capture with$() - Keep functions small and focused
Error Handling¶
#!/bin/bash
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO"' ERR
# Check command success
if ! command -v docker &> /dev/null; then
echo "Docker not found"
exit 1
fi
# Handle errors gracefully
if ! docker ps; then
echo "Docker daemon not running"
exit 1
fi
Common Patterns¶
Logging¶
LOG_FILE="/var/log/script.log"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Script started"
log "Processing files"
log "Script completed"
Input Validation¶
# Check arguments
if [ $# -eq 0 ]; then
echo "Usage: $0 <filename>"
exit 1
fi
# Validate file exists
if [ ! -f "$1" ]; then
echo "Error: File $1 not found"
exit 1
fi
Processing Files¶
# Process all files in directory
for file in /path/to/files/*; do
if [ -f "$file" ]; then
echo "Processing: $file"
# Process file
fi
done
Advanced Topics¶
Arrays¶
# Declare array
fruits=("apple" "banana" "orange")
# Access elements
echo "${fruits[0]}"
echo "${fruits[@]}" # All elements
# Array length
echo "${#fruits[@]}"
# Iterate array
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
String Manipulation¶
STRING="Hello World"
# Length
echo "${#STRING}"
# Substring
echo "${STRING:0:5}" # "Hello"
echo "${STRING:6}" # "World"
# Replace
echo "${STRING/World/Universe}"
# Remove prefix/suffix
echo "${STRING#Hello }" # Remove prefix
echo "${STRING% World}" # Remove suffix
Comprehensive Interview Questions¶
Q1: Write a script to find and delete files older than 30 days¶
Answer:
#!/bin/bash
set -euo pipefail
DIRECTORY="/var/log"
DAYS=30
# Find and delete files older than specified days
find "$DIRECTORY" -type f -mtime +$DAYS -delete
echo "Deleted files older than $DAYS days from $DIRECTORY"
Enhanced version with logging:
#!/bin/bash
set -euo pipefail
DIRECTORY="${1:-/var/log}"
DAYS="${2:-30}"
LOG_FILE="/var/log/cleanup.log"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Starting cleanup of files older than $DAYS days in $DIRECTORY"
# Count files before deletion
COUNT=$(find "$DIRECTORY" -type f -mtime +$DAYS | wc -l)
# Delete files
find "$DIRECTORY" -type f -mtime +$DAYS -delete
log "Deleted $COUNT files"
Q2: Write a script to backup a directory with rotation¶
Answer:
#!/bin/bash
set -euo pipefail
SOURCE_DIR="${1:-/home/user/data}"
BACKUP_DIR="${2:-/backup}"
RETENTION_DAYS=7
# Validate source directory
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Source directory $SOURCE_DIR does not exist"
exit 1
fi
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Generate backup filename with timestamp
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backup_$(basename $SOURCE_DIR)_$TIMESTAMP.tar.gz"
# Create backup
tar -czf "$BACKUP_FILE" -C "$(dirname $SOURCE_DIR)" "$(basename $SOURCE_DIR)"
# Remove old backups
find "$BACKUP_DIR" -name "backup_$(basename $SOURCE_DIR)_*.tar.gz" \
-mtime +$RETENTION_DAYS -delete
echo "Backup created: $BACKUP_FILE"
Q3: Write a script to check if a service is running and restart if not¶
Answer:
#!/bin/bash
set -euo pipefail
SERVICE="${1:-nginx}"
MAX_RETRIES=3
RETRY_DELAY=5
check_service() {
if systemctl is-active --quiet "$SERVICE"; then
return 0
else
return 1
fi
}
start_service() {
systemctl start "$SERVICE"
sleep "$RETRY_DELAY"
}
# Check if service is running
if check_service; then
echo "$SERVICE is running"
exit 0
fi
# Try to start service
echo "$SERVICE is not running. Attempting to start..."
for i in $(seq 1 $MAX_RETRIES); do
start_service
if check_service; then
echo "$SERVICE started successfully"
exit 0
fi
echo "Attempt $i failed. Retrying..."
done
echo "Failed to start $SERVICE after $MAX_RETRIES attempts"
exit 1
Q4: Write a script to monitor disk usage and send alert if above threshold¶
Answer:
#!/bin/bash
set -euo pipefail
THRESHOLD=80
PARTITION="${1:-/}"
ALERT_EMAIL="admin@example.com"
# Get disk usage percentage
USAGE=$(df -h "$PARTITION" | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
MESSAGE="Warning: Disk usage on $PARTITION is ${USAGE}% (threshold: ${THRESHOLD}%)"
echo "$MESSAGE"
# Send email alert (requires mail configured)
echo "$MESSAGE" | mail -s "Disk Usage Alert" "$ALERT_EMAIL"
# Or log to syslog
logger -t disk-monitor "$MESSAGE"
exit 1
fi
echo "Disk usage is normal: ${USAGE}%"
exit 0
Q5: Write a script to process log files and extract errors¶
Answer:
#!/bin/bash
set -euo pipefail
LOG_DIR="${1:-/var/log}"
OUTPUT_FILE="${2:-/tmp/errors.txt}"
PATTERN="${3:-ERROR|FATAL|CRITICAL}"
# Clear output file
> "$OUTPUT_FILE"
# Process all log files
find "$LOG_DIR" -type f -name "*.log" | while read -r logfile; do
echo "Processing: $logfile"
grep -iE "$PATTERN" "$logfile" >> "$OUTPUT_FILE" || true
done
# Count errors
ERROR_COUNT=$(wc -l < "$OUTPUT_FILE")
echo "Found $ERROR_COUNT errors. Results saved to $OUTPUT_FILE"
# Show top 10 most common errors
echo -e "\nTop 10 most common errors:"
sort "$OUTPUT_FILE" | uniq -c | sort -rn | head -10
Q6: Write a function to validate IP address¶
Answer:
#!/bin/bash
validate_ip() {
local ip=$1
local stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
OIFS=$IFS
IFS='.'
ip=($ip)
IFS=$OIFS
[[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && \
${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
stat=$?
fi
return $stat
}
# Usage
if validate_ip "192.168.1.1"; then
echo "Valid IP"
else
echo "Invalid IP"
fi
Q7: Write a script to parse CSV file and process data¶
Answer:
#!/bin/bash
set -euo pipefail
CSV_FILE="${1:-data.csv}"
if [ ! -f "$CSV_FILE" ]; then
echo "Error: File $CSV_FILE not found"
exit 1
fi
# Skip header and process each line
tail -n +2 "$CSV_FILE" | while IFS=',' read -r name email age; do
echo "Name: $name, Email: $email, Age: $age"
# Process data here
done
Q8: Write a script with proper error handling and logging¶
Answer:
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="$SCRIPT_DIR/script.log"
# Logging function
log() {
local level=$1
shift
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}
# Error handler
error_exit() {
log "ERROR" "$1"
exit 1
}
# Trap errors
trap 'error_exit "Error on line $LINENO"' ERR
log "INFO" "Script started"
# Your script logic here
if [ $# -eq 0 ]; then
error_exit "No arguments provided"
fi
log "INFO" "Processing argument: $1"
# Process argument...
log "INFO" "Script completed successfully"
Q9: Explain the difference between $* and $@¶
Answer:
- $*: Expands to all arguments as a single word
$@: Expands to all arguments as separate words
# If called with: script.sh "arg1" "arg2" "arg3"
echo "$@" # Output: arg1 arg2 arg3 (preserves arguments)
When to use:
- Use "$@" when you want to preserve arguments (recommended)
- Use $* when you want all arguments as one string
Q10: Write a script to check multiple services and report status¶
Answer:
#!/bin/bash
set -euo pipefail
SERVICES=("nginx" "mysql" "redis" "docker")
check_services() {
local all_ok=true
for service in "${SERVICES[@]}"; do
if systemctl is-active --quiet "$service"; then
echo "✓ $service is running"
else
echo "✗ $service is NOT running"
all_ok=false
fi
done
if [ "$all_ok" = true ]; then
echo "All services are running"
return 0
else
echo "Some services are not running"
return 1
fi
}
check_services
Q11: How do you handle command-line arguments in a script?¶
Answer:
#!/bin/bash
# Method 1: Positional parameters
FIRST_ARG=$1
SECOND_ARG=$2
# Method 2: Using getopts (for options)
while getopts "a:b:c:" opt; do
case $opt in
a) ARG_A="$OPTARG" ;;
b) ARG_B="$OPTARG" ;;
c) ARG_C="$OPTARG" ;;
\?) echo "Invalid option" ;;
esac
done
# Method 3: Using shift
while [ $# -gt 0 ]; do
case $1 in
--name) NAME=$2; shift ;;
--age) AGE=$2; shift ;;
*) echo "Unknown option: $1" ;;
esac
shift
done
Q12: Write a script to monitor process and restart if it dies¶
Answer:
#!/bin/bash
set -euo pipefail
PROCESS_NAME="${1:-myapp}"
CHECK_INTERVAL=60
MAX_RESTARTS=5
RESTART_COUNT=0
monitor_process() {
while true; do
if ! pgrep -f "$PROCESS_NAME" > /dev/null; then
echo "[$(date)] Process $PROCESS_NAME not found. Restarting..."
if [ $RESTART_COUNT -ge $MAX_RESTARTS ]; then
echo "Max restarts reached. Exiting."
exit 1
fi
# Restart process (adjust command as needed)
/path/to/$PROCESS_NAME &
((RESTART_COUNT++))
else
RESTART_COUNT=0 # Reset counter if process is running
fi
sleep $CHECK_INTERVAL
done
}
monitor_process
Best Practices¶
- Always use
set -euo pipefail - Quote variables to prevent word splitting
- Use
localin functions - Check return codes of commands
- Add comments for complex logic
- Validate input before processing
- Use meaningful variable names
- Handle errors gracefully
Previous: Linux Fundamentals | Next: Git & Version Control