Table of Contents
-
Advanced Variables & Data Structures
- Arrays
- Associative Arrays
- Parameter Expansion Tricks
-
Conditional Logic Beyond
if-else- Case Statements
- Arithmetic Conditions
- Pattern Matching with
[[ ]]
-
- Nested Loops
breakandcontinue- Infinite Loops & Safeguards
-
- Defining Functions
- Arguments & Return Values
- Scope: Local vs. Global Variables
-
- Exit Codes
setOptions (-e,-u,-o pipefail)trapCommand for Cleanup
-
Input/Output Redirection Mastery
- Advanced Redirection Operators
- Here-Documents & Here-Strings
- Process Substitution
-
Command Substitution & Expansion
$()vs. Backticks- Arithmetic Expansion
- Substring & Replacement Expansion
-
[[ ... =~ ... ]]for Regex Matching- Practical Regex Examples (Email, IP, Dates)
-
Process Management & Job Control
- Background Processes (
&) jobs,fg,bg- Parallel Execution with
wait
- Background Processes (
-
- Automated Backup Script
- Log Analyzer & Report Generator
- System Health Monitor
-
Best Practices for Advanced Scripts
- Commenting & Documentation
- Testing & Debugging
- Performance Optimization
1. Advanced Variables & Data Structures
Bash isn’t limited to simple strings and numbers. Advanced scripting leverages arrays and associative arrays for structured data, and parameter expansion for dynamic value manipulation.
Arrays
Arrays store ordered lists of values. They’re ideal for iterating over items like filenames, user IDs, or server names.
Syntax:
# Declare an array
fruits=("apple" "banana" "cherry" "date")
# Access an element (0-based index)
echo "First fruit: ${fruits[0]}" # Output: apple
# Get all elements
echo "All fruits: ${fruits[@]}" # Output: apple banana cherry date
# Get array length
echo "Number of fruits: ${#fruits[@]}" # Output: 4
# Add an element
fruits+=("elderberry")
echo "Updated fruits: ${fruits[@]}" # Output: apple banana cherry date elderberry
Use Case: Iterate over log files in a directory:
log_files=("/var/log/syslog" "/var/log/auth.log" "/var/log/dmesg")
for file in "${log_files[@]}"; do
echo "Processing $file..."
# Add logic to parse logs here
done
Associative Arrays
Associative arrays (dictionaries) store key-value pairs, perfect for mapping names to values (e.g., user roles, config settings).
Syntax:
# Declare an associative array (requires bash 4+)
declare -A user_roles
# Add key-value pairs
user_roles["alice"]="admin"
user_roles["bob"]="editor"
user_roles["charlie"]="viewer"
# Access a value by key
echo "Alice's role: ${user_roles["alice"]}" # Output: admin
# Iterate over keys
for user in "${!user_roles[@]}"; do
echo "$user: ${user_roles[$user]}"
done
# Output:
# alice: admin
# bob: editor
# charlie: viewer
Parameter Expansion Tricks
Parameter expansion lets you manipulate variable values dynamically (e.g., truncating strings, replacing substrings).
| Expansion | Purpose | Example | Output |
|---|---|---|---|
${var:position:length} | Extract substring | var="hello"; echo ${var:1:3} | ell |
${var#pattern} | Remove shortest prefix matching pattern | var="file.txt"; echo ${var#*.} | txt |
${var##pattern} | Remove longest prefix matching pattern | var="/home/user/docs/file.txt"; echo ${var##*/} | file.txt |
${var%pattern} | Remove shortest suffix matching pattern | var="file.txt"; echo ${var%.txt} | file |
${var/old/new} | Replace first occurrence of old with new | var="foo bar foo"; echo ${var/foo/baz} | baz bar foo |
${var//old/new} | Replace all occurrences | var="foo bar foo"; echo ${var//foo/baz} | baz bar baz |
${var:-default} | Use default if var is unset/empty | unset var; echo ${var:-"no value"} | no value |
2. Conditional Logic Beyond if-else
While if-else is foundational, advanced scripts use case statements for multi-branch logic, arithmetic conditions, and pattern matching with [[ ]].
Case Statements
Use case for when a variable could match multiple patterns (e.g., menu-driven scripts).
Syntax:
read -p "Enter a day (mon/fri/sun): " day
case $day in
mon|tue|wed|thu|fri)
echo "Weekday: Go to work!"
;;
sat|sun)
echo "Weekend: Relax!"
;;
*) # Default case (matches anything else)
echo "Invalid day!"
exit 1
;;
esac
Arithmetic Conditions
For numerical comparisons, use (( )) or [ $((...)) ]. Avoid [ ] for numbers—it treats values as strings!
Examples:
x=10
y=20
# Using (( )) (preferred for arithmetic)
if (( x < y )); then
echo "$x is less than $y" # Output: 10 is less than $y
fi
# Check if x is even
if (( x % 2 == 0 )); then
echo "$x is even" # Output: 10 is even
fi
Pattern Matching with [[ ]]
The [[ ]] construct supports regex and glob patterns, making it more powerful than [ ] (POSIX test).
Examples:
str="Hello, World!"
# Check if string contains "World" (glob pattern)
if [[ $str == *"World"* ]]; then
echo "Contains 'World'" # Output: Contains 'World'
fi
# Regex matching (email validation example)
email="[email protected]"
if [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "Valid email" # Output: Valid email
fi
3. Advanced Loops & Loop Control
Loops are workhorses for automation. Advanced scripting uses nested loops, loop control (break/continue), and safeguards for infinite loops.
Nested Loops
Loop inside another loop to process multi-dimensional data (e.g., rows and columns in a CSV).
Example: Generate a multiplication table
echo "Multiplication Table (1-5):"
for i in {1..5}; do
for j in {1..5}; do
product=$((i * j))
printf "%4d" $product # Align with 4 spaces
done
echo # New line after each row
done
Output:
Multiplication Table (1-5):
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
4 8 12 16 20
5 10 15 20 25
break and continue
break N: Exit the innermost N loops.continue N: Skip to the next iteration of the innermost N loops.
Example: Stop processing after finding a target
target=3
for i in {1..5}; do
if (( i == target )); then
echo "Found target $target! Exiting loop."
break # Exit the loop entirely
fi
echo "Processing $i..."
done
Output:
Processing 1...
Processing 2...
Found target 3! Exiting loop.
Infinite Loops & Safeguards
Infinite loops (while true) run until a condition is met. Always add a break condition to avoid hanging.
Example: Retry a failed command
max_retries=3
retry_count=0
command="curl https://api.example.com"
while true; do
if $command; then
echo "Command succeeded!"
break
else
retry_count=$((retry_count + 1))
if (( retry_count >= max_retries )); then
echo "Failed after $max_retries retries. Exiting."
exit 1
fi
echo "Retry $retry_count/$max_retries..."
sleep 2 # Wait 2 seconds before retrying
fi
done
4. Functions: Modular Scripting
Functions let you reuse code, making scripts cleaner and easier to debug. Advanced functions handle arguments, return values, and variable scope.
Defining Functions
Syntax:
# Basic function
greet() {
echo "Hello, $1!" # $1 = first argument
}
greet "Alice" # Output: Hello, Alice!
Arguments & Return Values
- Arguments: Access with
$1,$2, …,$@(all args),$#(number of args). - Return Values: Bash functions don’t return values directly—use
echoand capture output with$().
Example: Function to calculate factorial
factorial() {
local n=$1 # Local variable (only visible in the function)
if (( n <= 1 )); then
echo 1
else
echo $(( n * $(factorial $((n - 1))) )) # Recursive call
fi
}
result=$(factorial 5)
echo "5! = $result" # Output: 5! = 120
Scope: Local vs. Global Variables
- Global: Visible everywhere (default).
- Local: Use
local var=valueto limit to the function.
Example:
global_var="I'm global"
my_func() {
local local_var="I'm local" # Local to the function
global_var="Updated global" # Modifies global variable
echo "Inside function: local_var=$local_var, global_var=$global_var"
}
my_func
# Output: Inside function: local_var=I'm local, global_var=Updated global
echo "Outside function: global_var=$global_var" # Output: Updated global
echo "Outside function: local_var=$local_var" # Output: local_var= (undefined)
5. Error Handling & Robustness
Professional scripts handle errors gracefully. Use exit codes, set options, and trap to make scripts resilient.
Exit Codes
Every command returns an exit code:
0: Success1-255: Failure (custom codes: e.g.,1for general error,2for invalid input).
Check exit codes with $? (last command’s exit code):
ls non_existent_file
echo "Exit code: $?" # Output: 2 (failure)
set Options for Strictness
Add these at the top of scripts to catch errors early:
set -e: Exit immediately if any command fails.set -u: Treat undefined variables as errors.set -o pipefail: Exit if any command in a pipeline fails (not just the last one).
Example: Strict script header
#!/bin/bash
set -euo pipefail # Exit on error, undefined var, or pipeline failure
# This will fail because "undefined_var" is unset (due to set -u)
echo $undefined_var # Script exits here with error: "undefined_var: unbound variable"
trap Command for Cleanup
Use trap to run commands on script exit (e.g., clean up temporary files, stop background processes).
Example: Clean up temp files on exit
#!/bin/bash
set -euo pipefail
temp_file=$(mktemp) # Create a temporary file
echo "Temporary file: $temp_file"
# Define cleanup function
cleanup() {
echo "Cleaning up $temp_file..."
rm -f "$temp_file"
}
# Trap EXIT signal to run cleanup on script exit (success or failure)
trap cleanup EXIT
# Simulate work (e.g., write data to temp file)
echo "Hello, temp file!" > "$temp_file"
sleep 3 # Give time to check the temp file exists
When the script exits (even if interrupted with Ctrl+C), cleanup runs and deletes the temp file.
6. Input/Output Redirection Mastery
Advanced I/O redirection lets you control where output goes, feed input to commands, and treat commands as files.
Advanced Redirection Operators
| Operator | Purpose | Example |
|---|---|---|
> | Overwrite file with stdout | echo "Hi" > output.txt |
>> | Append stdout to file | echo "Bye" >> output.txt |
< | Read stdin from file | grep "error" < /var/log/syslog |
2> | Redirect stderr to file | ls non_existent 2> errors.txt |
&> | Redirect both stdout and stderr to file | command &> combined.log |
2>&1 | Redirect stderr to stdout (e.g., for piping) | `command 2>&1 |
Here-Documents & Here-Strings
- Here-Document (
<<): Pass multi-line input to a command. - Here-String (
<<<): Pass a single line of input quickly.
Example: Write a config file with here-document
cat > config.ini << "EOF" # "EOF" quotes disable variable expansion
[server]
host = "localhost"
port = 8080
debug = false
EOF
Example: Use here-string with grep
grep "foo" <<< "foo bar baz" # Output: foo bar baz
Process Substitution
Treat the output of a command as a temporary file with <() (input) or >() (output).
Example: Compare two command outputs
# Compare the output of "ls /tmp" and "ls /var/tmp"
diff <(ls /tmp) <(ls /var/tmp)
Example: Feed multiple logs to grep
# Search for "error" in syslog and auth.log simultaneously
grep "error" <(tail -f /var/log/syslog) <(tail -f /var/log/auth.log)
7. Command Substitution & Expansion
Command substitution runs a command and replaces it with its output. Advanced expansion includes arithmetic and dynamic string manipulation.
$() vs. Backticks
Both capture command output, but $() is preferred (nested commands work, easier to read).
Example: Get current date
# Using $() (preferred)
date=$($(date +%Y-%m-%d))
# Using backticks (legacy)
date=`date +%Y-%m-%d`
echo "Today: $date" # Output: Today: 2024-05-20
Arithmetic Expansion
Use $(( ... )) for integer math (no floating points—use bc for decimals).
Example: Calculate disk usage percentage
total=$(df -P / | tail -1 | awk '{print $2}') # Total blocks
used=$(df -P / | tail -1 | awk '{print $3}') # Used blocks
pct_used=$(( (used * 100) / total ))
echo "Disk usage: $pct_used%" # Output: Disk usage: 35%
8. Regular Expressions in Bash
Bash supports regex matching with [[ string =~ regex ]]. Use this for validation, parsing, and pattern matching.
[[ ... =~ ... ]] Syntax
Example: Validate an IP address (simplified)
ip="192.168.1.1"
regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
if [[ $ip =~ $regex ]]; then
echo "$ip is a valid IP (format)"
else
echo "$ip is invalid"
fi
Example: Extract numbers from a string
string="Order 1234: Total $50.99"
regex='([0-9]+)' # Match one or more digits
if [[ $string =~ $regex ]]; then
echo "First number found: ${BASH_REMATCH[1]}" # BASH_REMATCH[1] = first capture group
fi
# Output: First number found: 1234
9. Process Management & Job Control
Run commands in the background, manage jobs, and execute tasks in parallel with advanced process control.
Background Processes
Add & to run a command in the background. Use jobs to list background jobs, fg to bring them to the foreground, and bg to resume suspended jobs.
Example:
# Run a long command in the background
sleep 10 &
echo "Background job ID: $!" # $! = PID of last background job
# List jobs
jobs # Output: [1]+ Running sleep 10 &
# Bring job 1 to foreground
fg %1 # Job resumes in foreground; press Ctrl+Z to suspend, then "bg %1" to resume in background
Parallel Execution with wait
Use wait to pause until background jobs finish. Ideal for running tasks in parallel to save time.
Example: Run 3 tasks in parallel
#!/bin/bash
set -euo pipefail
task1() { sleep 2; echo "Task 1 done"; }
task2() { sleep 3; echo "Task 2 done"; }
task3() { sleep 1; echo "Task 3 done"; }
echo "Starting tasks..."
task1 &
task2 &
task3 &
wait # Wait for all background jobs to finish
echo "All tasks done!"
Output (order may vary, but “All tasks done!” waits for all):
Starting tasks...
Task 3 done
Task 1 done
Task 2 done
All tasks done!
10. Real-World Script Examples
Let’s apply advanced concepts to solve practical problems.
Example 1: Automated Backup Script
Compress a directory, check disk space, and log results.
#!/bin/bash
set -euo pipefail
# Configuration
source_dir="/home/user/documents"
backup_dir="/mnt/backup"
log_file="/var/log/backup.log"
date=$(date +%Y-%m-%d_%H-%M-%S)
backup_file="$backup_dir/docs_backup_$date.tar.gz"
# Cleanup on exit (e.g., if backup fails mid-compression)
trap 'rm -f "$backup_file"' EXIT
# Check if source directory exists
if [[ ! -d "$source_dir" ]]; then
echo "ERROR: Source directory $source_dir not found." | tee -a "$log_file"
exit 1
fi
# Check free space on backup drive (at least 1GB required)
free_space=$(df -P "$backup_dir" | tail -1 | awk '{print $4}') # Free blocks
required_space=$((1024 * 1024)) # 1GB in 1K blocks
if (( free_space < required_space )); then
echo "ERROR: Not enough space on $backup_dir. Required: 1GB, Free: $((free_space / 1024))MB." | tee -a "$log_file"
exit 1
fi
# Compress and backup
echo "Starting backup: $source_dir -> $backup_file" | tee -a "$log_file"
tar -czf "$backup_file" -C "$source_dir" . # -C changes to source_dir before archiving
# Verify backup
if [[ -f "$backup_file" && $(tar -tzf "$backup_file" | wc -l) -gt 0 ]]; then
echo "Backup succeeded! File size: $(du -h "$backup_file")" | tee -a "$log_file"
trap - EXIT # Disable cleanup (backup is valid)
else
echo "ERROR: Backup failed or is empty." | tee -a "$log_file"
exit 1
fi
Example 2: Log Analyzer & Report Generator
Count errors in a log file and generate a summary.
#!/bin/bash
set -euo pipefail
log_file="/var/log/syslog"
report_file="error_report_$(date +%Y-%m-%d).txt"
# Check log file exists
[[ -f "$log_file" ]] || { echo "Log file $log_file not found."; exit 1; }
# Generate report
{
echo "Error Report for $log_file"
echo "=========================="
echo "Generated: $(date)"
echo -e "\nTotal errors (last 24h): $(grep -cE 'ERROR|error' "$log_file" --since "24 hours ago")"
echo -e "\nTop 5 error messages:"
grep -E 'ERROR|error' "$log_file" --since "24 hours ago" | awk '{print $0}' | sort | uniq -c | sort -nr | head -5
} > "$report_file"
echo "Report generated: $report_file"
11. Best Practices for Advanced Scripts
- Comment Liberally: Explain why (not just what) the code does.
- Use
shellcheck: A linter for bash scripts (install withsudo apt install shellcheck). - Test Incrementally: Test small sections before combining them.
- Avoid Hard-Coded Values: Use variables or config files for paths/settings.
- Limit Global Variables: Use local variables in functions to avoid side effects.
- Handle Edge Cases: Check for empty inputs, missing files, or permission errors.
12. References
By mastering these advanced bash scripting techniques, you’ll transform from a casual user to a Linux automation pro. Start small, experiment, and build up to complex scripts—your future self (and colleagues) will thank you! 🚀