Table of Contents
- Advanced Parameter Expansion
- Arrays and Associative Arrays
- Process Substitution and Pipes
- Advanced Error Handling and Debugging
- Signal Trapping and Process Management
- Advanced Functions and Closures
- Performance Optimization Techniques
- Integration with External Tools and APIs
- Conclusion
- References
1. Advanced Parameter Expansion
Bash parameter expansion lets you manipulate variables dynamically without external tools like sed or awk. Mastering it reduces dependency on subshells and improves script efficiency. Here are key techniques:
1.1 Substring Extraction
Extract parts of a string using ${var:position:length}:
filename="report_2024-05-20.txt"
# Extract "2024-05-20" (start at index 7, length 10)
date=${filename:7:10}
echo $date # Output: 2024-05-20
1.2 Pattern Matching (Trimming)
Remove prefixes/suffixes with # (prefix) and % (suffix). Use ## or %% for greedy matching:
path="/home/user/docs/report.pdf"
# Remove shortest prefix matching "/home/"
echo ${path#/home/} # Output: user/docs/report.pdf
# Remove longest prefix matching "/*/" (greedy)
echo ${path##/*/} # Output: report.pdf
# Remove shortest suffix matching ".pdf"
echo ${path%.pdf} # Output: /home/user/docs/report
# Remove longest suffix matching "-*" (e.g., from "v1.2.3-beta")
version="v1.2.3-beta"
echo ${version%%-*} # Output: v1.2.3
1.3 Substitution and Replacement
Replace substrings with ${var/pattern/replacement}. Use // for global replacement:
message="Hello world! world is great."
# Replace first "world" with "Bash"
echo ${message/world/Bash} # Output: Hello Bash! world is great.
# Global replace "world" with "Bash"
echo ${message//world/Bash} # Output: Hello Bash! Bash is great.
# Replace if pattern matches start/end (^/$)
filename="data.csv"
echo ${filename/#data/report} # Output: report.csv (replace start)
echo ${filename/%.csv/.txt} # Output: data.txt (replace end)
1.4 Case Conversion
Convert case with ${var^^} (uppercase) and ${var,,} (lowercase):
text="Hello Bash Scripting"
echo ${text^^} # Output: HELLO BASH SCRIPTING
echo ${text,,} # Output: hello bash scripting
echo ${text^} # Output: Hello bash scripting (only first char uppercase)
1.5 Default Values and Fallbacks
Set defaults for unset/empty variables with ${var:-default} (use default if unset) or ${var:+value} (use value if set):
# Use "guest" if USER is unset/empty
echo "User: ${USER:-guest}" # Output: User: guest (if USER is unset)
# Use "active" if STATUS is set (even if empty)
STATUS=""; echo "Status: ${STATUS:+active}" # Output: Status: active
2. Arrays and Associative Arrays
Bash supports indexed arrays (ordered lists) and associative arrays (key-value pairs), enabling complex data structures for tasks like config management or log parsing.
2.1 Indexed Arrays
Initialize, modify, and iterate over indexed arrays:
# Initialize array
fruits=("apple" "banana" "cherry")
# Add element (append)
fruits+=("date")
# Access element (0-based index)
echo ${fruits[1]} # Output: banana
# Get all elements
echo ${fruits[@]} # Output: apple banana cherry date
# Get array length
echo ${#fruits[@]} # Output: 4
# Iterate over elements
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
2.2 Associative Arrays (Key-Value Pairs)
Use declare -A to define associative arrays (requires Bash 4+):
# Initialize associative array
declare -A capitals
capitals["France"]="Paris"
capitals["Japan"]="Tokyo"
capitals["Germany"]="Berlin"
# Access value by key
echo "Capital of Japan: ${capitals["Japan"]}" # Output: Capital of Japan: Tokyo
# Get all keys/values
echo "Countries: ${!capitals[@]}" # Output: Countries: France Japan Germany
echo "Capitals: ${capitals[@]}" # Output: Capitals: Paris Tokyo Berlin
# Iterate over key-value pairs
for country in "${!capitals[@]}"; do
echo "$country: ${capitals[$country]}"
done
2.3 Multidimensional Arrays (Emulated)
Bash lacks native multidimensional arrays, but you can emulate them using nested indexes or delimited strings:
# Emulate 2D array with indexed arrays
declare -a matrix
matrix[0]="1 2 3" # Row 0: [1, 2, 3]
matrix[1]="4 5 6" # Row 1: [4, 5, 6]
# Access element (row 1, column 2)
row=1; col=2
echo $(echo ${matrix[$row]} | cut -d' ' -f$((col+1))) # Output: 6
# Emulate with associative arrays (row,col as key)
declare -A grid
grid["0,0"]=1; grid["0,1"]=2; grid["0,2"]=3
grid["1,0"]=4; grid["1,1"]=5; grid["1,2"]=6
echo ${grid["1,2"]} # Output: 6
3. Process Substitution and Pipes
Process substitution lets you treat the output of a command as a temporary file, enabling seamless integration of commands without intermediate files. Use <(command) (input) or >(command) (output).
3.1 Compare Command Outputs
Use diff or comm with process substitution to compare outputs of two commands:
# Compare sorted contents of two directories (without temp files)
diff <(ls -l dir1 | sort) <(ls -l dir2 | sort)
# Find common lines between two log files (after filtering errors)
comm -12 <(grep "ERROR" app1.log | sort) <(grep "ERROR" app2.log | sort)
3.2 Feed Command Output to Another Command
Pass the output of one command as input to another without subshells:
# Count unique IPs in access.log (using awk and sort)
awk '{print $1}' access.log | sort | uniq -c | sort -nr
# Equivalent with process substitution (more readable for complex pipelines)
sort -nr <(uniq -c <(sort <(awk '{print $1}' access.log)))
3.3 Write to Multiple Processes
Use tee with process substitution to send output to multiple commands:
# Log to file AND display filtered errors in real-time
tail -f app.log | tee >(grep "ERROR" > errors.log)
4. Advanced Error Handling and Debugging
Robust scripts require granular error control. Beyond set -e (exit on error), use these techniques:
4.1 Strict Error Checking
Enable set -eo pipefail to exit on errors and failed pipeline commands:
#!/bin/bash
set -eo pipefail # Exit on error, and if any command in a pipe fails
# This will fail because "false" returns non-zero, and pipefail catches it
echo "Before" | false | echo "After" # Script exits here
echo "This line never runs"
4.2 Custom Error Functions
Create reusable error handlers with context (e.g., line numbers via $LINENO):
error_exit() {
echo "ERROR (line $1): $2" >&2 # Send to stderr
exit 1
}
# Usage: error_exit $LINENO "Message"
config_file="app.conf"
[ -f "$config_file" ] || error_exit $LINENO "Config file $config_file missing"
4.3 Trap Errors with trap
Use trap to catch errors (ERR signal) and execute cleanup logic:
#!/bin/bash
set -e
# Cleanup temp files on error or exit
cleanup() {
rm -f /tmp/temp_data*
echo "Cleanup complete"
}
trap cleanup ERR EXIT # Trigger on error (ERR) or exit (EXIT)
# Simulate error: temp file is deleted via cleanup()
cp important_data /tmp/temp_data.txt
false # This triggers ERR, cleanup runs, then script exits
4.4 Debugging with set -x
Enable debug tracing with set -x to print commands as they execute (use PS4 to customize output):
#!/bin/bash
PS4='+ [${BASH_SOURCE}:${LINENO}] ' # Show file and line number
set -x # Enable tracing
var="hello"
echo $var
set +x # Disable tracing
echo "Debug off"
Output:
+ [./script.sh:5] var=hello
+ [./script.sh:6] echo hello
hello
+ [./script.sh:7] set +x
Debug off
5. Signal Trapping and Process Management
Control how scripts respond to external signals (e.g., Ctrl+C or kill) with trap, ensuring graceful cleanup.
5.1 Catch Interrupts (SIGINT)
Prevent accidental termination during critical operations:
#!/bin/bash
interrupt_handler() {
echo "Script interrupted! Use Ctrl+D to exit."
}
trap interrupt_handler SIGINT # Catch Ctrl+C
# Keep script running until user presses Ctrl+D
while true; do
read -p "Enter input (Ctrl+D to exit): " input || break
echo "You entered: $input"
done
5.2 Graceful Shutdown on SIGTERM
Handle kill commands to clean up before exiting:
#!/bin/bash
shutdown() {
echo "Shutting down gracefully..."
# Stop background processes, save state, etc.
exit 0
}
trap shutdown SIGTERM # Catch `kill <pid>`
# Simulate long-running process
while true; do
echo "Running..."
sleep 1
done
6. Advanced Functions and Closures
Bash functions support local variables, recursion, and even closures (functions that capture variables from their environment).
6.1 Recursive Functions
Implement recursion for tasks like directory traversal or factorial calculation:
factorial() {
local n=$1
if [ $n -eq 0 ]; then
echo 1
else
echo $(( n * $(factorial $((n-1))) )) # Recursive call
fi
}
echo "5! = $(factorial 5)" # Output: 5! = 120
6.2 Closures (Emulated)
Bash lacks native closures, but you can emulate them with eval or command substitution to capture variables:
make_counter() {
local count=0
# Return a function that increments $count
echo 'count=$((count+1)); echo $count'
}
# Create a counter closure
counter=$(make_counter)
# Use the closure (increments each call)
eval "$counter" # Output: 1
eval "$counter" # Output: 2
eval "$counter" # Output: 3
7. Performance Optimization Techniques
Optimize scripts for speed by minimizing subshells, reducing I/O, and using builtins.
7.1 Avoid Subshells with { ... }
Use { ... } instead of ( ... ) to group commands without spawning a subshell:
# Slow: subshell creates a new process
total=0
for i in {1..1000}; do
total=$(($total + i)) # Subshell for each iteration
done
# Fast: no subshell (use arithmetic expansion)
total=0
for ((i=1; i<=1000; i++)); do
total=$((total + i)) # Builtin arithmetic
done
7.2 Batch I/O Operations
Avoid repeated file opens by redirecting once:
# Slow: opens/closes file 1000 times
for i in {1..1000}; do
echo "Line $i" >> data.txt # Slow!
done
# Fast: opens file once, appends all lines
{
for i in {1..1000}; do
echo "Line $i"
done
} >> data.txt # Single I/O operation
7.3 Use Builtins Over External Commands
Replace grep, sed, or awk with Bash builtins for simple tasks:
text="Hello World"
# Slow: uses external `grep`
if echo "$text" | grep -q "World"; then ...
# Fast: uses builtin `[[ ... =~ ]]` (regex match)
if [[ "$text" =~ "World" ]]; then ...
fi
8. Integration with External Tools and APIs
Bash scripts rarely work in isolation. Integrate with tools like jq (JSON), curl (APIs), or awk for advanced workflows.
8.1 Parse JSON with jq
Use jq to query JSON APIs (e.g., GitHub API):
#!/bin/bash
set -eo pipefail
# Fetch GitHub user info and extract name/bio
user="octocat"
curl -s "https://api.github.com/users/$user" | jq -r '.name, .bio'
Output:
The Octocat
A metaphorical cat with octopus arms
8.2 Automate AWS CLI with Bash
Combine Bash loops with AWS CLI to manage resources:
#!/bin/bash
# Stop all EC2 instances with tag "Environment=dev"
instance_ids=$(aws ec2 describe-instances \
--filters "Name=tag:Environment,Values=dev" \
--query "Reservations[].Instances[].InstanceId" \
--output text)
for id in $instance_ids; do
echo "Stopping instance $id..."
aws ec2 stop-instances --instance-ids "$id"
done
9. Conclusion
Advanced Bash scripting transforms simple automation into powerful, maintainable tools. By mastering parameter expansion, arrays, error handling, and integration with external tools, experienced developers can solve complex problems with minimal overhead.
Key takeaways:
- Use parameter expansion for string manipulation without external tools.
- Leverage arrays for structured data and associative arrays for key-value storage.
- Optimize performance by avoiding subshells and batching I/O.
- Ensure robustness with strict error checking and
trapfor cleanup.
Bash may not replace Python or Go for large-scale applications, but its ubiquity and simplicity make it indispensable for system administration, DevOps, and rapid automation.