Your First Script

A shell script is a text file containing a sequence of commands that the shell executes. Every script begins with a shebang line that tells the system which interpreter to use:

#!/bin/bash
# This is a comment - it explains what the script does
echo "Hello, World!"
echo "Today is $(date +%A), $(date +%B) $(date +%d), $(date +%Y)"

Making a Script Executable

# Create the script file
nano myscript.sh

# Make it executable
chmod +x myscript.sh

# Run it
./myscript.sh

# Alternative: run with bash explicitly (no chmod needed)
bash myscript.sh

Variables

Variables store data for use throughout your script. In Bash, variables are assigned without spaces around the equals sign and accessed with a $ prefix:

#!/bin/bash

# Variable assignment (NO spaces around =)
name="Alice"
age=30
distro="Ubuntu"

# Using variables
echo "Name: $name"
echo "Age: $age"
echo "$name uses $distro"

# Curly braces for clarity (recommended)
echo "${name}'s system runs ${distro}"

# Command substitution - store command output in a variable
current_date=$(date +%Y-%m-%d)
file_count=$(ls -1 | wc -l)
kernel=$(uname -r)

echo "Date: $current_date"
echo "Files in current directory: $file_count"
echo "Kernel version: $kernel"

# Read-only variables
readonly PI=3.14159
# PI=3.0  # This would cause an error

# Arithmetic
count=5
count=$((count + 1))
result=$((10 * 5 + 3))
echo "Count: $count, Result: $result"

Special Variables

#!/bin/bash

echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Exit status of last command: $?"
echo "Process ID of this script: $$"
echo "PID of last background process: $!"

User Input

#!/bin/bash

# Basic input
read -p "Enter your name: " username
echo "Hello, $username!"

# Silent input (for passwords)
read -sp "Enter password: " password
echo  # newline after silent input
echo "Password length: ${#password} characters"

# Input with default value
read -p "Enter shell [/bin/bash]: " shell
shell=${shell:-/bin/bash}
echo "Shell set to: $shell"

# Input with timeout
read -t 10 -p "Quick! Enter a number (10s): " num

Conditionals

#!/bin/bash

# if/elif/else
age=25

if [ "$age" -lt 18 ]; then
    echo "Minor"
elif [ "$age" -lt 65 ]; then
    echo "Adult"
else
    echo "Senior"
fi

# String comparisons
name="Alice"

if [ "$name" = "Alice" ]; then
    echo "Hello, Alice!"
fi

if [ -z "$empty_var" ]; then
    echo "Variable is empty or unset"
fi

if [ -n "$name" ]; then
    echo "Variable is not empty"
fi

# File tests
if [ -f "/etc/passwd" ]; then
    echo "/etc/passwd exists and is a regular file"
fi

if [ -d "/home" ]; then
    echo "/home is a directory"
fi

if [ -r "/etc/hostname" ]; then
    echo "/etc/hostname is readable"
fi

if [ -x "/usr/bin/bash" ]; then
    echo "Bash is executable"
fi

# Combined conditions with && (AND) and || (OR)
if [ -f "/etc/passwd" ] && [ -r "/etc/passwd" ]; then
    echo "/etc/passwd exists and is readable"
fi

# Modern double-bracket syntax (preferred in Bash)
if [[ "$name" == A* ]]; then
    echo "Name starts with A"
fi

if [[ "$age" -ge 18 && "$age" -le 65 ]]; then
    echo "Working age"
fi

Case Statements

#!/bin/bash

read -p "Enter a fruit: " fruit

case "$fruit" in
    apple)
        echo "Apples are red or green"
        ;;
    banana)
        echo "Bananas are yellow"
        ;;
    orange|tangerine)
        echo "Citrus fruit!"
        ;;
    *)
        echo "Unknown fruit: $fruit"
        ;;
esac

Loops

#!/bin/bash

# For loop - iterating over a list
for color in red green blue yellow; do
    echo "Color: $color"
done

# For loop - iterating over files
for file in /var/log/*.log; do
    echo "Log file: $file ($(wc -l < "$file") lines)"
done

# C-style for loop
for ((i = 1; i <= 5; i++)); do
    echo "Iteration: $i"
done

# While loop
count=1
while [ "$count" -le 10 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

# Reading a file line by line
while IFS= read -r line; do
    echo "Line: $line"
done < /etc/hostname

# Until loop (runs while condition is false)
num=1
until [ "$num" -gt 5 ]; do
    echo "Number: $num"
    num=$((num + 1))
done

# Loop control
for i in $(seq 1 100); do
    if [ "$i" -eq 5 ]; then
        continue    # skip iteration
    fi
    if [ "$i" -eq 10 ]; then
        break       # exit loop
    fi
    echo "$i"
done

Functions

#!/bin/bash

# Define a function
greet() {
    local name="$1"    # local variable (scoped to function)
    local time_of_day="$2"
    echo "Good $time_of_day, $name!"
}

# Call the function
greet "Alice" "morning"
greet "Bob" "evening"

# Function with return value
is_root() {
    if [ "$(id -u)" -eq 0 ]; then
        return 0    # success (true)
    else
        return 1    # failure (false)
    fi
}

if is_root; then
    echo "Running as root"
else
    echo "Not running as root"
fi

# Function that outputs a value (captured with command substitution)
get_disk_usage() {
    df -h / | awk 'NR==2 {print $5}'
}

usage=$(get_disk_usage)
echo "Root partition usage: $usage"

Arrays

#!/bin/bash

# Declare an array
fruits=("apple" "banana" "cherry" "date")

# Access elements
echo "First fruit: ${fruits[0]}"
echo "Third fruit: ${fruits[2]}"

# All elements
echo "All fruits: ${fruits[@]}"

# Array length
echo "Number of fruits: ${#fruits[@]}"

# Add an element
fruits+=("elderberry")

# Loop through an array
for fruit in "${fruits[@]}"; do
    echo "- $fruit"
done

# Loop with index
for i in "${!fruits[@]}"; do
    echo "$i: ${fruits[$i]}"
done

# Remove an element
unset 'fruits[1]'

# Associative arrays (Bash 4+)
declare -A capitals
capitals[France]="Paris"
capitals[Germany]="Berlin"
capitals[Japan]="Tokyo"

echo "Capital of France: ${capitals[France]}"

for country in "${!capitals[@]}"; do
    echo "$country -> ${capitals[$country]}"
done

Practical Examples

Backup Script

#!/bin/bash

# Simple backup script
backup_dir="/home/$(whoami)/backups"
source_dir="/home/$(whoami)/documents"
timestamp=$(date +%Y%m%d_%H%M%S)
backup_file="backup_${timestamp}.tar.gz"

# Create backup directory if it does not exist
mkdir -p "$backup_dir"

# Create compressed archive
tar -czf "${backup_dir}/${backup_file}" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"

if [ $? -eq 0 ]; then
    echo "Backup created: ${backup_dir}/${backup_file}"
    echo "Size: $(du -h "${backup_dir}/${backup_file}" | cut -f1)"
else
    echo "ERROR: Backup failed!" >&2
    exit 1
fi

# Remove backups older than 30 days
find "$backup_dir" -name "backup_*.tar.gz" -mtime +30 -delete
echo "Old backups cleaned up."

System Health Check

#!/bin/bash

echo "=== System Health Report ==="
echo "Date: $(date)"
echo "Hostname: $(hostname)"
echo ""

echo "--- CPU Load ---"
uptime | awk -F'load average:' '{print $2}'

echo ""
echo "--- Memory Usage ---"
free -h | grep -E "Mem|Swap"

echo ""
echo "--- Disk Usage ---"
df -h | grep -E "^/dev" | awk '{print $1, $5, "used of", $2}'

echo ""
echo "--- Top 5 Processes by CPU ---"
ps aux --sort=-%cpu | head -6

echo ""
echo "--- Top 5 Processes by Memory ---"
ps aux --sort=-%mem | head -6

Best Practices

  • Always quote variables: "$var" prevents word splitting and glob expansion.
  • Use set -euo pipefail at the top of scripts to catch errors early.
  • Use shellcheck to lint your scripts for common mistakes.
  • Use local for function variables to avoid polluting the global scope.
  • Always check return codes for critical operations.