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.
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.
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 "$@"
fiThis 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.shLoad modules with source only when they are trusted executable shell 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 prodSome 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.
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.