Linux Shell Scripting Fundamentals
Once you are comfortable running commands individually (see [LinuxCommandLineEssentials](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
```bash
!/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:
```bash
chmod +x info.sh
./info.sh
```
**Key points:**
- `#!/bin/bash` (the "shebang") tells the system which interpreter to use
- `#` starts a comment
- `$(command)` runs a command and inserts its output
- `echo` prints text
Variables
```bash
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
```bash
!/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
| Operator | Meaning | Example |
|---------|---------|--------|
| `-f file` | File exists and is a regular file | `[ -f config.txt ]` |
| `-d dir` | Directory exists | `[ -d /var/log ]` |
| `-e path` | Path 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 $b` | Numbers are equal | `[ $count -eq 0 ]` |
| `$a -gt $b` | Greater than | `[ $size -gt 1000 ]` |
| `$a -lt $b` | Less than | `[ $age -lt 18 ]` |
Loops
```bash
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
```bash
!/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
```bash
!/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
```bash
!/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
```bash
!/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:
```bash
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:
| File | Purpose |
|------|--------|
| `~/.bashrc` | Runs every time you open a terminal (aliases, functions, prompt) |
| `~/.bash_profile` | Runs on login (environment variables, PATH) |
| `~/.profile` | Generic login configuration (used by many shells) |
Common customizations:
```bash
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
- [LinuxForWindowsUsers](LinuxForWindowsUsers) — The complete learning roadmap
- [Linux Command Line Essentials](LinuxCommandLineEssentials) — The commands your scripts will use
- [Linux System Administration](LinuxSystemAdministration) — The systems your scripts will manage
- [Linux Filesystem and Permissions](LinuxFilesystemAndPermissions) — Understanding script permissions and paths