Chapter 18Lesson 05~105 minutes

Dependency Checks, Version Gates, and Safe Degradation

A production script should know what environment it requires before it starts changing state. Dependency preflight and capability detection turn missing tools and incompatible versions into clear, actionable failures instead of mysterious mid-run errors.

IntermediateSecurity & portabilityHands-on lab

Learning objectives

By the end of this lesson

  • Fail early on required dependencies.
  • Distinguish optional from required capabilities.
  • Gate versions only when features require it.
  • Use feature detection where practical.
  • Degrade only when correctness and security are preserved.

1. Fail early on required dependencies

require_command() {
  command -v "$1" >/dev/null 2>&1 || {
    printf 'required command missing: %s\n' "$1" >&2
    return 69
  }
}

require_command curl || exit $?
require_command jq || exit $?

A clear preflight error is better than a later failure buried inside a complex pipeline.

2. Optional dependencies should change capability, not correctness

if command -v shellcheck >/dev/null 2>&1; then
  shellcheck scripts/*.sh
else
  printf 'warning: ShellCheck unavailable; static analysis skipped\n' >&2
fi

Safe degradation means the reduced mode still has a clearly defined contract.

3. Presence is not enough when flags or output changed

bash_major=${BASH_VERSINFO[0]}
bash_minor=${BASH_VERSINFO[1]}

if (( bash_major < 5 )); then
  printf 'Bash 5+ required; found %s\n' "$BASH_VERSION" >&2
  exit 69
fi

Only enforce a version gate when you actually depend on a feature introduced in that range.

4. Do not compare arbitrary version strings lexicographically

# Wrong idea:
# [[ "10.0" > "9.5" ]]

# Prefer a tool-specific version parser or explicit numeric fields.

Version formats vary by tool. Parse according to the tool's documented version scheme rather than inventing a generic string comparison.

5. Feature detection can be more robust than version detection

if wait -n 2>/dev/null; then
  :
fi

if help wait 2>/dev/null | grep -q -- '-n'; then
  have_wait_n=true
else
  have_wait_n=false
fi

When possible, test for the feature or flag itself.

6. Same command name can refer to different implementations

sed, date, nc, yq, and other commands may differ significantly across systems. A preflight can inspect version output or execute a small capability probe.

7. Degrade only when the fallback preserves the required property

if command -v flock >/dev/null 2>&1; then
  use_flock_lock
else
  use_mkdir_lock
fi

This is a valid fallback only if both locking implementations satisfy the same concurrency policy in your environment.

8. Never degrade a security property silently

If a required TLS verifier, signature checker, secret provider, or policy gate is missing, the safe fallback may be to stop rather than continue insecurely.

Fail closed for security-critical capability

A missing verification tool should not automatically become “skip verification.”

9. Preflight output should explain remediation

printf 'error: jq >= 1.6 required; install jq or use the supported container image\n' >&2

State what is missing, what capability requires it, and how the operator can restore a supported environment.

10. A pinned tool image can simplify dependency control

For CI, a versioned container image or managed runner image can provide a stable Bash/toolchain baseline. That trades local dependency checks for image maintenance and supply-chain controls.

11. Test minimum and preferred environments

If your script supports Bash 4.4 through 5.x or both GNU and BSD userlands, include representative CI jobs. A support claim that is never tested tends to drift into fiction.

12. Hands-on lab: capability-aware preflight

mkdir -p "$HOME/devops-academy/bash/chapter18/lesson05"
cd "$HOME/devops-academy/bash/chapter18/lesson05"

cat > preflight.sh <<'EOF'
#!/usr/bin/env bash
set -u

require() {
  command -v "$1" >/dev/null 2>&1 || {
    printf 'missing required command: %s\n' "$1" >&2
    return 69
  }
}

require bash || exit $?
require printf || exit $?

if command -v jq >/dev/null 2>&1; then
  jq_mode=available
else
  jq_mode=unavailable
fi

if help wait 2>/dev/null | grep -q -- '-n'; then
  wait_n=yes
else
  wait_n=no
fi

printf 'bash=%s\n' "$BASH_VERSION"
printf 'jq=%s\n' "$jq_mode"
printf 'wait_n=%s\n' "$wait_n"
EOF

chmod u+x preflight.sh
bash preflight.sh

Verification checklist

13. Knowledge check

Question 1. When should a dependency be required rather than optional?

Question 2. Why is lexicographic version comparison dangerous?

Question 3. What is often better than version gating?

Question 4. Should a missing security verifier degrade to no verification?

14. Summary

Production shell scripts should preflight required tools, detect optional capabilities explicitly, gate versions only when needed, prefer feature detection where possible, and degrade only when the fallback preserves the original correctness and security contract.

15. Further reading

  • GNU Bash Reference Manual — BASH_VERSINFO.
  • ShellCheck portability documentation.
  • Tool-specific version and compatibility documentation.
  • Security engineering guidance on fail-open versus fail-closed behavior.
Next lesson

Avoiding Needless Processes and Useless Pipelines

Chapter 19 will focus on shell performance, streaming, profiling, bounded parallelism, and deciding when Bash should give way to another language.

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.