Shell Scripting Guide — Variables, Conditionals, Loops, Functions, Error Handling
Shell scripting is the automation backbone of Linux administration. This guide teaches production-ready Bash scripting — from variables and conditionals to functions, error handling, and debugging — so you can automate repetitive tasks, build deployment pipelines, and create monitoring tools.
What You’ll Learn
You’ll write Bash scripts with proper variable handling, conditional logic, loop structures, reusable functions, robust error handling, and debugging techniques. You’ll also see how DodaZIP uses shell scripts for its build pipeline and how Durga Antivirus Pro uses scripts for log analysis and threat detection automation.
Why Shell Scripting Matters
A single shell script can replace hours of manual work. Deploying an application to 10 servers becomes a one-command operation. Log analysis, backup rotation, health checks, and system monitoring are all scripting tasks. Shell scripting is the force multiplier for every system administrator.
Learning Path
flowchart LR
A[Security Hardening] --> B[Shell Scripting<br/>You are here]
B --> C[Monitoring & Logging]
C --> D[Infrastructure Automation]
style B fill:#f90,color:#fff
Script Structure and Shebang
Every Bash script starts with a shebang line that tells the system which interpreter to use:
#!/bin/bash
# A minimal script
echo "Hello, World!"Save as hello.sh, make executable, and run:
chmod +x hello.sh
./hello.shVariables
# Assignment (no spaces around =)
name="Alice"
count=42
today=$(date +%Y-%m-%d) # Command substitution
files=$(ls -la) # Alternative: `ls -la`
# Usage — always quote variables to prevent word splitting
echo "$name"
echo "Count: $count"
echo "Today: $today"
# Read-only variables
readonly API_KEY="abc123"
# Default values
echo "${GREETING:-Hello}" # If GREETING unset, use "Hello"
echo "${NAME:?Name is required}" # Error if unset
# Positional parameters
echo "Script: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "All args: $*"
echo "Args count: $#"Conditionals
if/elif/else
if [ "$1" = "start" ]; then
echo "Starting service..."
elif [ "$1" = "stop" ]; then
echo "Stopping service..."
else
echo "Usage: $0 {start|stop}"
exit 1
fiTest Operators
# File tests
[ -f "$file" ] # File exists and is regular
[ -d "$dir" ] # Directory exists
[ -x "$binary" ] # File is executable
[ -s "$file" ] # File exists and is non-empty
# String tests
[ -z "$var" ] # String is empty
[ -n "$var" ] # String is non-empty
[ "$a" = "$b" ] # Strings equal
[ "$a" != "$b" ] # Strings different
# Numeric tests
[ "$num" -eq 0 ] # Equal
[ "$num" -lt 10 ] # Less than
[ "$num" -gt 100 ] # Greater than
# Compound conditions
[ -f "$file" ] && [ -s "$file" ] # AND
[ -z "$var" ] || [ -n "$var2" ] # ORcase Statements
case "$1" in
start)
echo "Starting..."
/usr/bin/myapp start
;;
stop|kill)
echo "Stopping..."
/usr/bin/myapp stop
;;
restart)
"$0" stop
"$0" start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esacLoops
for Loop
# Loop over a list
for server in web-01 web-02 db-01; do
echo "Deploying to $server..."
ssh "$server" "systemctl restart myapp"
done
# Loop over numbers
for i in {1..5}; do
echo "Attempt $i"
done
# Loop over files
for log in /var/log/*.log; do
echo "Processing: $log"
gzip "$log"
done
# C-style for loop
for ((i=0; i<10; i++)); do
echo "Iteration $i"
donewhile Loop
# Read a file line by line
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hosts
# Infinite loop with condition
count=0
while [ "$count" -lt 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
# Watch a process
while pgrep -x "myapp" >/dev/null; do
echo "myapp is running..."
sleep 5
done
echo "myapp has stopped!"until Loop
# Run until condition is true
until ping -c 1 -W 1 "$HOST" >/dev/null 2>&1; do
echo "Waiting for $HOST to be reachable..."
sleep 2
done
echo "$HOST is up!"Functions
# Define a function
health_check() {
local service="$1"
local port="$2"
if nc -z localhost "$port"; then
echo "$service is running on port $port"
return 0
else
echo "$service is NOT running!"
return 1
fi
}
# Call the function
health_check "nginx" 80
health_check "mysql" 3306
# Function with return value
is_valid_ip() {
local ip="$1"
if [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
return 0
else
return 1
fi
}
if is_valid_ip "192.168.1.100"; then
echo "Valid IP"
fiVariable Scope
Use local inside functions to avoid global scope pollution:
# Bad — global variable leak
set_name() {
name="Alice" # Modifies global variable!
}
# Good — local variable
set_name() {
local name="Alice" # Scoped to function
echo "$name"
}Error Handling
Exit Codes and Error Traps
#!/bin/bash
set -euo pipefail # Fail on error, unset vars, pipe failures
# Or explicitly:
set -e # Exit immediately on error
set -u # Error on unset variables
set -o pipefail # Pipes fail if any command fails
# Custom error handler
error_handler() {
local line="$1"
local command="$2"
echo "ERROR: Command '$command' failed at line $line"
exit 1
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
# Check command success
if ! mkdir -p /backups/daily; then
echo "Failed to create backup directory"
exit 1
fiInput Validation
validate_input() {
local input="$1"
# Check if empty
if [ -z "$input" ]; then
echo "Error: Input cannot be empty"
return 1
fi
# Check for dangerous characters
if echo "$input" | grep -q '[;&|`$]'; then
echo "Error: Input contains dangerous characters"
return 1
fi
return 0
}
read -p "Enter filename: " filename
if validate_input "$filename"; then
cat "$filename"
fiArrays
# Declare an array
servers=("web-01" "web-02" "db-01")
# Access elements
echo "${servers[0]}" # web-01
echo "${servers[@]}" # All elements
echo "${#servers[@]}" # Length
# Loop over array
for server in "${servers[@]}"; do
echo "Deploying to $server"
done
# Associative arrays (Bash 4+)
declare -A ports
ports[nginx]=80
ports[mysql]=3306
ports[redis]=6379
echo "nginx port: ${ports[nginx]}"Debugging
# Run with debug mode
bash -x script.sh
# Debug specific sections
set -x # Start debugging
# ... code to debug ...
set +x # Stop debugging
# Verbose mode (show lines before execution)
bash -v script.sh
# Check syntax without running
bash -n script.shCommon Debugging Patterns
#!/bin/bash
# Print what you're about to run
echo "Executing: cp $SOURCE $DEST"
cp "$SOURCE" "$DEST"
# Check after each critical step
echo "Backup size: $(du -sh "$DEST")"
# Use shellcheck for static analysis
# Install: sudo apt install shellcheck
# Run: shellcheck script.shProduction Script Template
#!/bin/bash
# Production-ready script template
# Usage: ./deploy.sh [environment]
set -euo pipefail
# Configuration
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/$(basename "$0" .sh).log"
readonly TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# Logging
log() {
local level="$1"
shift
echo "[$TIMESTAMP] [$level] $*" | tee -a "$LOG_FILE"
}
info() { log "INFO" "$*"; }
warn() { log "WARN" "$*"; }
error() { log "ERROR" "$*"; exit 1; }
# Cleanup on exit
cleanup() {
local exit_code=$?
info "Script finished with exit code $exit_code"
# Remove temp files, unlock resources, etc.
}
trap cleanup EXIT
# Usage
usage() {
echo "Usage: $0 [options] <environment>"
echo "Options:"
echo " -v Verbose output"
echo " -d Dry run (no changes)"
exit 1
}
# Parse arguments
VERBOSE=false
DRY_RUN=false
while getopts "vd" opt; do
case "$opt" in
v) VERBOSE=true ;;
d) DRY_RUN=true ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
ENVIRONMENT="${1:?Environment is required (staging|production)}"
info "Starting deployment to $ENVIRONMENT"
info "Dry run: $DRY_RUN"
if [ "$DRY_RUN" = true ]; then
info "Would execute: ansible-playbook -i $ENVIRONMENT deploy.yml"
exit 0
fi
# Main logic
info "Running deployment..."
# ansible-playbook -i "$ENVIRONMENT" deploy.yml
info "Deployment complete!"Common Scripting Mistakes
1. Not Quoting Variables
echo $var splits on whitespace and expands globs. Always use echo "$var". The difference between rm -rf $dir and rm -rf "$dir" is potentially catastrophic.
2. Using for Loops Over Command Output
for file in $(ls *.txt) breaks on filenames with spaces. Use for file in *.txt or while IFS= read -r file.
3. Missing Error Handling
Commands fail silently without set -e. A failed mkdir, cp, or rm continues execution, potentially corrupting data.
4. Running Scripts with sh Instead of bash
sh may be dash (Debian) or a minimal POSIX shell without arrays, [[ ]], or source. Always use #!/bin/bash if you use Bash features.
5. Hardcoding Paths
/home/alice/backups/ works only on your machine. Use relative paths, environment variables, or configuration files.
6. Not Using Shellcheck
Shellcheck catches 90% of common mistakes — unquoted variables, missing error handling, deprecated syntax. Run it before deploying scripts.
7. Leaving Sensitive Data in Scripts
Hardcoding passwords, API keys, or tokens in scripts is a security risk. Use environment variables or encrypted config files.
Practice Questions
1. What does set -euo pipefail do?
-e: exit on first error. -u: error on unset variables. -o pipefail: entire pipeline fails if any command fails.
2. How do you make a variable read-only in Bash?
readonly VAR_NAME="value". Any subsequent attempt to modify it will fail.
3. What’s the difference between $@ and $*?
$@ expands each argument as a separate word (preserving quotes). $* expands all arguments as a single word. Use "$@" for safe argument forwarding.
4. How do you check if a command succeeded?
Check $? which holds the exit code of the last command. 0 = success, non-zero = failure. Or use if command; then to check directly.
5. Challenge: Write a script that monitors a log file for ERROR entries and sends an alert if 10 or more errors appear within 5 minutes. Include proper error handling and run continuously.
Answer: A while loop with tail -f, counting ERROR lines with grep, comparing against a threshold, and using mail or curl to send an alert. Include trap for clean exit.
Mini Project: Application Health Checker
Create a script that checks multiple aspects of application health:
#!/bin/bash
# app_health.sh — Multi-check application health monitor
# Usage: ./app_health.sh
set -euo pipefail
readonly SCRIPT_NAME=$(basename "$0")
readonly ALERT_EMAIL="ops@dodatech.com"
readonly THRESHOLD_DISK=90 # Percent
readonly THRESHOLD_MEM=90 # Percent
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
check_pass() { echo -e "${GREEN}PASS${NC} $1"; }
check_fail() { echo -e "${RED}FAIL${NC} $1"; }
# HTTP check
check_http() {
local url="$1"
local expected="${2:-200}"
local status
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url" 2>/dev/null || echo "000")
if [ "$status" = "$expected" ]; then
check_pass "HTTP $url → $status"
return 0
else
check_fail "HTTP $url → $status (expected $expected)"
return 1
fi
}
# Disk check
check_disk() {
local usage
usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$usage" -lt "$THRESHOLD_DISK" ]; then
check_pass "Disk usage: ${usage}%"
return 0
else
check_fail "Disk usage: ${usage}% (threshold: ${THRESHOLD_DISK}%)"
return 1
fi
}
# Memory check
check_memory() {
local usage
usage=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
if [ "$usage" -lt "$THRESHOLD_MEM" ]; then
check_pass "Memory usage: ${usage}%"
return 0
else
check_fail "Memory usage: ${usage}% (threshold: ${THRESHOLD_MEM}%)"
return 1
fi
}
# Process check
check_process() {
local proc="$1"
if pgrep -x "$proc" >/dev/null; then
local count
count=$(pgrep -c -x "$proc")
check_pass "Process $proc: running ($count instances)"
return 0
else
check_fail "Process $proc: NOT running"
return 1
fi
}
# Main
echo "=== Application Health Check ==="
echo "Time: $(date)"
echo ""
failures=0
check_http "http://localhost:80" 200 || ((failures++))
check_http "http://localhost:8080/health" 200 || ((failures++))
check_disk || ((failures++))
check_memory || ((failures++))
check_process "nginx" || ((failures++))
check_process "myapp" || ((failures++))
echo ""
if [ "$failures" -eq 0 ]; then
echo "Result: ALL CHECKS PASSED"
exit 0
else
echo "Result: $failures check(s) FAILED"
# Send alert
# mail -s "Alert: $failures health checks failed" "$ALERT_EMAIL" < "$0"
exit 1
fiExpected output:
=== Application Health Check ===
Time: Sat Jun 20 10:00:00 UTC 2026
PASS HTTP http://localhost:80 → 200
PASS HTTP http://localhost:8080/health → 200
PASS Disk usage: 45%
PASS Memory usage: 62%
PASS Process nginx: running (2 instances)
PASS Process myapp: running (1 instances)
Result: ALL CHECKS PASSEDFAQ
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro