Linux Shell Scripting Fundamentals

Once you are comfortable running commands individually (see LinuxCommandLineEssentials), the next step is combining them into scripts — files that run a sequence of commands automatically. If you have ever written a batch file (.bat) in Windows or used PowerShell scripts, the concept is the same. The syntax is different, and bash scripting is both more powerful and more quirky than either.

Shell scripting is where Linux becomes genuinely transformative. The ability to automate anything you can do in the terminal — backups, deployments, log analysis, system monitoring — is the practical payoff of learning the command line.

Your First Script

#!/bin/bash
# My first script — greet the user and show system info

echo "Hello, $(whoami)!"
echo "Today is $(date '+%A, %B %d, %Y')"
echo "You are on $(hostname)"
echo "Uptime: $(uptime -p)"

Save this as info.sh, make it executable, and run it:

chmod +x info.sh
./info.sh

Key points:

Variables

# Assigning variables (NO spaces around =)
name="Jake"
count=42
today=$(date '+%Y-%m-%d')

# Using variables (prefix with $)
echo "Hello, $name"
echo "Count: $count"
echo "Date: $today"

# Quoting matters
file="my document.txt"
cp "$file" backup/     # CORRECT: quotes preserve spaces
cp $file backup/        # WRONG: bash sees two arguments: "my" and "document.txt"

Always quote your variables. "$variable" prevents word splitting on spaces. This is the single most common source of bugs in shell scripts.

Conditionals

#!/bin/bash

if [ -f "/etc/nginx/nginx.conf" ]; then
    echo "Nginx is configured"
elif [ -f "/etc/apache2/apache2.conf" ]; then
    echo "Apache is configured"
else
    echo "No web server config found"
fi

Common Test Operators

OperatorMeaningExample
-f fileFile exists and is a regular file[ -f config.txt ]
-d dirDirectory exists[ -d /var/log ]
-e pathPath exists (any type)[ -e /tmp/lock ]
-z "$var"Variable is empty[ -z "$name" ]
-n "$var"Variable is not empty[ -n "$name" ]
"$a" = "$b"Strings are equal[ "$user" = "root" ]
"$a" != "$b"Strings differ[ "$env" != "prod" ]
$a -eq $bNumbers are equal[ $count -eq 0 ]
$a -gt $bGreater than[ $size -gt 1000 ]
$a -lt $bLess than[ $age -lt 18 ]

Loops

# Loop over a list
for server in web01 web02 web03; do
    echo "Checking $server..."
    ping -c 1 "$server" > /dev/null 2>&1 && echo "  up" || echo "  down"
done

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

# Loop with a counter
for i in {1..10}; do
    echo "Iteration $i"
done

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

# Read a file line by line
while IFS= read -r line; do
    echo "Processing: $line"
done < input.txt

Functions

#!/bin/bash

# Define a function
backup_file() {
    local source="\$1"    # First argument
    local dest="\$2"      # Second argument
    
    if [ ! -f "$source" ]; then
        echo "ERROR: $source does not exist" >&2
        return 1
    fi
    
    cp "$source" "$dest"
    echo "Backed up $source to $dest"
}

# Call the function
backup_file /etc/nginx/nginx.conf /tmp/nginx.conf.bak

Practical Script Examples

Backup Script

#!/bin/bash
# Daily backup of important directories

BACKUP_DIR="/backup/$(date '+%Y-%m-%d')"
SOURCE_DIRS=("/home/jake/documents" "/home/jake/projects" "/etc")

mkdir -p "$BACKUP_DIR"

for dir in "${SOURCE_DIRS[@]}"; do
    dirname=$(basename "$dir")
    tar czf "$BACKUP_DIR/${dirname}.tar.gz" "$dir" 2>/dev/null
    echo "Backed up $dir"
done

# Remove backups older than 30 days
find /backup -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +

echo "Backup complete: $BACKUP_DIR"

Log Monitor

#!/bin/bash
# Watch a log file for errors and alert

LOGFILE="/var/log/application.log"
ALERT_PATTERN="ERROR|CRITICAL|FATAL"

tail -F "$LOGFILE" | while IFS= read -r line; do
    if echo "$line" | grep -qE "$ALERT_PATTERN"; then
        echo "[ALERT $(date '+%H:%M:%S')] $line"
    fi
done

System Health Check

#!/bin/bash
# Quick system health report

echo "=== System Health Report ==="
echo "Date: $(date)"
echo
echo "--- Disk Usage ---"
df -h / /home | tail -n +2
echo
echo "--- Memory ---"
free -h | head -2
echo
echo "--- Load Average ---"
uptime
echo
echo "--- Top 5 CPU Processes ---"
ps aux --sort=-%cpu | head -6
echo
echo "--- Failed Services ---"
systemctl --failed --no-legend

Scheduling with cron

cron runs scripts on a schedule — the Linux equivalent of Windows Task Scheduler, but configured with a text file.

Edit your crontab:

crontab -e

Format: minute hour day-of-month month day-of-week command

# Run backup every day at 2 AM
0 2 * * * /home/jake/scripts/backup.sh

# Run health check every 5 minutes
*/5 * * * * /home/jake/scripts/health.sh >> /var/log/health.log 2>&1

# Run cleanup every Sunday at midnight
0 0 * * 0 /home/jake/scripts/cleanup.sh

# Run at 9 AM on the 1st of every month
0 9 1 * * /home/jake/scripts/monthly-report.sh

Cron tip: Always use full paths in cron jobs. Cron runs with a minimal environment — commands like node or python3 need their full path (use which python3 to find it).

Customizing Your Environment

Your shell configuration lives in dotfiles in your home directory:

FilePurpose
~/.bashrcRuns every time you open a terminal (aliases, functions, prompt)
~/.bash_profileRuns on login (environment variables, PATH)
~/.profileGeneric login configuration (used by many shells)

Common customizations:

# Add to ~/.bashrc

# Useful aliases
alias ll='ls -la'
alias gs='git status'
alias update='sudo apt update && sudo apt upgrade -y'
alias ..='cd ..'
alias ...='cd ../..'

# Better history
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups

# Custom prompt showing git branch
parse_git_branch() {
    git branch 2>/dev/null | grep '\*' | sed 's/* //'
}
export PS1='\u@\h:\w$(parse_git_branch)\$ '

After editing, apply changes: source ~/.bashrc

Further Reading