Ports generic-scripts-module-v1 into a routed skill: module anatomy, shared loggers/base modules, Taskfile integration, workflows, and pattern decisions. Fixes a latent defect in the loggers example that silenced all logging in the module scaffold: loggers/lib/--index-api.sh now sources --env-vars-reader.sh, and the reader preserves already-exported values so caller overrides still win. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.7 KiB
6.7 KiB
Module Anatomy
The four file types inside lib/, the api/ wrapper, and the patterns each follows.
Read this when writing or changing a reader, a validator, an implementation function, or a public wrapper.
1. --env-vars-reader.sh — defaults and CLI parsing
Responsibilities, in order:
- export defaults, preserving any value already exported by the caller;
- parse CLI flags into environment variables;
- restore the positional arguments so later consumers still see them.
#!/bin/bash
export MODULE_EXAMPLE_V1_NAME="${MODULE_EXAMPLE_V1_NAME:-default-name}"
export MODULE_EXAMPLE_V1_OUTPUT_DIR="${MODULE_EXAMPLE_V1_OUTPUT_DIR:-.tmp/module-example-v1}"
export MODULE_EXAMPLE_V1_DRY_RUN="${MODULE_EXAMPLE_V1_DRY_RUN:-false}"
ALL_ARGS=("$@")
while [[ "$#" -gt 0 ]]; do
case $1 in
--name)
export MODULE_EXAMPLE_V1_NAME="$2"
shift
;;
--output-dir)
export MODULE_EXAMPLE_V1_OUTPUT_DIR="$2"
shift
;;
--dry-run)
export MODULE_EXAMPLE_V1_DRY_RUN="true"
;;
*) ;;
esac
shift
done
set -- "${ALL_ARGS[@]}"
Rules:
- Use a stable uppercase environment prefix per module.
- Always use
${VAR:-default}, never a bareexport VAR=value. A bare assignment silently discards a caller's override, and the caller has no way to detect it. This is what makes a reader safe to source more than once. - Prefer explicit CLI flags over positional arguments.
- Defaults must be safe to run locally — a default that writes outside
.tmp/or touches a shared system is not a safe default. - Never print secrets here.
The ALL_ARGS save/restore matters because shift consumes the arguments. Without the
set -- at the end, anything sourced after the reader sees an empty argument list.
2. --env-vars-validator.sh — required inputs
#!/bin/bash
. ./.scripts/loggers/lib/--index-api.sh
if [ -z "${MODULE_EXAMPLE_V1_NAME}" ]; then
_loggers_error "module-example-v1" "Missing required --name / MODULE_EXAMPLE_V1_NAME"
exit 1
fi
Rules:
- Runs from
--index.sh, after the reader and before the API index, so failures happen before any implementation function is even defined. - Use
_loggers_errorfor the failure and_loggers_infofor a usage hint. exit 1is correct here — the validator runs during bootstrap of a top-level command, and stopping is the intent.- Validate only what every command in the module needs. A variable that only one command requires is validated inside that command's function, not globally — otherwise unrelated commands fail on input they never use.
3. --index-api.sh — dependency and implementation wiring
#!/bin/bash
. ./.scripts/loggers/lib/--index-api.sh
. ./.scripts/base/lib/--index-api.sh
. ./.scripts/module-example-v1/lib/-initialize-one.sh
Rules:
- Upstream dependencies first, then this module's own implementation files.
- Source other modules'
--index-api.sh, not their--index.sh— seereferences/shared-modules.md§2. - Every implementation file must appear here. A
lib/-*.shthat is never sourced defines nothing, and the failure surfaces ascommand not foundfor a function you can see on disk. - Safe to source repeatedly. Sourcing defines functions; it should have no other side effect.
4. --index.sh — runtime bootstrap
#!/bin/bash
# --> passed parameters are read & exported environment variables
. ./.scripts/module-example-v1/lib/--env-vars-reader.sh
# --> required environment variables are validated for existence
. ./.scripts/module-example-v1/lib/--env-vars-validator.sh
# --> available functions are imported/exported
. ./.scripts/module-example-v1/lib/--index-api.sh
Rules:
- Exactly these three sources, in exactly this order.
- This is the module's own entrypoint, sourced by its
api/wrappers. Other modules never source it — they take--index-api.sh. - A module with no required inputs may omit the validator, but keeping an empty one makes the next required variable a one-line change.
5. Implementation functions — lib/-<command>.sh
#!/bin/bash
. ./.scripts/loggers/lib/--index-api.sh
_moduleExampleV1_initializeOne() {
local FUNCTION_NAME="_moduleExampleV1_initializeOne"
_loggers_info "${FUNCTION_NAME}" "Initializing module-example-v1"
_loggers_debug "${FUNCTION_NAME}" "Output directory: ${MODULE_EXAMPLE_V1_OUTPUT_DIR}"
if [ "${MODULE_EXAMPLE_V1_DRY_RUN}" = "true" ]; then
_loggers_info "${FUNCTION_NAME}" "Dry run enabled; no files will be changed."
return 0
fi
mkdir -p "${MODULE_EXAMPLE_V1_OUTPUT_DIR}"
_loggers_info "${FUNCTION_NAME}" "Initialization complete."
}
Rules:
- Open with
local FUNCTION_NAME="_<module>_<function>"— every logger call takes it as the first argument, which is what makes log output traceable to a specific function. - One file, one primary function. Small private helpers may live alongside it.
- Use
_loggers_*helpers, never rawecho, for status and errors. returnnon-zero on failure; neverexit. The file is sourced, soexitterminates the caller's shell — including an interactive one.- Read configuration from the module's environment variables. Never re-parse
$@here. - Quote every expansion unless word splitting is deliberate.
- Never expose secret values in a log line.
6. api/ wrappers
#!/bin/bash
. ./.scripts/module-example-v1/lib/--index.sh
_moduleExampleV1_initializeOne
That is the whole file. Rules:
- Source exactly
lib/--index.sh, then call exactly one function. - No implementation, no flag parsing, no validation, no argument forwarding.
- Arguments reach the reader automatically: sourcing happens while the wrapper's own
$@is still in scope, so--env-vars-reader.shsees them without being passed anything. - Create a wrapper only for a genuinely user-facing command. An internal helper stays in
lib/— every file inapi/is a public interface someone may come to depend on.
7. Diagnosing a broken module
| Symptom | Cause |
|---|---|
command not found for a function that exists on disk |
Its file is missing from lib/--index-api.sh |
| No log output at all, exit code 0 | Logger level flags never exported — see references/shared-modules.md §3 |
| A flag is ignored | Not handled in --env-vars-reader.sh, or handled after the set -- restore |
| Later sources see no arguments | Reader is missing the ALL_ARGS save/set -- restore |
No such file or directory for a lib/ path |
Run from inside the module instead of the repository root |
.env loaded unexpectedly |
Something sourced base/lib/--index.sh instead of base/lib/--index-api.sh |
| The shell exits on a handled error | exit used inside a sourced implementation function |