Designing Usage, Help, and Validation
Parsing only tells you what tokens the user supplied. A usable CLI must also explain its grammar, validate values, report errors on the correct stream, document defaults, and make safety behavior predictable.
Learning objectives
By the end of this lesson
- Separate usage from full help.
- Layer syntax, semantic, and prerequisite validation.
- Write actionable diagnostics.
- Keep help and errors on appropriate streams.
- Design trustworthy dry-run and production safeguards.
1. Usage and help serve different purposes
usage() {
printf 'usage: %s [-n] [-e ENV] [-r N] SERVICE\n' "$0"
}
help_text() {
usage
cat <<'EOF'
Plan or execute a service deployment.
Options:
-n dry run
-e ENV dev, staging, or prod
-r N replicas
-h help
EOF
}2. Intentional help belongs on stdout
if [[ $want_help == true ]]; then
help_text
exit 0
fi
if [[ -z $service ]]; then
printf 'error: SERVICE is required\n' >&2
usage >&2
exit 64
fiAutomation can then distinguish successful help output from a failed invocation.
3. Validate in layers
flowchart TD A["parse tokens"] --> B["required values"] B --> C["syntax/type"] C --> D["semantic policy"] D --> E["dependencies"] E --> F["mutation"]
Layered validation produces precise diagnostics and keeps dangerous work behind read-only checks.
4. Validate representation before arithmetic
if [[ ! $replicas =~ ^[0-9]+$ ]]; then
printf 'error: replicas must be decimal digits\n' >&2
exit 64
fi
replicas=$((10#$replicas))
if (( replicas < 1 || replicas > 100 )); then
printf 'error: replicas must be 1..100\n' >&2
exit 65
fi5. Semantic policy is separate from parsing
case $environment in
dev|staging|prod) ;;
*) printf 'error: unsupported environment\n' >&2; exit 65 ;;
esac
if [[ $environment == prod && $tag == latest ]]; then
printf 'error: production requires immutable tag\n' >&2
exit 65
fi6. Check dependencies before state changes
require_command() {
local name=$1
command -v "$name" >/dev/null 2>&1 || {
printf 'error: missing dependency: %s\n' "$name" >&2
return 69
}
}Only require a dependency when the selected command actually needs it; --help should not fail because deployment tooling is absent.
7. Error messages should tell users what to fix
# Weak:
printf 'invalid input\n' >&2
# Better:
printf 'error: --replicas expects 1..100; received %q\n' \
"$replicas" >&28. Keep exit-code contracts small and stable
# Example convention:
# 0 success
# 64 invocation error
# 65 invalid domain value / policy rejection
# 69 unavailable dependency
# 1 operational failureA descriptive stderr message is usually more useful than dozens of granular numeric statuses.
9. Dry-run must actually avoid mutation
run_or_plan() {
if [[ $dry_run == true ]]; then
printf 'DRY-RUN:'
printf ' %q' "$@"
printf '\n'
else
"$@"
fi
}Do not call a mode dry-run if it still changes remote state, writes files unexpectedly, or triggers irreversible API calls.
10. Production safety cannot rely only on prompts
if [[ $environment == prod && $approved != yes ]]; then
printf 'error: production approval missing\n' >&2
exit 65
fiCI has no reliable interactive user. Enforce production policy through explicit configuration, approvals, or external authorization.
11. Help examples clarify grammar
Examples:
deployctl -e staging api
deployctl -n -e prod -r 3 payments
deployctl -hExamples should remain synchronized with the implementation.
12. Hands-on lab: polished help and validation
mkdir -p "$HOME/devops-academy/bash/chapter06/lesson04"
cd "$HOME/devops-academy/bash/chapter06/lesson04"
cat > deploy-help.sh <<'EOF'
#!/usr/bin/env bash
usage() { printf 'usage: %s [-n] [-e ENV] [-r N] SERVICE\n' "$0"; }
help_text() {
usage
cat <<'HELP'
Options:
-n dry run
-e ENV dev, staging, prod (default: staging)
-r N replicas 1..100 (default: 2)
-h help
HELP
}
dry_run=false
environment=staging
replicas=2
while getopts ':hne:r:' option; do
case $option in
h) help_text; exit 0 ;;
n) dry_run=true ;;
e) environment=$OPTARG ;;
r) replicas=$OPTARG ;;
:) printf 'error: -%s needs a value\n' "$OPTARG" >&2; usage >&2; exit 64 ;;
\?) printf 'error: unknown option -%s\n' "$OPTARG" >&2; usage >&2; exit 64 ;;
esac
done
shift "$((OPTIND - 1))"
service=${1:-}
[[ -n $service ]] || { printf 'error: SERVICE required\n' >&2; usage >&2; exit 64; }
case $environment in dev|staging|prod) ;; *) exit 65 ;; esac
[[ $replicas =~ ^[0-9]+$ ]] || exit 64
replicas=$((10#$replicas))
(( replicas >= 1 && replicas <= 100 )) || exit 65
printf 'PLAN service=%s env=%s replicas=%d dry_run=%s\n' \
"$service" "$environment" "$replicas" "$dry_run"
EOF
bash deploy-help.sh -h
bash deploy-help.sh -n -e prod -r 3 api
bash deploy-help.sh -r nope api || trueVerification checklist
13. Knowledge check
Question 1. Where should intentional help normally go?
Question 2. What is semantic validation?
Question 3. What guarantee should dry-run make?
Question 4. Why not rely only on confirmation prompts for production?
14. Summary
A predictable CLI documents its grammar, validates in layers, writes intentional help to stdout and errors to stderr, and makes safety features real behavioral guarantees.
15. Further reading
- POSIX Utility Syntax Guidelines.
- GNU Coding Standards — command-line interfaces.
- GNU Bash Reference Manual — getopts and exit status.
- Command Line Interface Guidelines (clig.dev).
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.