Shell scripting automates repetitive tasks and ties together Linux tools. Here’s how to write effective scripts.
Script Basics
First Script
1
2
3
|
#!/bin/bash
# This is a comment
echo "Hello, World!"
|
1
2
3
4
5
6
7
|
# Make executable
chmod +x script.sh
# Run
./script.sh
# or
bash script.sh
|
Shebang
The first line tells which interpreter to use:
1
2
3
|
#!/bin/bash # Bash
#!/bin/sh # POSIX shell
#!/usr/bin/env bash # Find bash in PATH (portable)
|
Variables
Defining Variables
1
2
3
4
5
6
7
8
9
10
11
|
# No spaces around =
name="Alice"
count=42
path="/var/log"
# Command output
date_now=$(date)
files=$(ls -la)
# Arithmetic
sum=$((5 + 3))
|
Using Variables
1
2
3
|
echo "Hello, $name"
echo "Path is ${path}/file.log" # Braces for clarity
echo "Count: ${count}"
|
Special Variables
| Variable |
Meaning |
$0 |
Script name |
$1, $2, ... |
Arguments |
$# |
Number of arguments |
$@ |
All arguments (as separate strings) |
$* |
All arguments (as one string) |
$? |
Exit status of last command |
$$ |
Current process ID |
$! |
Last background process ID |
1
2
3
4
5
|
#!/bin/bash
echo "Script: $0"
echo "First arg: $1"
echo "All args: $@"
echo "Count: $# arguments"
|
Environment Variables
1
2
3
4
5
6
7
8
|
# Export for child processes
export MY_VAR="value"
# Common environment variables
echo $HOME
echo $USER
echo $PATH
echo $PWD
|
1
2
3
4
5
6
7
8
9
|
# Read into variable
read -p "Enter name: " name
echo "Hello, $name"
# Read with timeout
read -t 5 -p "Quick, enter something: " input
# Silent (for passwords)
read -s -p "Password: " password
|
Output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# Standard output
echo "Hello"
printf "Name: %s, Age: %d\n" "$name" "$age"
# Standard error
echo "Error!" >&2
# Redirect to file
echo "Log entry" >> logfile.txt
# Here document
cat << EOF
This is a
multi-line
text block
EOF
|
Conditionals
if Statements
1
2
3
4
5
6
7
|
if [ condition ]; then
commands
elif [ condition ]; then
commands
else
commands
fi
|
Test Operators
String Comparisons:
1
2
3
4
|
[ "$a" = "$b" ] # Equal
[ "$a" != "$b" ] # Not equal
[ -z "$a" ] # Empty
[ -n "$a" ] # Not empty
|
Numeric Comparisons:
1
2
3
4
5
6
|
[ "$a" -eq "$b" ] # Equal
[ "$a" -ne "$b" ] # Not equal
[ "$a" -lt "$b" ] # Less than
[ "$a" -le "$b" ] # Less or equal
[ "$a" -gt "$b" ] # Greater than
[ "$a" -ge "$b" ] # Greater or equal
|
File Tests:
1
2
3
4
5
6
7
|
[ -e "$file" ] # Exists
[ -f "$file" ] # Is regular file
[ -d "$file" ] # Is directory
[ -r "$file" ] # Is readable
[ -w "$file" ] # Is writable
[ -x "$file" ] # Is executable
[ -s "$file" ] # Size > 0
|
Logical Operators:
1
2
3
|
[ condition1 ] && [ condition2 ] # AND
[ condition1 ] || [ condition2 ] # OR
[ ! condition ] # NOT
|
Modern Syntax [[ ]]
1
2
3
4
5
6
7
8
|
# Preferred in bash - more features, fewer surprises
if [[ "$string" == *pattern* ]]; then
echo "Pattern match"
fi
if [[ "$a" -gt 5 && "$b" -lt 10 ]]; then
echo "Both conditions"
fi
|
Case Statement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
case "$option" in
start)
echo "Starting..."
;;
stop)
echo "Stopping..."
;;
restart)
echo "Restarting..."
;;
*)
echo "Unknown option"
;;
esac
|
Loops
for Loop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# List
for item in apple banana cherry; do
echo "$item"
done
# Range
for i in {1..5}; do
echo "Number: $i"
done
# C-style
for ((i=0; i<5; i++)); do
echo "Index: $i"
done
# Files
for file in *.txt; do
echo "Processing: $file"
done
# Command output
for user in $(cat users.txt); do
echo "User: $user"
done
|
while Loop
1
2
3
4
5
6
7
8
9
10
|
count=0
while [ $count -lt 5 ]; do
echo "Count: $count"
((count++))
done
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
|
until Loop
1
2
3
4
5
|
count=0
until [ $count -ge 5 ]; do
echo "Count: $count"
((count++))
done
|
Loop Control
1
2
3
4
5
6
7
8
9
10
11
|
# Skip iteration
for i in {1..10}; do
[ $i -eq 5 ] && continue
echo $i
done
# Exit loop
for i in {1..10}; do
[ $i -eq 5 ] && break
echo $i
done
|
Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# Define function
greet() {
local name="$1" # Local variable
echo "Hello, $name!"
return 0 # Return status
}
# Call function
greet "Alice"
# Capture output
result=$(greet "Bob")
echo "$result"
# Check return status
if greet "Charlie"; then
echo "Success"
fi
|
Error Handling
Exit Status
1
2
3
4
5
6
7
8
9
10
11
12
|
# Exit with status
exit 0 # Success
exit 1 # Error
# Check last command
if [ $? -ne 0 ]; then
echo "Command failed"
fi
# Chain commands
command1 && command2 # Run command2 only if command1 succeeds
command1 || command2 # Run command2 only if command1 fails
|
Set Options
1
2
3
4
5
6
7
8
9
10
|
#!/bin/bash
set -e # Exit on error
set -u # Error on undefined variables
set -o pipefail # Pipeline fails if any command fails
# Combined
set -euo pipefail
# Debug mode
set -x # Print commands before execution
|
Trap
1
2
3
4
5
6
7
8
9
|
# Cleanup on exit
cleanup() {
rm -f /tmp/tempfile
echo "Cleaned up"
}
trap cleanup EXIT
# Handle signals
trap 'echo "Interrupted!"; exit 1' INT TERM
|
Practical Examples
Backup Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#!/bin/bash
set -euo pipefail
SOURCE="/home/user/documents"
DEST="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$DEST/backup_$DATE.tar.gz"
echo "Starting backup..."
tar -czf "$BACKUP_FILE" "$SOURCE"
echo "Backup created: $BACKUP_FILE"
# Remove backups older than 7 days
find "$DEST" -name "backup_*.tar.gz" -mtime +7 -delete
echo "Old backups cleaned"
|
Log Monitoring
1
2
3
4
5
6
7
8
9
10
|
#!/bin/bash
LOG_FILE="/var/log/app.log"
PATTERN="ERROR"
tail -f "$LOG_FILE" | while read line; do
if [[ "$line" == *"$PATTERN"* ]]; then
echo "ALERT: $line"
# Send notification
fi
done
|
Argument Parsing
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
|
#!/bin/bash
usage() {
echo "Usage: $0 [-v] [-o output] input_file"
exit 1
}
verbose=false
output=""
while getopts "vo:" opt; do
case $opt in
v) verbose=true ;;
o) output="$OPTARG" ;;
*) usage ;;
esac
done
shift $((OPTIND-1))
if [ $# -eq 0 ]; then
usage
fi
input_file="$1"
$verbose && echo "Processing: $input_file"
|
Service Health Check
1
2
3
4
5
6
7
8
9
10
11
12
|
#!/bin/bash
services=("nginx" "postgresql" "redis")
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service"; then
echo "[OK] $service is running"
else
echo "[FAIL] $service is NOT running"
systemctl start "$service"
fi
done
|
Best Practices
- Always quote variables:
"$variable"
- Use
set -euo pipefail for safety
- Check command existence:
command -v cmd >/dev/null
- Use functions for reusable code
- Add comments for complex logic
- Use shellcheck:
shellcheck script.sh
- Handle errors gracefully
- Use meaningful variable names
Quick Reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# Variables
var="value"
echo "$var"
# Conditionals
if [[ condition ]]; then
commands
fi
# Loops
for item in list; do
commands
done
# Functions
func() {
local var="$1"
echo "$var"
}
# Safety
set -euo pipefail
|
Shell scripting is the glue of Linux administration. Master it to automate everything.
Comments