Przejdź do treści

📚 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

  1. Single Responsibility: Each library has one clear purpose
  2. Loose Coupling: Libraries minimize dependencies on each other
  3. High Cohesion: Related functions grouped together
  4. Explicit Interfaces: Clear public APIs
  5. 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