Bash scripts automate everything from deployments to backups. Here’s how to write them properly.
Script Structure#
1
2
3
4
| #!/bin/bash
set -euo pipefail
# Your code here
|
The shebang (#!/bin/bash) tells the system which interpreter to use. The set options make scripts safer:
-e: Exit on error-u: Error on undefined variables-o pipefail: Catch errors in pipelines
Variables#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Assignment (no spaces!)
NAME="Alice"
COUNT=42
# Usage
echo "Hello, $NAME"
echo "Count is ${COUNT}"
# Command substitution
DATE=$(date +%Y-%m-%d)
FILES=$(ls -1 | wc -l)
# Default values
ENVIRONMENT=${1:-production} # First arg, default "production"
LOG_LEVEL=${LOG_LEVEL:-info} # Env var with default
|
Variable Scope#
1
2
3
4
5
6
7
8
| # Global by default
GLOBAL="I'm everywhere"
# Local to function
my_function() {
local LOCAL="I'm only here"
echo "$LOCAL"
}
|
Conditionals#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # String comparison
if [ "$NAME" = "Alice" ]; then
echo "Hi Alice"
elif [ "$NAME" = "Bob" ]; then
echo "Hi Bob"
else
echo "Who are you?"
fi
# Numeric comparison
if [ "$COUNT" -gt 10 ]; then
echo "More than 10"
fi
# File tests
if [ -f "$FILE" ]; then
echo "File exists"
fi
if [ -d "$DIR" ]; then
echo "Directory exists"
fi
if [ -z "$VAR" ]; then
echo "Variable is empty"
fi
if [ -n "$VAR" ]; then
echo "Variable is not empty"
fi
|
Comparison Operators#
| String | Numeric | Meaning |
|---|
= | -eq | Equal |
!= | -ne | Not equal |
| -lt | Less than |
| -le | Less or equal |
| -gt | Greater than |
| -ge | Greater or equal |
Modern Syntax#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # Double brackets (safer, more features)
if [[ "$NAME" == "Alice" ]]; then
echo "Hi"
fi
# Pattern matching
if [[ "$FILE" == *.txt ]]; then
echo "Text file"
fi
# Regex
if [[ "$EMAIL" =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]]; then
echo "Valid email"
fi
# Logical operators
if [[ "$A" == "yes" && "$B" == "yes" ]]; then
echo "Both yes"
fi
if [[ "$A" == "yes" || "$B" == "yes" ]]; then
echo "At least one yes"
fi
|
Loops#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # For loop over list
for NAME in Alice Bob Charlie; do
echo "Hello, $NAME"
done
# For loop over files
for FILE in *.txt; do
echo "Processing $FILE"
done
# For loop with range
for i in {1..10}; do
echo "Number $i"
done
# C-style for loop
for ((i=0; i<10; i++)); do
echo "Index $i"
done
# While loop
while [ "$COUNT" -gt 0 ]; do
echo "$COUNT"
COUNT=$((COUNT - 1))
done
# Read lines from file
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
# Read from command
while read -r file; do
echo "Found: $file"
done < <(find . -name "*.log")
|
Functions#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # Basic function
greet() {
echo "Hello, $1"
}
greet "World"
# With local variables
calculate() {
local a=$1
local b=$2
echo $((a + b))
}
result=$(calculate 5 3)
echo "Result: $result"
# Return values (0 = success, non-zero = failure)
is_valid() {
if [[ "$1" =~ ^[0-9]+$ ]]; then
return 0
else
return 1
fi
}
if is_valid "42"; then
echo "Valid number"
fi
|
Arguments#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #!/bin/bash
# Positional arguments
echo "Script: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "All args: $@"
echo "Arg count: $#"
# Shift through args
while [ $# -gt 0 ]; do
echo "Arg: $1"
shift
done
# Getopts for flags
while getopts "vf:o:" opt; do
case $opt in
v) VERBOSE=true ;;
f) FILE="$OPTARG" ;;
o) OUTPUT="$OPTARG" ;;
?) echo "Usage: $0 [-v] [-f file] [-o output]"; exit 1 ;;
esac
done
|
Error Handling#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #!/bin/bash
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO"; exit 1' ERR
# Trap cleanup on exit
cleanup() {
echo "Cleaning up..."
rm -f "$TEMP_FILE"
}
trap cleanup EXIT
# Create temp file
TEMP_FILE=$(mktemp)
# Check command success
if ! command -v docker &> /dev/null; then
echo "Docker not installed"
exit 1
fi
# Or with ||
docker ps || { echo "Docker not running"; exit 1; }
|
Useful Patterns#
Check if root#
1
2
3
4
| if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
|
Confirm before proceeding#
1
2
3
4
5
| read -p "Are you sure? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
|
Logging#
1
2
3
4
5
6
7
8
| LOG_FILE="/var/log/myscript.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Starting script"
log "Task completed"
|
Config file#
1
2
3
4
5
6
7
8
| # config.sh
DB_HOST="localhost"
DB_PORT=5432
DB_NAME="myapp"
# main.sh
source ./config.sh
echo "Connecting to $DB_HOST:$DB_PORT"
|
Lock file (prevent concurrent runs)#
1
2
3
4
5
6
7
8
9
10
11
| LOCKFILE="/tmp/myscript.lock"
if [ -f "$LOCKFILE" ]; then
echo "Script already running"
exit 1
fi
trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"
# Your script here
|
Arrays#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # Define array
SERVERS=("web1" "web2" "web3")
# Access elements
echo "${SERVERS[0]}" # First element
echo "${SERVERS[@]}" # All elements
echo "${#SERVERS[@]}" # Length
# Loop over array
for server in "${SERVERS[@]}"; do
echo "Deploying to $server"
done
# Add element
SERVERS+=("web4")
# Associative array (bash 4+)
declare -A PORTS
PORTS[web]=80
PORTS[api]=8080
echo "${PORTS[web]}"
|
String Operations#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| STR="Hello, World!"
# Length
echo "${#STR}" # 13
# Substring
echo "${STR:0:5}" # Hello
# Replace
echo "${STR/World/Bash}" # Hello, Bash!
# Replace all
echo "${STR//o/0}" # Hell0, W0rld!
# Remove prefix
FILE="document.txt"
echo "${FILE%.txt}" # document
# Remove suffix
PATH="/home/user/file.txt"
echo "${PATH##*/}" # file.txt
echo "${PATH%/*}" # /home/user
|
Real Script Example#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| #!/bin/bash
set -euo pipefail
# Deploy script with logging and error handling
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="$SCRIPT_DIR/deploy.log"
ENVIRONMENT="${1:-staging}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$ENVIRONMENT] $1" | tee -a "$LOG_FILE"
}
die() {
log "ERROR: $1"
exit 1
}
cleanup() {
log "Cleanup complete"
}
trap cleanup EXIT
# Validate environment
if [[ ! "$ENVIRONMENT" =~ ^(staging|production)$ ]]; then
die "Invalid environment: $ENVIRONMENT"
fi
# Production confirmation
if [ "$ENVIRONMENT" = "production" ]; then
read -p "Deploy to PRODUCTION? (yes/no) " confirm
[ "$confirm" = "yes" ] || die "Aborted"
fi
log "Starting deployment to $ENVIRONMENT"
# Pull latest code
log "Pulling latest code..."
git pull origin main || die "Git pull failed"
# Build
log "Building..."
npm run build || die "Build failed"
# Deploy
log "Deploying..."
rsync -avz ./dist/ "deploy@$ENVIRONMENT.example.com:/var/www/" || die "Deploy failed"
log "Deployment complete!"
|
Quick Reference#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # Variables
VAR="value"
VAR=${VAR:-default}
VAR=$(command)
# Conditionals
[[ -f file ]] # File exists
[[ -d dir ]] # Dir exists
[[ -z "$var" ]] # Empty
[[ "$a" == "$b" ]] # String equal
[[ $n -eq 5 ]] # Numeric equal
# Loops
for i in list; do ...; done
while cond; do ...; done
# Functions
func() { local x=$1; echo $x; }
# Error handling
set -euo pipefail
trap 'cleanup' EXIT
command || { echo "failed"; exit 1; }
|
Bash isn’t pretty, but it’s everywhere. Master these patterns and you can automate anything.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.