The Ultimate Bash Scripting Tutorial: From Beginner to Advanced


Bash scripting is a powerful tool for automating tasks, managing systems, and boosting your productivity as a developer. Whether you're a command-line newbie or a seasoned coder, this guide will take you from Bash basics to advanced scripting with practical examples, tips, and resources.
What is Bash?
Bash (Bourne Again SHell) is a Unix shell and command language. It's the default shell on:
Linux distributions
macOS (until Catalina, now Zsh)
Windows Subsystem for Linux (WSL)
Most Unix-like systems
Why Learn Bash Scripting?
๐ Automation: Save hours by scripting repetitive tasks
๐ System Administration: Essential for DevOps and sysadmins
๐ป Developer Productivity: Combine tools and process data quickly
๐ Data Processing: Powerful text manipulation capabilities
Getting Started
Your First Bash Script
Create a new file:
touch hello.sh
Add the following content:
#!/bin/bash # This is a comment echo "Hello, World!"
Make it executable:
chmod +x hello.sh
Run it:
./hello.sh
Key Components:
#!/bin/bash (Shebang line) tells the system to use Bash
# indicates comments
echo prints text to the terminal
Pro Tip: Always include the shebang line for portability, even though Bash is often the default.
Variables and Parameters
Basic Variables
name="Mohammad" # No spaces around =
age=10
greeting="Good morning, $name!"
Accessing Variables:
echo "$name is $age years old." # Mohammad is 10 years old.
echo "Message: $greeting" # Good morning, Mohammad!
Command Substitution
Store command output in a variable:
current_date=$(date)
echo "Today is $current_date"
# Alternative syntax
files=`ls`
echo "Files: $files"
Positional Parameters
Access command-line arguments:
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
Run with:
./script.sh arg1 arg2 arg3
Special Variables
Variable | Description |
$0 | Script name |
$1-$9 | Positional arguments |
$# | Number of arguments |
$@ | All arguments as separate strings |
$* | All arguments as single string |
$? | Exit status of last command |
$$ | Process ID (PID) of script |
$! | PID of last background command |
Using getopts for Flags
Handle command-line options professionally:
#!/bin/bash
while getopts "f:d:v" opt; do
case $opt in
f) file="$OPTARG" ;;
d) dir="$OPTARG" ;;
v) verbose=true ;;
\?) echo "Invalid option: -$OPTARG" >&2
exit 1 ;;
esac
done
echo "File: ${file:-not specified}"
echo "Directory: ${dir:-not specified}"
echo "Verbose: ${verbose:-false}"
Usage:
./script.sh -f data.txt -d /home/user -v
Best Practices:
Always quote variables: "$var"
Use ${var:-default} for default values
Prefer $@ over $* to preserve argument separation
Control Structures
Conditionals (if/elif/else)
Basic Syntax:
if [[ condition ]]; then
# commands
elif [[ another_condition ]]; then
# commands
else
# commands
fi
Common Test Operators:
Operator | Description | Example |
-eq | Equal | [[ "$a" -eq "$b" ]] |
-ne | Not equal | [[ "$a" -ne "$b" ]] |
-lt | Less than | [[ "$a" -lt "$b" ]] |
-gt | Greater than | [[ "$a" -gt "$b" ]] |
-z | String is empty | [[ -z "$str" ]] |
-n | String is not empty | [[ -n "$str" ]] |
\== | String equality | [[ "$str1" == "$str2" ]] |
\=~ | Regex match | [[ "$str" =~ ^[0-9]+$ ]] |
-e | File exists | [[ -e "file.txt" ]] |
-d | Directory exists | [[ -d "/path" ]] |
Example:
#!/bin/bash
read -p "Enter temperature (ยฐC): " temp
if [[ "$temp" -gt 30 ]]; then
echo "It's hot outside! ๐ฅต"
elif [[ "$temp" -lt 10 ]]; then
echo "It's cold outside! โ๏ธ"
else
echo "It's pleasant weather. ๐"
fi
Case Statements
#!/bin/bash
read -p "Enter your favorite fruit: " fruit
case "$fruit" in
apple)
echo "Apples are crunchy!"
;;
banana)
echo "Bananas are rich in potassium!"
;;
orange|lemon)
echo "Citrus fruits are vitamin C rich!"
;;
*)
echo "I don't know about $fruit"
;;
esac
Loops
For Loop:
# Basic for loop
for i in {1..5}; do
echo "Number: $i"
done
# C-style for loop
for ((i=0; i<5; i++)); do
echo "Count: $i"
done
# Iterate over files
for file in *.txt; do
echo "Processing $file"
done
While Loop:
count=1
while [[ $count -le 5 ]]; do
echo "Count: $count"
((count++))
done
# Reading file line by line
while IFS= read -r line; do
echo "Line: $line"
done < "file.txt"
Until Loop:
attempt=1
until ping -c1 example.com &>/dev/null; do
echo "Attempt $attempt: Waiting for connection..."
((attempt++))
sleep 1
done
echo "Connection established!"
Loop Control:
break: Exit the loop
continue: Skip to next iteration
for i in {1..10}; do
if [[ $i -eq 5 ]]; then
continue
fi
if [[ $i -eq 8 ]]; then
break
fi
echo "Number: $i"
done
Functions
Basic Function
greet() {
local name="$1" # Local variable
echo "Hello, $name!"
}
greet "Alice" # Output: Hello, Alice!
greet "Bob" # Output: Hello, Bob!
Returning Values
add() {
local sum=$(( $1 + $2 ))
echo "$sum" # Output the result
}
result=$(add 3 5)
echo "3 + 5 = $result" # 3 + 5 = 8
Function Parameters
create_user() {
local username="$1"
local shell="${2:-/bin/bash}" # Default value
echo "Creating user $username with shell $shell"
# useradd "$username" -s "$shell"
}
create_user "Mohammad"
create_user "Aman" "/bin/zsh"
Best Practices:
Use local for function variables to avoid side effects
Return status codes (0 for success) with return
Use echo to "return" data
Document functions with comments
Error Handling
Exit Codes
Every command returns an exit code (0 = success, non-zero = error):
mkdir /non_existent_dir
if [[ $? -ne 0 ]]; then
echo "Failed to create directory!" >&2
exit 1
fi
The set Command
Control script behavior:
set -e # Exit immediately if a command fails
set -u # Treat unset variables as an error
set -o pipefail # Fail if any command in a pipeline fails
set -x # Print commands before execution (debugging)
Trapping Signals
cleanup() {
echo "Cleaning up..."
rm -f tempfile.txt
}
trap cleanup EXIT INT TERM # Run on exit or interrupt
Logging
log() {
local message="$1"
local level="${2:-INFO}"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" >> script.log
}
log "Starting script execution"
log "Error encountered" "ERROR"
Advanced Topics
Arrays
# Indexed arrays
fruits=("apple" "banana" "cherry")
echo ${fruits[0]} # apple
echo ${fruits[@]} # all elements
echo ${#fruits[@]} # array length
# Adding elements
fruits+=("date")
# Associative arrays (Bash 4+)
declare -A user
user["name"]="Sakir"
user["age"]=20
echo "${user["name"]} is ${user["age"]} years old."
String Manipulation
str="Hello World"
# Substrings
echo ${str:0:5} # Hello
echo ${str:6} # World
# Replacement
echo ${str/World/Everyone} # Hello Everyone
echo ${str//l/L} # HeLLo WorLd (global replacement)
# Case conversion
echo ${str^^} # HELLO WORLD
echo ${str,,} # hello world
Arithmetic Operations
a=5
b=3
# Using $(( ))
echo $((a + b)) # 8
echo $((a * b)) # 15
echo $((a++)) # 5 (then a becomes 6)
# Using let
let "sum = a + b"
echo $sum # 9 (if a is now 6)
# Using expr (older syntax)
echo $(expr $a + $b)
Process Substitution
# Compare outputs
diff <(ls dir1) <(ls dir2)
# Process multiple files
paste <(cut -f1 file1) <(cut -f2 file2) > output.txt
Here Documents
cat <<EOF
This is a multi-line
text block that preserves
formatting and variables like $HOME
EOF
# With variable expansion disabled
cat <<'EOF'
No variable expansion here $HOME
EOF
Performance Tips
Avoid unnecessary subshells:
# Slow count=$(ls | wc -l) # Faster files=(*) count=${#files[@]}
Use built-in string operations instead of external commands:
# Instead of: uppercase=$(echo "$str" | tr '[:lower:]' '[:upper:]') # Use: uppercase="${str^^}"
Batch file operations:
# Instead of: for file in *.txt; do process "$file" done # Consider: find . -name "*.txt" -exec process {} \;
Use printf instead of echo for complex output:
printf "Name: %-20s Age: %3d\n" "$name" "$age"
Security Best Practices
Always quote variables:
rm "$file" # Safe rm $file # Dangerous (filename with spaces becomes multiple arguments)
Validate input:
if [[ ! "$input" =~ ^[a-zA-Z0-9_]+$ ]]; then echo "Invalid input" >&2 exit 1 fi
Use mktemp for temporary files:
tempfile=$(mktemp /tmp/script.XXXXXX) echo "Data" > "$tempfile"
Avoid eval - it can execute arbitrary code:
# Dangerous! eval "$user_input"
Run with least privileges:
# Use sudo only when necessary sudo chown root:root /important/file
Real-World Script Examples
System Backup Script
#!/bin/bash
# backup.sh - Automated system backup
set -euo pipefail
BACKUP_DIR="/backups/$(date +%Y-%m-%d_%H-%M-%S)"
LOG_FILE="/var/log/backup.log"
mkdir -p "$BACKUP_DIR"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Starting backup to $BACKUP_DIR"
# Backup home directories
for user in /home/*; do
if [[ -d "$user" ]]; then
username=$(basename "$user")
log "Backing up $username"
tar -czf "$BACKUP_DIR/$username.tar.gz" "$user"
fi
done
# Backup system configurations
log "Backing up system configurations"
tar -czf "$BACKUP_DIR/etc.tar.gz" /etc
log "Backup completed successfully"
Log Analyzer
#!/bin/bash
# log_analyzer.sh - Analyze log files
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <logfile> [error_level]"
exit 1
fi
LOG_FILE="$1"
ERROR_LEVEL="${2:-ERROR}"
if [[ ! -f "$LOG_FILE" ]]; then
echo "Error: File $LOG_FILE not found" >&2
exit 1
fi
echo "Analyzing $LOG_FILE for $ERROR_LEVEL messages"
declare -A error_counts
while IFS= read -r line; do
if [[ "$line" =~ \[([^]]+)\] ]]; then
timestamp="${BASH_REMATCH[1]}"
date_part="${timestamp%% *}"
if [[ "$line" =~ $ERROR_LEVEL ]]; then
((error_counts["$date_part"]++))
fi
fi
done < "$LOG_FILE"
echo -e "\nError Count by Date:"
for date in "${!error_counts[@]}"; do
printf "%s: %d\n" "$date" "${error_counts[$date]}"
done | sort
Cheat Sheet
Variable Manipulation
Syntax | Description |
${var} | Variable value |
${var:-default} | Use default if var is unset |
${var:=default} | Set default if var is unset |
${#var} | Length of string |
${var:position:length} | Substring |
${var/pattern/repl} | Replace first match |
${var//pattern/repl} | Replace all matches |
Test Operators
Operator | Description |
-e | File exists |
-f | Regular file |
-d | Directory |
-r | Readable |
-w | Writable |
-x | Executable |
-z | String is empty |
-n | String is not empty |
Arithmetic
Operator | Description |
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Modulus |
** | Exponentiation |
Resources
Documentation
Tools
Books
Communities
Conclusion
Bash scripting is a must-have skill for developers and system administrators. Start with simple automation tasks and gradually incorporate advanced features as you gain confidence. Key takeaways:
Test thoroughly: Especially for scripts that modify files or systems
Add comments: Document your code for future you
Follow conventions: Make your scripts readable and maintainable
Share your work: Contribute to open source or publish your utilities
Next Steps:
Explore awk and sed for advanced text processing
Learn about cron for scheduling scripts
Dive into shell scripting frameworks like Bash Infinity
Happy scripting! ๐๐ป
Discussion Prompt: What's your favorite Bash scripting trick? Share in the comments below! ๐
Subscribe to my newsletter
Read articles from Mohammad Aman directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mohammad Aman
Mohammad Aman
Full-stack developer with a good foundation in frontend, now specializing in backend development. Passionate about building efficient, scalable systems and continuously sharpening my problem-solving skills. Always learning, always evolving.