Introduction to Bash Scripting
Bash (Bourne Again Shell) is the default shell on most Linux distributions and an essential skill for anyone working with Unix-like systems. This comprehensive guide takes you from basic concepts to advanced automation techniques, equipping you to write robust, maintainable scripts.
Your First Bash Script
Every Bash script starts with a shebang and should be made executable:
#!/bin/bash
# This is a comment
echo "Hello, World!"
# Make it executable
chmod +x script.sh
./script.sh
The shebang (#!/bin/bash) tells the system which interpreter to use. Always include it for portability.
Variables and Data Types
Bash variables are untyped and do not require declaration:
#!/bin/bash
# Variable assignment (no spaces around =)
name="Linux Admin"
count=42
today=$(date +%Y-%m-%d)
# Variable usage
echo "Hello, $name"
echo "Today is $today"
echo "Count: ${count}"
# Read-only variables
readonly VERSION="1.0.0"
# Environment variables
export PATH="$PATH:/opt/scripts"
# Special variables
echo "Script name: $0"
echo "First argument: $1"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Exit status of last command: $?"
echo "Process ID: $$"
Conditionals and Control Flow
Bash provides flexible conditional statements:
#!/bin/bash
# If-else statement
if [ -f "/etc/passwd" ]; then
echo "Password file exists"
elif [ -d "/etc" ]; then
echo "At least /etc exists"
else
echo "Something is very wrong"
fi
# Using [[ ]] for advanced tests (recommended)
file="/var/log/syslog"
if [[ -f "$file" && -r "$file" ]]; then
echo "File exists and is readable"
fi
# String comparisons
if [[ "$USER" == "root" ]]; then
echo "Running as root"
fi
# Numeric comparisons
count=10
if [[ $count -gt 5 ]]; then
echo "Count is greater than 5"
fi
# Case statement
case "$1" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
Loops and Iteration
Iterate over data with various loop constructs:
#!/bin/bash
# For loop with list
for server in web1 web2 db1 db2; do
echo "Checking $server..."
ping -c 1 "$server" > /dev/null && echo " UP" || echo " DOWN"
done
# For loop with range
for i in {1..10}; do
echo "Iteration $i"
done
# C-style for loop
for ((i=0; i<5; i++)); do
echo "Index: $i"
done
# Loop over files
for file in /var/log/*.log; do
echo "Log file: $(basename "$file")"
done
# While loop
counter=0
while [[ $counter -lt 5 ]]; do
echo "Counter: $counter"
((counter++))
done
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hosts
# Until loop
until ping -c 1 google.com > /dev/null 2>&1; do
echo "Waiting for network..."
sleep 5
done
echo "Network is up!"
Functions
Organize code into reusable functions:
#!/bin/bash
# Function definition
greet() {
local name="$1" # Local variable
echo "Hello, $name!"
}
# Function with return value
is_root() {
[[ $EUID -eq 0 ]]
}
# Function with output capture
get_ip() {
hostname -I | awk '{print $1}'
}
# Usage
greet "World"
if is_root; then
echo "Running as root"
else
echo "Running as regular user"
fi
ip_address=$(get_ip)
echo "IP Address: $ip_address"
# Function with multiple parameters
log_message() {
local level="$1"
local message="$2"
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $message"
}
log_message "INFO" "Script started"
log_message "ERROR" "Something went wrong"
Error Handling
Robust scripts handle errors gracefully:
#!/bin/bash
# Exit on error
set -e
# Exit on undefined variable
set -u
# Pipe failure handling
set -o pipefail
# Combined: set -euo pipefail
# Trap for cleanup
cleanup() {
echo "Cleaning up..."
rm -f /tmp/script_temp_*
}
trap cleanup EXIT
# Error handling function
die() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
# Usage with error checking
config_file="/etc/myapp/config"
[[ -f "$config_file" ]] || die "Config file not found: $config_file" 2
# Try-catch pattern
if ! output=$(some_command 2>&1); then
echo "Command failed: $output" >&2
exit 1
fi
Working with Input and Output
Handle user input and file operations:
#!/bin/bash
# Read user input
read -p "Enter your name: " username
echo "Hello, $username"
# Read with timeout
if read -t 10 -p "Quick! Enter password: " -s password; then
echo -e "\nPassword received"
else
echo -e "\nTimeout!"
fi
# Redirect output
echo "Log entry" >> /var/log/myapp.log
# Redirect stderr
command 2> errors.log
# Redirect both stdout and stderr
command &> all_output.log
# Here document
cat < /tmp/config
server=localhost
port=8080
debug=true
EOF
# Process substitution
diff <(ls /dir1) <(ls /dir2)
Arrays
Work with indexed and associative arrays:
#!/bin/bash
# Indexed array
servers=("web1" "web2" "db1" "db2")
echo "First server: ${servers[0]}"
echo "All servers: ${servers[@]}"
echo "Number of servers: ${#servers[@]}"
# Add element
servers+=("cache1")
# Loop over array
for server in "${servers[@]}"; do
echo "Server: $server"
done
# Associative array (Bash 4+)
declare -A config
config[host]="localhost"
config[port]="3306"
config[user]="admin"
echo "Database host: ${config[host]}"
# Loop over associative array
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
Practical Script: Log Rotation
A complete example for log file management:
#!/bin/bash
set -euo pipefail
# Configuration
LOG_DIR="/var/log/myapp"
MAX_SIZE=$((10 * 1024 * 1024)) # 10MB
KEEP_DAYS=30
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
}
rotate_log() {
local file="$1"
local timestamp=$(date +'%Y%m%d_%H%M%S')
local rotated="${file}.${timestamp}"
mv "$file" "$rotated"
gzip "$rotated"
touch "$file"
log "Rotated: $(basename "$file")"
}
cleanup_old() {
find "$LOG_DIR" -name "*.gz" -mtime +$KEEP_DAYS -delete
log "Cleaned up logs older than $KEEP_DAYS days"
}
main() {
log "Starting log rotation"
for logfile in "$LOG_DIR"/*.log; do
[[ -f "$logfile" ]] || continue
size=$(stat -f%z "$logfile" 2>/dev/null || stat -c%s "$logfile")
if [[ $size -gt $MAX_SIZE ]]; then
rotate_log "$logfile"
fi
done
cleanup_old
log "Log rotation complete"
}
main "$@"
Best Practices
- Always quote variables: Use
"$variable"to prevent word splitting - Use
[[ ]]over[ ]: More features and safer syntax - Enable strict mode:
set -euo pipefailat script start - Use functions: Break complex scripts into manageable pieces
- Add help messages: Include
--helpoption for usability - Use ShellCheck: Lint your scripts for common mistakes
Conclusion
Bash scripting is a powerful skill that enables automation of virtually any task on Linux systems. From simple one-liners to complex automation frameworks, mastering Bash opens up endless possibilities for system administration and DevOps work. Practice regularly, read other scripts, and always strive for clean, maintainable code.
Want to take your shell scripting to the next level? Browse our collection of Linux and Bash programming books for in-depth learning resources.