Chapter 05Lesson 05~75 minutes

Breaking Large Scripts into Reusable Components

Once a shell script grows, architecture matters more than clever syntax. Reliable Bash programs separate orchestration, validation, adapters, reusable utilities, and configuration so that each part has a narrow responsibility.

BeginnerLoops & functionsHands-on lab

Learning objectives

By the end of this lesson

  • Decompose scripts by responsibility.
  • Separate orchestration from reusable logic.
  • Source trusted modules using script-relative paths.
  • Use a main function with a direct-execution guard.
  • Recognize when another implementation language is more appropriate.

1. Decompose by responsibility, not line count

Useful boundaries include input parsing, validation, external-system adapters, domain operations, logging, and top-level orchestration.

Script component architecture
flowchart TB
  M["main orchestration"] --> V["validation"]
  M --> A["artifact operations"]
  M --> D["deployment operations"]
  M --> L["logging"]
  A --> X["external CLIs"]
  D --> X

2. Put the workflow in a main function

main() {
  parse_args "$@"
  validate_inputs
  prepare_workspace
  deploy
  verify
}

main "$@"

This gives readers a concise execution narrative before they inspect implementation details.

3. Guard direct execution when files may also be sourced

main() {
  printf 'running main\n'
}

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
  main "$@"
fi

This prevents the main workflow from running when a test or another script sources the file for its function definitions.

4. Small trusted libraries can hold shared functions

project/
├── bin/
│   └── deploy
└── lib/
    ├── logging.sh
    ├── validation.sh
    └── artifacts.sh

Load modules with source only when they are trusted executable shell code.

Source executes code

A sourced file can change variables, functions, traps, options, and the working directory in the current shell.

5. Resolve module paths relative to the script file

script_dir=$(
  cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 &&
  pwd -P
) || exit 1

source "$script_dir/../lib/validation.sh"

Using ./lib/validation.sh incorrectly assumes the caller launched the script from a particular working directory.

6. Separate configuration from executable logic

Environment variables, command-line arguments, and structured data files are usually clearer configuration interfaces than arbitrary sourced shell fragments.

: "${DEPLOY_ENV:=staging}"
: "${REPLICA_COUNT:=2}"

[[ $REPLICA_COUNT =~ ^[0-9]+$ ]] || {
  printf 'invalid REPLICA_COUNT\n' >&2
  exit 64
}

7. Document component interfaces

For each function or module, define the arguments it accepts, environment it reads, stdout data it emits, stderr diagnostics, return statuses, and side effects. These are the internal APIs of your shell program.

8. Minimize shared mutable globals

# Hidden dependency:
ENVIRONMENT=prod

# Explicit dependency:
deploy() {
  local environment=$1
  printf 'deploying to %s\n' "$environment"
}

deploy prod

Some immutable metadata may remain global, but state that changes across functions creates coupling and test-order problems.

9. Wrap external CLIs at meaningful boundaries

kube_get_deployment() {
  local namespace=$1
  local name=$2
  kubectl -n "$namespace" get deployment "$name" -o json
}

deployment_ready() {
  local namespace=$1
  local name=$2
  kube_get_deployment "$namespace" "$name" |
    jq -e '.status.availableReplicas >= .spec.replicas' >/dev/null
}

Centralizing external calls makes testing, error handling, and future tool changes easier.

10. Structure should make tests possible

# deploy.sh
validate_environment() {
  [[ $1 == dev || $1 == staging || $1 == prod ]]
}

main() { :; }

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
  main "$@"
fi

# A test can source deploy.sh and call validate_environment directly.

Later chapters introduce Bats and broader shell testing practices.

11. Know when Bash has outgrown the problem

Consider Python, Go, or another language when the program needs substantial nested structured data, concurrency, complex error types, long-lived state, sophisticated algorithms, or cross-platform behavior beyond shell environments.

Language choice is architecture

Bash is strongest as orchestration and glue around operating-system and command-line interfaces. Refactoring cannot remove the language's fundamental constraints.

12. Hands-on lab: build a componentized mini project

mkdir -p "$HOME/devops-academy/bash/chapter05/lesson05/project"/{bin,lib}
cd "$HOME/devops-academy/bash/chapter05/lesson05/project"

cat > lib/validation.sh <<'EOF'
validate_environment() {
  local environment=${1:-}
  [[ $environment == dev || $environment == staging || $environment == prod ]]
}
EOF

cat > lib/reporting.sh <<'EOF'
report_plan() {
  local service=$1
  local environment=$2
  printf 'PLAN service=%s env=%s\n' "$service" "$environment"
}
EOF

cat > bin/deploy <<'EOF'
#!/usr/bin/env bash

script_dir=$(
  cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 &&
  pwd -P
) || exit 1

source "$script_dir/../lib/validation.sh"
source "$script_dir/../lib/reporting.sh"

main() {
  local service=${1:-api}
  local environment=${2:-staging}

  if ! validate_environment "$environment"; then
    printf 'invalid environment: %s\n' "$environment" >&2
    return 2
  fi

  report_plan "$service" "$environment"
}

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
  main "$@"
fi
EOF

chmod u+x bin/deploy
./bin/deploy api staging

(
  cd /tmp
  "$HOME/devops-academy/bash/chapter05/lesson05/project/bin/deploy" worker prod
)

Verification checklist

13. Knowledge check

Question 1. Why put orchestration in main?

Question 2. Why derive module paths from BASH_SOURCE?

Question 3. What does a direct-execution guard enable?

Question 4. When should you consider moving beyond Bash?

14. Summary

Large Bash programs become maintainable by separating orchestration, validation, adapters, and utilities; resolving modules relative to the script; minimizing global state; and defining clear internal interfaces. Good structure also reveals when the problem should move to another language.

15. Further reading

  • GNU Bash Reference Manual — Shell Functions and BASH_SOURCE.
  • ShellCheck documentation — sourced files and path handling.
  • Bats-core documentation — shell testing.
  • Google Shell Style Guide — function and script organization.
Next lesson

Positional Parameters and Special Bash Parameters

Chapter 6 will turn reusable logic into command-line interfaces with positional arguments, shifting, option parsing, help text, and validation.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.