Introduction to Bash Functions
Functions in Bash are reusable blocks of code that can be defined once and called multiple times throughout a script. They are essential for organizing code, reducing duplication, and creating modular, maintainable scripts. Unlike many programming languages, Bash functions are simple but powerful, with unique characteristics like handling arguments similarly to scripts.
Key Concepts
- Reusability: Write once, use many times
- Modularity: Break complex scripts into manageable pieces
- Scope: Variables can be local or global
- Return Values: Functions return exit status, not arbitrary values
- Arguments: Accessed similarly to script arguments ($1, $2, etc.)
1. Basic Function Syntax
Function Declaration
#!/bin/bash
# Method 1: Using function keyword
function greet {
echo "Hello, World!"
}
# Method 2: Using parentheses (POSIX-compliant)
greet2() {
echo "Hello again!"
}
# Method 3: One-line function
hello() { echo "Hi!"; }
# Calling functions
greet
greet2
hello
# Functions must be defined before they're called
# This will fail:
# call_early
# call_early() {
# echo "This function is called before definition"
# }
Function with Arguments
#!/bin/bash
# Function that accepts arguments
greet_person() {
echo "Hello, $1! How is your $2?"
}
# Function with multiple arguments
print_info() {
echo "Name: $1"
echo "Age: $2"
echo "City: $3"
echo "All arguments: $@"
echo "Number of arguments: $#"
}
# Calling functions with arguments
greet_person "John" "day"
print_info "Alice" 30 "New York"
# Accessing all arguments
show_args() {
echo "First: $1"
echo "Second: $2"
echo "All: $*"
echo "Count: $#"
}
show_args "one" "two" "three" "four"
Function Return Values
#!/bin/bash
# Functions return exit status (0-255)
check_file() {
if [ -f "$1" ]; then
return 0 # Success (true)
else
return 1 # Failure (false)
fi
}
# Using return status
if check_file "/etc/passwd"; then
echo "File exists"
else
echo "File not found"
fi
# Return values are limited to 0-255
get_number() {
return 42 # Returns exit code 42
}
get_number
echo "Return value: $?" # 42
# To return arbitrary values, use echo
get_name() {
echo "John Doe"
}
name=$(get_name)
echo "Name: $name"
# Multiple return values
get_user_info() {
echo "John" # First value
echo "30" # Second value
}
read name age <<< "$(get_user_info)"
echo "Name: $name, Age: $age"
# Or using arrays
get_colors() {
echo "red green blue"
}
colors=($(get_colors))
echo "First color: ${colors[0]}"
2. Variable Scope
Local vs Global Variables
#!/bin/bash
# Global variable
global_var="I'm global"
demo_scope() {
# Local variable
local local_var="I'm local"
# Modifying global
global_var="Modified in function"
echo "Inside function:"
echo " global_var = $global_var"
echo " local_var = $local_var"
}
echo "Before function:"
echo " global_var = $global_var"
demo_scope
echo "After function:"
echo " global_var = $global_var"
echo " local_var = $local_var" # Empty (not accessible)
# Local variable with same name as global
name="Global"
test_local() {
local name="Local"
echo "Inside: $name"
}
test_local
echo "Outside: $name" # Still "Global"
# Declare can also create local variables
test_declare() {
declare var="Declared inside"
echo "Inside: $var"
}
test_declare
echo "Outside: $var" # Empty
Variable Scope Best Practices
#!/bin/bash
# Avoid global variables when possible
# Bad
counter=0
increment_bad() {
((counter++))
}
# Good
increment_good() {
local count=$1
echo $((count + 1))
}
counter=0
counter=$(increment_good $counter)
# Using local with default values
configure() {
local host="${1:-localhost}"
local port="${2:-8080}"
local debug="${3:-false}"
echo "Host: $host"
echo "Port: $port"
echo "Debug: $debug"
}
configure
configure "example.com" 9090 true
# Read-only local variables
calculate() {
local -r PI=3.14159
local -r radius=$1
echo "Area: $(echo "$PI * $radius * $radius" | bc)"
}
calculate 5
# PI cannot be changed inside function
3. Advanced Function Features
Functions with Options
#!/bin/bash
# Function that accepts options
process_file() {
local verbose=false
local backup=false
local file=""
# Parse options
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
verbose=true
shift
;;
-b|--backup)
backup=true
shift
;;
-f|--file)
file="$2"
shift 2
;;
-h|--help)
echo "Usage: process_file [-v] [-b] -f FILE"
return 0
;;
*)
echo "Unknown option: $1"
return 1
;;
esac
done
# Validate required arguments
if [ -z "$file" ]; then
echo "Error: File required"
return 1
fi
# Process file
[ "$verbose" = true ] && echo "Processing file: $file"
[ "$backup" = true ] && cp "$file" "$file.bak"
# Do something with file
wc -l "$file"
}
# Usage
process_file -v -b -f /etc/passwd
process_file --verbose --file /etc/hosts
Recursive Functions
#!/bin/bash
# Factorial using recursion
factorial() {
local n=$1
if [ $n -le 1 ]; then
echo 1
else
local prev=$(factorial $((n - 1)))
echo $((n * prev))
fi
}
echo "Factorial of 5: $(factorial 5)"
# Fibonacci sequence
fibonacci() {
local n=$1
if [ $n -le 1 ]; then
echo $n
else
local prev1=$(fibonacci $((n - 1)))
local prev2=$(fibonacci $((n - 2)))
echo $((prev1 + prev2))
fi
}
for i in {0..10}; do
echo "fib($i) = $(fibonacci $i)"
done
# Directory traversal
traverse() {
local dir="$1"
local indent="$2"
echo "${indent}📁 $dir"
for item in "$dir"/*; do
if [ -d "$item" ]; then
traverse "$item" " $indent"
elif [ -f "$item" ]; then
echo " ${indent}📄 $(basename "$item")"
fi
done
}
traverse "/tmp" ""
Functions with Variable Number of Arguments
#!/bin/bash
# Sum all arguments
sum_all() {
local total=0
for num in "$@"; do
((total += num))
done
echo $total
}
echo "Sum: $(sum_all 1 2 3 4 5)"
# Join all arguments with separator
join() {
local separator="$1"
shift
local result="$1"
shift
for item in "$@"; do
result="${result}${separator}${item}"
done
echo "$result"
}
echo "$(join , a b c d e)"
echo "$(join ' | ' apple orange banana)"
# Maximum of arguments
max() {
local max=$1
for num in "$@"; do
if [ "$num" -gt "$max" ]; then
max=$num
fi
done
echo $max
}
echo "Max: $(max 42 17 93 56 28)"
# Filter arguments
filter() {
local pattern="$1"
shift
for item in "$@"; do
if [[ "$item" =~ $pattern ]]; then
echo "$item"
fi
done
}
echo "Numbers: $(filter '^[0-9]+$' abc 123 def 456 ghi)"
4. Functions as Commands
Command-like Functions
#!/bin/bash
# Function that behaves like a command
git() {
echo "Wrapped git command"
if [ "$1" = "clone" ]; then
echo "Cloning repository: $2"
# Add logging or validation
fi
command git "$@"
}
# Override built-in command
cd() {
echo "Changing to directory: $1"
builtin cd "$1"
echo "Now in: $(pwd)"
}
# Function that pipes output
list_files() {
ls -la "$@" | grep -v "^total" | awk '{print $9, $5}'
}
list_files /etc
# Function that accepts stdin
process_stdin() {
while read line; do
echo "Processed: ${line^^}"
done
}
echo -e "hello\nworld" | process_stdin
# Command pipeline
get_running_services() {
systemctl list-units --type=service --state=running --no-legend | awk '{print $1}'
}
get_running_services | head -5
Function Libraries
#!/bin/bash
# Source function library
# Save as lib/utils.sh
cat > lib/utils.sh << 'EOF'
#!/bin/bash
# Logging functions
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $*"
}
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $*" >&2
}
log_debug() {
if [ "${DEBUG:-false}" = "true" ]; then
echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') - $*"
fi
}
# File operations
safe_copy() {
local src="$1"
local dest="$2"
if [ ! -f "$src" ]; then
log_error "Source not found: $src"
return 1
fi
if [ -f "$dest" ]; then
log_debug "Destination exists, creating backup"
cp "$dest" "$dest.bak"
fi
cp "$src" "$dest" && log_info "Copied $src to $dest"
}
# String operations
to_upper() {
echo "${1^^}"
}
to_lower() {
echo "${1,,}"
}
trim() {
local var="$*"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
echo "$var"
}
# Array operations
array_join() {
local sep="$1"
shift
local IFS="$sep"
echo "$*"
}
array_contains() {
local search="$1"
shift
for item in "$@"; do
if [ "$item" = "$search" ]; then
return 0
fi
done
return 1
}
EOF
# Use the library
source lib/utils.sh
log_info "Starting script"
safe_copy "/etc/passwd" "/tmp/passwd.backup"
echo "Uppercase: $(to_upper "hello world")"
colors=("red" "green" "blue")
if array_contains "green" "${colors[@]}"; then
log_info "Found green in array"
fi
5. Error Handling in Functions
Return Codes and Error Checking
#!/bin/bash
# Function with error handling
divide() {
local dividend=$1
local divisor=$2
if [ "$divisor" -eq 0 ]; then
echo "Error: Division by zero" >&2
return 1
fi
echo $((dividend / divisor))
return 0
}
# Check return code
result=$(divide 10 2)
if [ $? -eq 0 ]; then
echo "Result: $result"
fi
if ! result=$(divide 10 0); then
echo "Division failed"
fi
# Multiple error conditions
validate_user() {
local username="$1"
# Check if empty
if [ -z "$username" ]; then
echo "Error: Username cannot be empty" >&2
return 1
fi
# Check length
if [ ${#username} -lt 3 ]; then
echo "Error: Username too short" >&2
return 2
fi
# Check characters
if [[ ! "$username" =~ ^[a-zA-Z0-9_]+$ ]]; then
echo "Error: Invalid characters" >&2
return 3
fi
# Check if exists
if id "$username" &>/dev/null; then
echo "Error: User already exists" >&2
return 4
fi
return 0
}
# Handle different error codes
validate_user "jo"
case $? in
1) echo "Empty username" ;;
2) echo "Too short" ;;
3) echo "Invalid chars" ;;
4) echo "Already exists" ;;
0) echo "Valid username" ;;
esac
Trap and Cleanup
#!/bin/bash
# Function with cleanup
work_with_temp() {
local temp_file=$(mktemp)
# Set trap to clean up on exit
trap 'rm -f "$temp_file"' RETURN
echo "Working with temp file: $temp_file"
echo "Some data" > "$temp_file"
# Do some work
cat "$temp_file"
# File will be removed automatically when function returns
}
work_with_temp
echo "Temp file should be gone"
# Function with multiple exit points
process_data() {
local input="$1"
local output="$2"
local temp1=$(mktemp)
local temp2=$(mktemp)
# Cleanup on any exit
trap 'rm -f "$temp1" "$temp2"' RETURN
# Step 1
if ! grep "pattern" "$input" > "$temp1"; then
echo "Pattern not found"
return 1
fi
# Step 2
if ! sort "$temp1" > "$temp2"; then
return 2
fi
# Step 3
if ! mv "$temp2" "$output"; then
return 3
fi
echo "Processing complete"
}
process_data "/etc/passwd" "/tmp/processed.txt"
6. Advanced Function Techniques
Function Composition
#!/bin/bash
# Function composition
compose() {
local f="$1"
local g="$2"
local x="$3"
# Apply g then f
eval "$f \"\$($g \"\$x\")\""
}
# Example functions
double() {
echo $(($1 * 2))
}
square() {
echo $(($1 * $1))
}
add_one() {
echo $(($1 + 1))
}
# Compose functions
result=$(compose "double" "square" 5)
echo "square then double: $result" # 5^2 = 25, *2 = 50
result=$(compose "add_one" "double" 5)
echo "double then add_one: $result" # 5*2 = 10, +1 = 11
# Pipeline composition
pipeline() {
local x="$1"
shift
for func in "$@"; do
x=$($func "$x")
done
echo "$x"
}
result=$(pipeline 5 "square" "double" "add_one")
echo "Pipeline: $result" # square(5)=25, double(25)=50, add_one(50)=51
Currying and Partial Application
#!/bin/bash
# Create function with preset arguments
with_prefix() {
local prefix="$1"
shift
echo "${prefix}: $*"
}
# Partial application
greet() {
local greeting="$1"
# Return a function that uses the greeting
cat <<EOF
greet_with() {
echo "$greeting, \$1"
}
EOF
}
# Create specific greeting functions
eval "$(greet "Hello")"
eval "$(greet "Goodbye")"
greet_with "John" # Hello, John
goodbye_with "Mary" # Goodbye, Mary
# Configuration presets
create_server() {
local default_host="$1"
local default_port="$2"
cat <<EOF
server() {
local host="\${1:-$default_host}"
local port="\${2:-$default_port}"
echo "Connecting to \$host:\$port"
}
EOF
}
eval "$(create_server "localhost" 8080)"
server # localhost:8080
server "example.com" 9090 # example.com:9090
Memoization (Caching)
#!/bin/bash
# Simple memoization
declare -A CACHE
memoize() {
local func="$1"
local arg="$2"
local key="${func}_${arg}"
if [ -n "${CACHE[$key]}" ]; then
echo "Using cached value for $arg"
echo "${CACHE[$key]}"
return
fi
# Compute value
local result=$($func "$arg")
CACHE[$key]="$result"
echo "$result"
}
# Expensive function
fib() {
local n=$1
if [ $n -le 1 ]; then
echo $n
else
local prev1=$(fib $((n - 1)))
local prev2=$(fib $((n - 2)))
echo $((prev1 + prev2))
fi
}
# Memoized version
memo_fib() {
memoize "fib" "$1"
}
# Compare performance
time fib 30
time memo_fib 30
time memo_fib 30 # Much faster, cached
7. Practical Function Examples
File Processing Functions
#!/bin/bash
# Count lines in file
count_lines() {
local file="$1"
if [ ! -f "$file" ]; then
echo "Error: File not found" >&2
return 1
fi
wc -l < "$file"
}
# Find and replace in file
find_replace() {
local file="$1"
local pattern="$2"
local replacement="$3"
local backup="${4:-false}"
if [ ! -f "$file" ]; then
echo "Error: File not found" >&2
return 1
fi
if [ "$backup" = "true" ]; then
cp "$file" "$file.bak"
fi
sed -i "s/$pattern/$replacement/g" "$file"
echo "Replaced all occurrences in $file"
}
# Extract unique words
unique_words() {
local file="$1"
tr -cs '[:alpha:]' '\n' < "$file" | \
tr '[:upper:]' '[:lower:]' | \
sort | uniq | grep -v '^$'
}
# File statistics
file_stats() {
local file="$1"
if [ ! -f "$file" ]; then
echo "Error: File not found" >&2
return 1
fi
echo "Statistics for: $file"
echo "Size: $(du -h "$file" | cut -f1)"
echo "Lines: $(wc -l < "$file")"
echo "Words: $(wc -w < "$file")"
echo "Characters: $(wc -c < "$file")"
echo "Owner: $(stat -c %U "$file")"
echo "Permissions: $(stat -c %a "$file")"
}
String Manipulation Functions
#!/bin/bash
# Reverse a string
str_reverse() {
local str="$1"
local reversed=""
for (( i=${#str}-1; i>=0; i-- )); do
reversed="${reversed}${str:$i:1}"
done
echo "$reversed"
}
# Check if palindrome
is_palindrome() {
local str="${1,,}" # lowercase
str=$(echo "$str" | tr -d '[:punct:][:space:]')
[ "$str" = "$(str_reverse "$str")" ]
}
# Slugify string
slugify() {
local str="$1"
# Convert to lowercase
str="${str,,}"
# Replace spaces with hyphens
str="${str// /-}"
# Remove non-alphanumeric characters
str=$(echo "$str" | sed 's/[^a-z0-9-]//g')
# Remove duplicate hyphens
while [[ "$str" == *--* ]]; do
str="${str//--/-}"
done
# Trim hyphens from ends
str="${str#-}"
str="${str%-}"
echo "$str"
}
# Truncate string
str_truncate() {
local str="$1"
local max="$2"
local suffix="${3:-...}"
if [ ${#str} -le $max ]; then
echo "$str"
else
echo "${str:0:$((max - ${#suffix}))}${suffix}"
fi
}
# Test
str="Hello, World!"
echo "Reverse: $(str_reverse "$str")"
is_palindrome "racecar" && echo "Is palindrome"
echo "Slug: $(slugify "Hello World! How are you?")"
echo "Truncated: $(str_truncate "$str" 10)"
Math Functions
#!/bin/bash
# Basic math functions
abs() {
echo "${1#-}"
}
min() {
local min=$1
for num in "$@"; do
[ "$num" -lt "$min" ] && min=$num
done
echo $min
}
max() {
local max=$1
for num in "$@"; do
[ "$num" -gt "$max" ] && max=$num
done
echo $max
}
# Average
avg() {
local sum=0
for num in "$@"; do
((sum += num))
done
echo "scale=2; $sum / $#" | bc
}
# Factorial
fact() {
local n=$1
local result=1
for (( i=2; i<=n; i++ )); do
((result *= i))
done
echo $result
}
# Prime check
is_prime() {
local n=$1
if [ "$n" -lt 2 ]; then
return 1
fi
local max=$(echo "sqrt($n)" | bc)
for (( i=2; i<=max; i++ )); do
if [ $((n % i)) -eq 0 ]; then
return 1
fi
done
return 0
}
# GCD
gcd() {
local a=$1
local b=$2
while [ "$b" -ne 0 ]; do
local temp=$b
b=$((a % b))
a=$temp
done
echo $a
}
# LCM
lcm() {
local a=$1
local b=$2
local g=$(gcd $a $b)
echo $((a * b / g))
}
# Test
echo "min 5 2 8 1: $(min 5 2 8 1)"
echo "max 5 2 8 1: $(max 5 2 8 1)"
echo "avg 5 2 8 1: $(avg 5 2 8 1)"
echo "fact 5: $(fact 5)"
is_prime 17 && echo "17 is prime"
echo "gcd 48 18: $(gcd 48 18)"
echo "lcm 12 18: $(lcm 12 18)"
System Information Functions
#!/bin/bash
# Get system information
get_os() {
case "$(uname -s)" in
Linux*) echo "Linux" ;;
Darwin*) echo "macOS" ;;
CYGWIN*) echo "Cygwin" ;;
MINGW*) echo "MinGW" ;;
*) echo "Unknown" ;;
esac
}
get_cpu_count() {
nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "Unknown"
}
get_memory_info() {
if [ "$(get_os)" = "Linux" ]; then
free -h | awk '/^Mem:/ {print "Total: " $2, "Available: " $7}'
elif [ "$(get_os)" = "macOS" ]; then
vm_stat | perl -ne '/page size of (\d+)/ and $size=$1; /Pages free: (\d+)/ and printf("Free: %.2f GB\n", $1 * $size / 1073741824);'
fi
}
get_disk_usage() {
local mount_point="${1:-/}"
df -h "$mount_point" | awk 'NR==2 {print "Used: " $3, "Available: " $4, "Usage: " $5}'
}
get_load_average() {
uptime | awk -F'load average:' '{print $2}'
}
get_process_count() {
ps aux | wc -l
}
# System report
system_report() {
echo "=== System Report ==="
echo "OS: $(get_os)"
echo "Hostname: $(hostname)"
echo "Kernel: $(uname -r)"
echo "CPU Cores: $(get_cpu_count)"
echo "Load Average:$(get_load_average)"
echo "Memory: $(get_memory_info)"
echo "Disk: $(get_disk_usage /)"
echo "Processes: $(get_process_count)"
echo "Uptime: $(uptime | awk '{print $3,$4}' | sed 's/,//')"
}
8. Testing and Debugging Functions
Unit Testing Functions
#!/bin/bash
# Simple testing framework
test_framework() {
local tests_run=0
local tests_passed=0
local tests_failed=0
# Assert functions
assert_equals() {
local expected="$1"
local actual="$2"
local message="${3:-}"
((tests_run++))
if [ "$expected" = "$actual" ]; then
echo "✓ PASS: $message"
((tests_passed++))
else
echo "✗ FAIL: $message"
echo " Expected: '$expected'"
echo " Actual: '$actual'"
((tests_failed++))
fi
}
assert_true() {
assert_equals 0 $? "$1"
}
assert_false() {
assert_equals 1 $? "$1"
}
# Run test suite
echo "Running tests..."
echo "================="
# Define tests
test_string_functions() {
local result
result=$(to_upper "hello")
assert_equals "HELLO" "$result" "to_upper converts to uppercase"
result=$(to_lower "WORLD")
assert_equals "world" "$result" "to_lower converts to lowercase"
result=$(trim " spaced ")
assert_equals "spaced" "$result" "trim removes whitespace"
}
test_math_functions() {
local result
result=$(abs -5)
assert_equals "5" "$result" "abs of -5 is 5"
result=$(abs 5)
assert_equals "5" "$result" "abs of 5 is 5"
result=$(min 5 2 8 1)
assert_equals "1" "$result" "min of [5,2,8,1] is 1"
result=$(max 5 2 8 1)
assert_equals "8" "$result" "max of [5,2,8,1] is 8"
}
test_file_functions() {
local test_file="/tmp/test_$$.txt"
echo "test content" > "$test_file"
assert_true "check_file exists" check_file "$test_file"
rm "$test_file"
assert_false "check_file fails for missing file" check_file "$test_file"
}
# Run tests
test_string_functions
test_math_functions
test_file_functions
# Summary
echo "================="
echo "Tests: $tests_run, Passed: $tests_passed, Failed: $tests_failed"
return $tests_failed
}
# Run the tests
test_framework
Debugging Functions
#!/bin/bash
# Debug mode flag
DEBUG=${DEBUG:-false}
# Debug logging
debug_log() {
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG] $(date '+%H:%M:%S') - $*" >&2
fi
}
# Function call tracer
trace_call() {
local func="$1"
shift
debug_log "Calling $func with args: $*"
# Time the function
local start=$(date +%s.%N)
"$func" "$@"
local result=$?
local end=$(date +%s.%N)
local duration=$(echo "$end - $start" | bc)
debug_log "$func returned $result in ${duration}s"
return $result
}
# Function profiler
profile_function() {
local func="$1"
local iterations="${2:-100}"
shift 2
debug_log "Profiling $func with $iterations iterations"
local start=$SECONDS
for ((i=0; i<iterations; i++)); do
"$func" "$@" > /dev/null
done
local duration=$((SECONDS - start))
echo "$func: ${duration}s for $iterations iterations ($(bc <<< "scale=3; $duration / $iterations")s per call)"
}
# Example usage
slow_function() {
sleep 0.01
echo "Done"
}
# Enable debug
DEBUG=true
# Call with tracing
trace_call slow_function
# Profile
profile_function slow_function 10
9. Function Libraries and Modules
Creating Function Libraries
#!/bin/bash
# lib/strings.sh
cat > lib/strings.sh << 'EOF'
#!/bin/bash
# String manipulation library
# Check if string contains substring
contains() {
local haystack="$1"
local needle="$2"
[[ "$haystack" == *"$needle"* ]]
}
# Get string before delimiter
before() {
local str="$1"
local delim="$2"
echo "${str%%$delim*}"
}
# Get string after delimiter
after() {
local str="$1"
local delim="$2"
echo "${str#*$delim}"
}
# Count occurrences
count_occurrences() {
local str="$1"
local substr="$2"
local count=0
while [[ "$str" == *"$substr"* ]]; do
((count++))
str="${str#*$substr}"
done
echo "$count"
}
# Export functions
EOF
# lib/math.sh
cat > lib/math.sh << 'EOF'
#!/bin/bash
# Math library
# Check if number is even
is_even() {
[ $(( $1 % 2 )) -eq 0 ]
}
# Check if number is odd
is_odd() {
[ $(( $1 % 2 )) -ne 0 ]
}
# Power function
pow() {
local base="$1"
local exp="$2"
local result=1
for ((i=0; i<exp; i++)); do
((result *= base))
done
echo "$result"
}
# Clamp value between min and max
clamp() {
local value="$1"
local min="$2"
local max="$3"
if [ "$value" -lt "$min" ]; then
echo "$min"
elif [ "$value" -gt "$max" ]; then
echo "$max"
else
echo "$value"
fi
}
EOF
# lib/file.sh
cat > lib/file.sh << 'EOF'
#!/bin/bash
# File operations library
# Check if file is readable
is_readable() {
[ -r "$1" ]
}
# Check if file is writable
is_writable() {
[ -w "$1" ]
}
# Get file extension
get_extension() {
local file="$1"
echo "${file##*.}"
}
# Get filename without extension
get_basename() {
local file="$1"
echo "${file%.*}"
}
# Safe delete (move to trash)
safe_delete() {
local trash="/tmp/trash"
mkdir -p "$trash"
mv "$1" "$trash/$(basename "$1").$(date +%s)"
echo "Moved $1 to trash"
}
EOF
# Main script using libraries
source lib/strings.sh
source lib/math.sh
source lib/file.sh
# Use library functions
echo "Contains: $(contains "hello world" "world" && echo yes || echo no)"
echo "Before comma: $(before "apple,banana,cherry" ",")"
echo "After colon: $(after "key:value" ":")"
echo "Count 'a' in 'banana': $(count_occurrences "banana" "a")"
echo "Is 42 even? $(is_even 42 && echo yes || echo no)"
echo "2^8 = $(pow 2 8)"
echo "Clamp 15 between 10 and 20: $(clamp 15 10 20)"
is_readable "/etc/passwd" && echo "/etc/passwd is readable"
echo "Extension of file.txt: $(get_extension "file.txt")"
Module System
#!/bin/bash
# Simple module system
MODULE_PATH="${MODULE_PATH:-./lib}"
declare -A MODULES_LOADED
# Module loader
import() {
local module="$1"
# Check if already loaded
if [ "${MODULES_LOADED[$module]}" = "1" ]; then
return 0
fi
# Try to find module
local module_file="$MODULE_PATH/${module}.sh"
if [ ! -f "$module_file" ]; then
echo "Error: Module not found: $module" >&2
return 1
fi
# Load module
source "$module_file"
MODULES_LOADED[$module]=1
# Call module init function if exists
if declare -f "${module}_init" > /dev/null; then
"${module}_init"
fi
return 0
}
# List loaded modules
loaded_modules() {
echo "Loaded modules:"
for module in "${!MODULES_LOADED[@]}"; do
echo " - $module"
done
}
# Module configuration
configure_module() {
local module="$1"
local key="$2"
local value="$3"
eval "${module}_CONFIG_${key}='$value'"
}
# Usage
import "strings"
import "math"
import "file"
loaded_modules
# Configure module
configure_module "math" "precision" "4"
# Use module functions
strings_contains "hello" "he" && echo "Found"
10. Performance Optimization
Function Inlining and Optimization
#!/bin/bash
# Function call overhead demonstration
empty_function() {
:
}
# Benchmark function calls
benchmark_calls() {
local iterations="${1:-10000}"
echo "Benchmarking with $iterations iterations"
# Direct code
start=$SECONDS
for ((i=0; i<iterations; i++)); do
:
done
direct_time=$((SECONDS - start))
# Function call
start=$SECONDS
for ((i=0; i<iterations; i++)); do
empty_function
done
function_time=$((SECONDS - start))
echo "Direct code: $direct_time s"
echo "Function calls: $function_time s"
echo "Overhead: $((function_time - direct_time)) s"
}
# Inline critical code
# Bad
is_positive() {
[ "$1" -gt 0 ]
}
process_numbers_slow() {
for num in "$@"; do
if is_positive "$num"; then
echo "$num is positive"
fi
done
}
# Good - inline for performance
process_numbers_fast() {
for num in "$@"; do
if [ "$num" -gt 0 ]; then
echo "$num is positive"
fi
done
}
# Cache expensive computations
declare -A CACHE
fibonacci_cached() {
local n=$1
if [ -n "${CACHE[$n]}" ]; then
echo "${CACHE[$n]}"
return
fi
if [ $n -le 1 ]; then
result=$n
else
local prev1=$(fibonacci_cached $((n - 1)))
local prev2=$(fibonacci_cached $((n - 2)))
result=$((prev1 + prev2))
fi
CACHE[$n]=$result
echo $result
}
Reducing Function Overhead
#!/bin/bash
# Avoid unnecessary subshells
# Bad - creates subshell
get_value_bad() {
echo "value"
}
result=$(get_value_bad)
# Good - sets variable directly
get_value_good() {
result="value"
}
# Bad - multiple commands in subshell
process_data_bad() {
grep "pattern" "$1" | sort | uniq
}
# Good - process in current shell
process_data_good() {
grep "pattern" "$1" | sort | uniq
} # Still creates pipeline subshells, but better
# Use local variables for speed
slow_function() {
counter=0
for i in {1..1000}; do
((counter++))
done
echo $counter
}
fast_function() {
local counter=0
for i in {1..1000}; do
((counter++))
done
echo $counter
}
# Avoid function calls in loops
# Bad
square() { echo $(($1 * $1)); }
process_array_bad() {
for num in "${@}"; do
square "$num"
done
}
# Good
process_array_good() {
for num in "${@}"; do
echo $((num * num))
done
}
11. Best Practices and Patterns
Function Documentation
#!/bin/bash
# Document functions properly
#===============================================================================
# @function: calculate_discount
# @description: Calculate discounted price based on customer type
# @param1: Original price (float)
# @param2: Customer type (regular|premium|vip)
# @returns: Discounted price (float)
# @example: calculate_discount 100.00 "premium"
#===============================================================================
calculate_discount() {
local price="$1"
local customer_type="${2:-regular}"
case "$customer_type" in
regular) echo "scale=2; $price * 0.95" | bc ;;
premium) echo "scale=2; $price * 0.90" | bc ;;
vip) echo "scale=2; $price * 0.85" | bc ;;
*) echo "$price" ;;
esac
}
#===============================================================================
# @function: validate_email
# @description: Validate email address format
# @param1: Email address to validate
# @returns: 0 (success) if valid, 1 (failure) if invalid
# @example: if validate_email "[email protected]"; then ...
#===============================================================================
validate_email() {
local email="$1"
# Basic email validation regex
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# Generate function documentation
function_docs() {
local func="$1"
# Extract comment block before function
sed -n "/^#=/,/^${func}/p" "$0" | grep -v "^${func}"
}
function_docs "calculate_discount"
Design Patterns
#!/bin/bash
# Factory pattern
create_logger() {
local type="$1"
case "$type" in
file)
cat << 'EOF'
logger() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "/var/log/app.log"
}
EOF
;;
console)
cat << 'EOF'
logger() {
echo "[$(date '+%H:%M:%S')] $*"
}
EOF
;;
syslog)
cat << 'EOF'
logger() {
logger -t "APP" "$*"
}
EOF
;;
esac
}
# Create and use logger
eval "$(create_logger "console")"
logger "Application started"
# Strategy pattern
set_sort_strategy() {
local strategy="$1"
case "$strategy" in
asc)
sort_function() { sort; }
;;
desc)
sort_function() { sort -r; }
;;
numeric)
sort_function() { sort -n; }
;;
random)
sort_function() { shuf; }
;;
esac
}
# Use strategy
set_sort_strategy "desc"
echo -e "3\n1\n4\n2" | sort_function
# Singleton pattern
get_config() {
# Only load config once
if [ -z "$CONFIG_LOADED" ]; then
echo "Loading configuration..."
declare -g -A CONFIG=(
["host"]="localhost"
["port"]="8080"
["debug"]="true"
)
declare -g CONFIG_LOADED=true
fi
echo "${CONFIG[$1]}"
}
echo "Host: $(get_config "host")"
echo "Port: $(get_config "port")"
echo "Debug: $(get_config "debug")"
# Observer pattern
declare -a OBSERVERS
subscribe() {
OBSERVERS+=("$1")
}
notify() {
local event="$1"
shift
for observer in "${OBSERVERS[@]}"; do
$observer "$event" "$@"
done
}
# Example observers
email_observer() {
echo "Email: $*"
}
log_observer() {
echo "Log: $(date) - $*"
}
# Subscribe and notify
subscribe "email_observer"
subscribe "log_observer"
notify "user_login" "john" "success"
Error Handling Patterns
#!/bin/bash
# Try-catch pattern
try() {
local temp_file=$(mktemp)
# Redirect errors to temp file
(
set -e # Exit on error
"$@" 2>&1 || echo "ERROR:$?" > "$temp_file"
)
if [ -f "$temp_file" ] && grep -q "^ERROR:" "$temp_file"; then
local code=$(sed 's/^ERROR://' "$temp_file")
rm "$temp_file"
return "$code"
fi
rm "$temp_file"
return 0
}
catch() {
local error_handler="$1"
local error_code=$?
if [ $error_code -ne 0 ]; then
$error_handler "$error_code"
fi
}
# Usage
error_handler() {
echo "Error occurred with code: $1"
}
try divide 10 0
catch error_handler
# Guard clause pattern
process_user() {
local username="$1"
local password="$2"
# Guard clauses
[ -n "$username" ] || { echo "Username required" >&2; return 1; }
[ -n "$password" ] || { echo "Password required" >&2; return 1; }
[ ${#password} -ge 8 ] || { echo "Password too short" >&2; return 1; }
# Main logic
echo "Processing user: $username"
return 0
}
# Retry pattern
with_retry() {
local max_attempts="${1:-3}"
local delay="${2:-1}"
shift 2
local attempt=1
while [ $attempt -le $max_attempts ]; do
if "$@"; then
return 0
fi
echo "Attempt $attempt failed, retrying in ${delay}s..." >&2
sleep $delay
attempt=$((attempt + 1))
delay=$((delay * 2)) # Exponential backoff
done
echo "All $max_attempts attempts failed" >&2
return 1
}
# Usage
with_retry 3 1 curl -f http://example.com/api
12. Command Summary and Cheat Sheet
Quick Reference
# Function declaration
function name { commands; }
name() { commands; }
# Function arguments
$1, $2, ... # Individual arguments
$@ # All arguments as array
$* # All arguments as string
$# # Number of arguments
# Return values
return n # Return exit code (0-255)
echo # Return data (capture with $(function))
# Variable scope
local var # Local variable
global var # Global variable (default)
readonly var # Read-only variable
# Function information
declare -f function # Show function definition
declare -F function # List function names
type function # Show function type
Common Patterns
| Pattern | Code |
|---|---|
| Simple function | greet() { echo "Hello"; } |
| With parameters | greet() { echo "Hi $1"; } |
| Return value | get_name() { echo "John"; } |
| Return code | check() { [ -f "$1" ]; } |
| Default values | func() { local val="${1:-default}"; } |
| Local variables | func() { local temp="$1"; } |
| Parse options | while getopts "ab:" opt; do |
Best Practices Summary
- Always use
localfor function variables - Document functions with comments
- Return meaningful exit codes
- Use
echoto return data - Keep functions focused (single responsibility)
- Validate input parameters
- Handle errors gracefully
- Use libraries for reusable functions
- Avoid side effects when possible
- Test functions thoroughly
Conclusion
Bash functions are powerful tools for creating modular, reusable, and maintainable scripts:
Key Takeaways
- Modularity: Break complex scripts into manageable pieces
- Reusability: Write once, use many times
- Scope Control: Local variables prevent naming conflicts
- Return Values: Use echo for data, return for status
- Arguments: Access via $1, $2, etc., similar to scripts
- Libraries: Create function libraries for common tasks
- Error Handling: Implement robust error checking
- Performance: Be mindful of function call overhead
When to Use Functions
- Repeated code that appears multiple times
- Complex operations that deserve a name
- Logical units that do one thing well
- API boundaries between script components
- Library code shared across scripts
Functions are fundamental to writing professional, maintainable Bash scripts. Mastering them will significantly improve your scripting capabilities.