📚 Library-Style Shell Scripting Patterns
Library-style shell scripting promotes code reuse, modularity, and maintainability by organizing functionality into reusable components. This pattern treats shell scripts as libraries rather than monolithic programs.
🎯 Core Principles
Modularity and Reusability
Structure code as independent, composable 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
30 | # ❌ Monolithic script
#!/bin/bash
# do-everything.sh
main() {
# 500 lines of mixed functionality
}
# ✅ Library-style approach
# lib/network.sh
network_is_reachable() {
local host="${1:-google.com}"
ping -c 1 -W 5 "$host" >/dev/null 2>&1
}
# lib/filesystem.sh
ensure_directory() {
local dir="$1"
mkdir -p "$dir"
}
# main.sh
source lib/network.sh
source lib/filesystem.sh
main() {
if network_is_reachable; then
ensure_directory "/tmp/downloads"
# ... rest of logic
fi
}
|
Clear Function Contracts
Define explicit inputs, outputs, and behavior.
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 | # Document function contracts clearly
#
# download_file - Download file from URL to local path
#
# Arguments:
# $1 - URL to download
# $2 - Local destination path (optional, defaults to basename)
#
# Returns:
# 0 - Success
# 1 - Download failed
# 2 - Invalid arguments
#
# Exports:
# DOWNLOAD_FILE_LAST_STATUS - HTTP status code
#
download_file() {
local url="$1"
local dest="${2:-$(basename "$url")}"
# Validate arguments
if [ -z "$url" ]; then
echo "Error: URL required" >&2
return 2
fi
# Perform download
local http_status
http_status=$(curl -w "%{http_code}" -o "$dest" "$url" 2>/dev/null)
export DOWNLOAD_FILE_LAST_STATUS="$http_status"
# Check result
if [ "$http_status" -eq 200 ]; then
return 0
else
rm -f "$dest" # Cleanup on failure
return 1
fi
}
|
📁 Library Organization
Standard Directory Structure
Organize libraries with clear separation of concerns.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | lib/
├── core/
│ ├── logging.sh # Logging utilities
│ ├── validation.sh # Input validation
│ └── utils.sh # General utilities
├── system/
│ ├── process.sh # Process management
│ ├── filesystem.sh # File operations
│ └── network.sh # Network utilities
├── app/
│ ├── config.sh # Configuration management
│ ├── database.sh # Database operations
│ └── api.sh # API clients
└── compat/
├── bash.sh # Bash-specific features
└── posix.sh # POSIX compatibility
|
Library Loading Patterns
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 | # lib/core/loader.sh - Library loader
load_library() {
local lib_name="$1"
local lib_path="lib/${lib_name}.sh"
if [ -f "$lib_path" ]; then
source "$lib_path"
export LOADED_LIBRARIES="${LOADED_LIBRARIES}:$lib_name"
return 0
else
echo "Library not found: $lib_path" >&2
return 1
fi
}
# Prevent multiple loading
load_library_once() {
local lib_name="$1"
if [[ ":${LOADED_LIBRARIES}:" != *":${lib_name}:"* ]]; then
load_library "$lib_name"
fi
}
# Usage in scripts
load_library_once "core/logging"
load_library_once "system/network"
|
🔧 Function Design Patterns
Pure Functions
Functions without side effects that return consistent results.
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | # ✅ Pure function - no side effects
calculate_disk_usage_percent() {
local path="$1"
local total used
read -r total used < <(df "$path" | awk 'NR==2 {print $2, $3}')
echo $((used * 100 / total))
}
# ✅ Another pure function
generate_random_string() {
local length="${1:-32}"
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w "$length" | head -n 1
}
|
Side Effect Functions
Functions that interact with the system but follow consistent patterns.
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 | # Side effect function with clear contract
create_backup() {
local source="$1"
local backup_dir="${2:-"/backups"}"
local timestamp
local backup_path
# Validate inputs
if [ ! -e "$source" ]; then
echo "Source does not exist: $source" >&2
return 1
fi
# Generate backup name
timestamp=$(date +%Y%m%d_%H%M%S)
backup_path="$backup_dir/$(basename "$source").$timestamp.tar.gz"
# Ensure backup directory exists
mkdir -p "$backup_dir"
# Create backup
if tar -czf "$backup_path" "$source" 2>/dev/null; then
echo "$backup_path"
return 0
else
echo "Backup failed" >&2
rm -f "$backup_path" # Cleanup on failure
return 1
fi
}
|
🎨 Advanced Library Features
Namespacing
Prevent function name collisions with namespaces.
1
2
3
4
5
6
7
8
9
10
11
12
13 | # lib/database/postgres.sh
postgres_connect() { ... }
postgres_query() { ... }
postgres_disconnect() { ... }
# lib/database/mysql.sh
mysql_connect() { ... }
mysql_query() { ... }
mysql_disconnect() { ... }
# Usage
postgres_connect "localhost" "mydb"
mysql_connect "localhost" "mydb"
|
Error Handling Conventions
Consistent error handling across library functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | # lib/core/errors.sh
ERROR_INVALID_ARGS=1
ERROR_PERMISSION_DENIED=2
ERROR_NETWORK_FAILURE=3
ERROR_RESOURCE_NOT_FOUND=4
throw_error() {
local error_code="$1"
local message="$2"
echo "Error $error_code: $message" >&2
return "$error_code"
}
# Usage in library functions
validate_required_arg() {
local arg_name="$1"
local arg_value="$2"
if [ -z "$arg_value" ]; then
throw_error "$ERROR_INVALID_ARGS" "Required argument '$arg_name' is missing"
fi
}
|
Configuration and State Management
Handle library configuration cleanly.
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 | # lib/core/config.sh
LIBRARY_CONFIG_DEFAULTS=(
["LOG_LEVEL"]="INFO"
["DEBUG_MODE"]="false"
["CACHE_DIR"]="/tmp"
)
library_get_config() {
local key="$1"
local default="${LIBRARY_CONFIG_DEFAULTS[$key]:-}"
# Check environment variable first
local env_var="LIB_$(echo "$key" | tr '[:lower:]' '[:upper:]' | tr '-' '_')"
if [ -n "${!env_var}" ]; then
echo "${!env_var}"
return
fi
# Check library config
if [ -n "${LIBRARY_CONFIG[$key]:-}" ]; then
echo "${LIBRARY_CONFIG[$key]}"
return
fi
# Return default
echo "$default"
}
library_set_config() {
LIBRARY_CONFIG["$1"]="$2"
}
|
🧪 Testing and Documentation
Embedded Documentation
Self-documenting library 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 | # lib/system/process.sh
# kill_process_tree - Kill process and all its children
#
# Usage:
# kill_process_tree <pid> [signal]
#
# Arguments:
# pid - Process ID to kill
# signal - Signal to send (default: TERM)
#
# Examples:
# kill_process_tree 1234
# kill_process_tree 1234 KILL
#
# Notes:
# - Uses pgrep to find child processes
# - Graceful termination attempted first
#
kill_process_tree() {
local pid="$1"
local signal="${2:-TERM}"
# Implementation here...
}
|
Unit Test Integration
Structure libraries for easy testing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | # lib/math/calculations.sh
math_add() {
echo $(($1 + $2))
}
math_multiply() {
echo $(($1 * $2))
}
# Self-test capability
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
# Run tests when executed directly
echo "Testing math functions..."
test_add() {
local result=$(math_add 2 3)
[ "$result" -eq 5 ] && echo "✓ add test passed" || echo "✗ add test failed"
}
test_add
fi
|
🧾 Summary Best Practices
Library Design Guidelines
✅ Use clear, descriptive function names
✅ Document function contracts thoroughly
✅ Separate pure functions from side-effect functions
✅ Implement consistent error handling
✅ Provide configuration flexibility
✅ Support testing and debugging
Organization Principles
- Single Responsibility: Each library has one clear purpose
- Loose Coupling: Libraries minimize dependencies on each other
- High Cohesion: Related functions grouped together
- Explicit Interfaces: Clear public APIs
- Version Management: Track library versions
Quality Assurance
✅ Write self-contained, testable functions
✅ Include usage examples in documentation
✅ Handle edge cases gracefully
✅ Provide meaningful error messages
✅ Maintain backward compatibility
🧠 Template Library Structure
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 | # lib/template.sh - Library template
# Library metadata
LIBRARY_NAME="template"
LIBRARY_VERSION="1.0.0"
LIBRARY_DESCRIPTION="Template library for new modules"
# Private variables (prefixed with underscore)
_TEMPLATE_PRIVATE_VAR="internal_value"
# Public functions
template_hello() {
local name="${1:-World}"
echo "Hello, $name!"
}
template_version() {
echo "$LIBRARY_VERSION"
}
# Private functions (prefixed with underscore)
_template_internal_helper() {
# Internal implementation details
:
}
# Self-test section
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "Running $LIBRARY_NAME tests..."
# Test implementations here
fi
|
🧾 See Also