What You’ll Learn in This Tutorial
- Basic structure of shell scripts
- Using variables
- Conditional branching (if statements)
- Loop processing (for, while)
- Practical automation scripts
What is Shell Script? Why Learn It?
The Birth of Shell Scripts
The history of shell scripts dates back to 1971 with the Thompson shell (sh). In 1979, Stephen Bourne developed the Bourne Shell (sh), which became the foundation of modern shell scripting.
In 1989, Brian Fox developed Bash (Bourne Again Shell) as part of the GNU project. Bash is currently the most widely used shell.
“Shell scripts are the easiest way to automate repetitive tasks” - Ancient Unix wisdom
Why Learn Shell Scripts
- Automation: Automate repetitive tasks and save time
- System Administration: Essential for server management, deployment, backups
- CI/CD: Heavily used in GitHub Actions, Jenkins, etc.
- Portability: Works on almost all Unix-like systems
Bash and Other Shells
| Shell | Characteristics | Main Uses |
|---|---|---|
| bash | Most widespread. POSIX compatible + extensions | General scripts |
| sh | POSIX standard. Minimal features | Portability-focused scripts |
| zsh | bash compatible + powerful completion | Interactive shell |
| fish | User-friendly | Interactive shell |
| dash | Lightweight, fast | System scripts |
Best Practice: Use
#!/bin/shfor portability, use#!/bin/bashwhen using Bash-specific features.
Your First Shell Script
Let’s start with “Hello World”.
hello.sh
#!/bin/bash
# This is my first shell script
echo "Hello, World!"
How to Run
# Grant execute permission
chmod +x hello.sh
# Execute
./hello.sh
# Or, explicitly specify bash
bash hello.sh
About Shebang
The first line #!/bin/bash is called the shebang, specifying which interpreter to use for this script.
#!/bin/bash # Execute with bash
#!/bin/sh # Execute with POSIX sh (more portable)
#!/usr/bin/env bash # Find bash in PATH (recommended)
Official Documentation: GNU Bash Manual
Using Variables
Basic Variables
#!/bin/bash
# Variable definition (no spaces around =!)
name="John"
age=25
# Variable reference
echo "Name: $name"
echo "Age: ${age} years old"
# Assign command output to variable
current_date=$(date +%Y-%m-%d)
echo "Today's date: $current_date"
# Old style (backticks) - not recommended
old_style=`date +%Y-%m-%d`
Common Mistake: Spaces
# Wrong (spaces cause errors)
name = "John" # command not found: name
name= "John" # Runs "John" as command with empty variable
# Correct
name="John"
Variable Scope
#!/bin/bash
# Global variable
global_var="I am global"
function example() {
# Local variable (only valid within function)
local local_var="I am local"
echo "$local_var"
echo "$global_var"
}
example
echo "$local_var" # Empty (not visible outside function)
Special Variables
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Number of arguments: $#"
echo "All arguments: $@"
echo "All arguments (string): $*"
echo "Exit status of last command: $?"
echo "Current process ID: $$"
echo "Background process PID: $!"
Execution example:
$ ./script.sh arg1 arg2 arg3
Script name: ./script.sh
First argument: arg1
Second argument: arg2
Number of arguments: 3
All arguments: arg1 arg2 arg3
Environment Variables
# Reference environment variables
echo "Home directory: $HOME"
echo "Username: $USER"
echo "Current directory: $PWD"
echo "Shell: $SHELL"
echo "Path: $PATH"
# Set environment variable (inherited by subprocesses)
export MY_VAR="some value"
# Temporarily set environment variable for command
DEBUG=true ./my_script.sh
Quote Differences
name="World"
# Double quotes: variables are expanded
echo "Hello, $name" # Hello, World
# Single quotes: output as-is
echo 'Hello, $name' # Hello, $name
# Escape with backslash
echo "Hello, \$name" # Hello, $name
Conditional Branching (if statements)
Basic Syntax
#!/bin/bash
age=20
if [ $age -ge 20 ]; then
echo "Adult"
elif [ $age -ge 13 ]; then
echo "Teenager"
else
echo "Child"
fi
Types of Test Syntax
# [ ] - Classic test syntax (POSIX compatible)
if [ $a -eq $b ]; then
# [[ ]] - Bash extended syntax (recommended)
if [[ $a == $b ]]; then
# (( )) - Arithmetic evaluation
if (( a > b )); then
Best Practice: Use
[[ ]]when using Bash. It supports pattern matching and regular expressions.
Numeric Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
-eq | equal | [ $a -eq $b ] |
-ne | not equal | [ $a -ne $b ] |
-lt | less than | [ $a -lt $b ] |
-le | less or equal | [ $a -le $b ] |
-gt | greater than | [ $a -gt $b ] |
-ge | greater or equal | [ $a -ge $b ] |
String Comparison
#!/bin/bash
str="hello"
# String comparison (quoting is important!)
if [ "$str" = "hello" ]; then
echo "Strings match"
fi
# Check for empty string
if [ -z "$str" ]; then
echo "String is empty"
fi
# Check if not empty
if [ -n "$str" ]; then
echo "String is not empty"
fi
# Pattern matching with [[ ]]
if [[ "$str" == h* ]]; then
echo "Starts with h"
fi
# Regex matching (Bash 3.0+)
if [[ "$str" =~ ^[a-z]+$ ]]; then
echo "Lowercase only"
fi
File/Directory Tests
#!/bin/bash
# File existence check
if [ -f "config.txt" ]; then
echo "config.txt exists"
fi
# Directory existence check
if [ -d "logs" ]; then
echo "logs directory exists"
fi
# File or directory exists
if [ -e "path" ]; then
echo "path exists"
fi
# Readable
if [ -r "file.txt" ]; then
echo "Readable"
fi
# Writable
if [ -w "file.txt" ]; then
echo "Writable"
fi
# Executable
if [ -x "script.sh" ]; then
echo "Executable"
fi
# File is not empty
if [ -s "file.txt" ]; then
echo "File is not empty"
fi
Logical Operators
# AND
if [ $a -gt 0 ] && [ $a -lt 10 ]; then
echo "0 < a < 10"
fi
# OR
if [ $a -eq 0 ] || [ $a -eq 1 ]; then
echo "a is 0 or 1"
fi
# NOT
if [ ! -f "file.txt" ]; then
echo "file.txt does not exist"
fi
# [[ ]] allows && and || inside
if [[ $a -gt 0 && $a -lt 10 ]]; then
echo "0 < a < 10"
fi
case Statement
case statements are convenient for multiple conditions:
#!/bin/bash
fruit="apple"
case $fruit in
apple)
echo "It's an apple"
;;
banana|orange)
echo "It's a banana or orange"
;;
*)
echo "Unknown fruit"
;;
esac
Loop Processing
for Loop
#!/bin/bash
# Process list sequentially
for fruit in apple banana orange; do
echo "Fruit: $fruit"
done
# Numeric range (Bash extension)
for i in {1..5}; do
echo "Count: $i"
done
# With step
for i in {0..10..2}; do
echo "Even: $i"
done
# C-style for
for ((i=0; i<5; i++)); do
echo "Index: $i"
done
# Process files
for file in *.txt; do
echo "File: $file"
done
# Process command output
for user in $(cat users.txt); do
echo "User: $user"
done
while Loop
#!/bin/bash
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
# Read file line by line (recommended pattern)
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
# Infinite loop
while true; do
echo "Press Ctrl+C to stop"
sleep 1
done
until Loop
#!/bin/bash
count=1
until [ $count -gt 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
Loop Control
# break to exit loop
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo $i
done
# continue to next iteration
for i in {1..5}; do
if [ $i -eq 3 ]; then
continue
fi
echo $i
done
Function Definition
#!/bin/bash
# Function definition (two ways)
function greet() {
local name=$1
echo "Hello, ${name}!"
}
# Or
greet2() {
echo "Hello, $1!"
}
# Function calls
greet "John"
greet2 "World"
# Return values (exit status)
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # Success (even)
else
return 1 # Failure (odd)
fi
}
if is_even 4; then
echo "4 is even"
fi
# Use echo to return values
add() {
echo $(( $1 + $2 ))
}
result=$(add 3 5)
echo "3 + 5 = $result"
Practical: Backup Script
Let’s create a practical backup script using what we’ve learned.
backup.sh
#!/bin/bash
#
# Automatic backup script
# Usage: ./backup.sh [source_dir] [backup_dir]
#
set -euo pipefail # Exit immediately on error
# Configuration
SOURCE_DIR="${1:-./src}"
BACKUP_DIR="${2:-./backups}"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$DATE.tar.gz"
RETENTION_DAYS=7
# Log function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Check/create backup directory
if [ ! -d "$BACKUP_DIR" ]; then
log "Creating backup directory..."
mkdir -p "$BACKUP_DIR"
fi
# Check source directory
if [ ! -d "$SOURCE_DIR" ]; then
log "Error: $SOURCE_DIR not found"
exit 1
fi
# Execute backup
log "Starting backup: $SOURCE_DIR -> $BACKUP_DIR/$BACKUP_NAME"
tar -czf "$BACKUP_DIR/$BACKUP_NAME" "$SOURCE_DIR"
if [ $? -eq 0 ]; then
log "Complete: $BACKUP_DIR/$BACKUP_NAME"
log "Size: $(du -h "$BACKUP_DIR/$BACKUP_NAME" | cut -f1)"
else
log "Error: Backup failed"
exit 1
fi
# Delete old backups
log "Deleting old backups (${RETENTION_DAYS}+ days old)..."
deleted=$(find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "Files deleted: $deleted"
log "All done!"
Script Best Practices
-
Use
set -euo pipefail-e: Exit immediately on error-u: Error on undefined variables-o pipefail: Detect errors in pipes
-
Always quote variables:
"$variable" -
Use functions: Improve reusability and readability
-
Include log output: For debugging and monitoring
Error Handling
#!/bin/bash
# Define error handling with trap
cleanup() {
echo "Running cleanup..."
# Delete temporary files, etc.
}
trap cleanup EXIT # Execute on script exit
trap 'echo "Error occurred: line $LINENO"' ERR
# To ignore errors
command_that_might_fail || true
# Alternative processing on error
config_file="config.txt"
if [ -f "$config_file" ]; then
source "$config_file"
else
echo "Config file not found. Using defaults."
default_value="fallback"
fi
Debugging Tips
# Run entire script in debug mode
bash -x script.sh
# Enable debugging within script
set -x # Start debugging
# ... code to debug ...
set +x # End debugging
# Exit immediately on error (recommended)
set -e
# Error on undefined variables (recommended)
set -u
# Detect pipe errors (recommended)
set -o pipefail
# Common combination
set -euo pipefail
Static Analysis with ShellCheck
ShellCheck is a static analysis tool that detects problems in shell scripts.
# Installation
# macOS
brew install shellcheck
# Ubuntu/Debian
sudo apt install shellcheck
# Usage
shellcheck script.sh
Example problems ShellCheck detects:
# Warning: Missing variable quotes
echo $USER # SC2086: Double quote to prevent globbing
# Fixed
echo "$USER"
ShellCheck usage is also recommended in the Google Shell Style Guide.
Common Patterns
Argument Validation
#!/bin/bash
if [ $# -lt 2 ]; then
echo "Usage: $0 <source> <destination>"
exit 1
fi
source=$1
destination=$2
Setting Default Values
# Use default if variable is unset or empty
name="${1:-World}"
echo "Hello, $name"
# Use default only if variable is unset
name="${1-World}"
Safe Temporary File Creation
#!/bin/bash
# Use mktemp (recommended)
temp_file=$(mktemp)
temp_dir=$(mktemp -d)
# Delete on exit
trap "rm -rf $temp_file $temp_dir" EXIT
echo "Temp file: $temp_file"
echo "Temp directory: $temp_dir"
Getting User Input
#!/bin/bash
# Read input
read -p "Enter your name: " name
echo "Hello, $name"
# Password input (hidden)
read -sp "Password: " password
echo ""
# With timeout
read -t 5 -p "Enter within 5 seconds: " input || echo "Timeout"
Next Steps
After mastering shell script basics, proceed to the next steps:
- Regular expressions with
grep,sed,awk - Scheduled execution with cron jobs
- More advanced automation (Ansible, Makefile, etc.)
Reference Links
Official Documentation
- GNU Bash Manual - Bash official reference
- POSIX Shell Command Language - POSIX standard
Style Guides & Best Practices
- Google Shell Style Guide - Google’s shell script style guide
- ShellCheck - Shell script static analysis tool
Learning Resources
- Bash Hackers Wiki - Advanced Bash techniques
- explainshell.com - Explains commands you enter
- Pure Bash Bible - Recipes implemented in pure Bash
Cheat Sheets
- Bash Scripting Cheat Sheet - Common syntax reference
- Advanced Bash-Scripting Guide - Detailed Bash guide