src/bx_skills/catalog_skills/bx-bash-clean-architecture/SKILL.md
Use when structuring Bash 4.3+ scripts or multi-file projects with clean architecture, reviewing layer dependency violations in shell scripts, deciding where functions belong across domain, application, adapter, and composition layers, or setting up ports and dependency inversion in bash
npx skillsauth add bitranox/bx_skills bx-bash-clean-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Framework-agnostic, structured Bash architecture optimized for change, testability, and clear boundaries. Inner layers never call outer layer functions directly. Domain stays pure (no I/O, no external commands).
Target: Bash >=4.3 | set -euo pipefail | Pure domain | Tool-agnostic core | No global mutable state in domain
When NOT to use:
rsync wrappers, etc.)script-mode.md)Inner layers never call outer layer functions directly. Dependencies point inward only.
| Layer | Contains | Rules |
|----------------------|---------------------------------------------------------------------|------------------------------------------------------------------------|
| Domain | Pure functions: validation, computation, transformation | No I/O, no external commands, no echo to terminal, no env mutation |
| Application | Use case functions, port contracts (documented function signatures) | Orchestrates domain + ports; receives port function names as arguments |
| Adapters | I/O functions: file, network, system, CLI parsing | Implements port contracts; maps external data to/from domain format |
| Composition Root | main() wiring | Binds adapters to ports; entry point |
| Principle | Rule |
|-----------|-----------------------------------------------------------------------------------------------------------------|
| SRP | One responsibility per function; split by use case, never utils.sh grab-bags |
| OCP | Extend by adding new adapter functions, not editing core logic |
| LSP | All adapters implementing a port contract must accept the same args and return the same format |
| ISP | Port contracts are narrow: one function per I/O concern |
| DIP | Core functions receive I/O function names as parameters; never hardcode external commands in domain/application |
CLI args -> validate (adapter boundary) -> domain functions (pure) -> format output (adapter) -> stdout/file
(raw) (composition/adapter) (no I/O, no side effects) (adapter boundary) (external)
| Use Case | Mechanism | Notes |
|------------------------|----------------------------------------------|------------------------------------|
| Function return values | stdout capture: result=$(fn args) | Primary data passing mechanism |
| Structured data | Associative arrays (declare -A) | Bash 4.0+; pass by nameref |
| Complex returns | Multiple lines on stdout, one field per line | Parse with read or mapfile |
| Error signaling | Return code + stderr | return 1 with echo "error" >&2 |
| Cross-function data | Nameref: declare -n ref="$1" | Bash 4.3+; avoids globals |
Never: Use global variables for data flow between layers. Globals acceptable only for: constants (readonly), configuration set once in composition root, and trap cleanup state.
Bash has no interfaces. Ports are documented function signatures that adapters must implement. Pass the function name to use cases.
# --- PORT CONTRACT ---
# port: read_config
# args: $1 = config file path
# stdout: key=value lines (one per line)
# return: 0 on success, 1 on not found, 2 on parse error
# --- ADAPTER (implements port) ---
adapter__read_config_file() {
local file="$1"
[[ -f "$file" ]] || return 1
grep -v '^#' "$file" | grep -v '^$'
}
# --- USE CASE (receives port function name) ---
uc__load_settings() {
local read_config_fn="$1"
local config_path="$2"
local raw
raw=$("$read_config_fn" "$config_path") || return 1
# Call domain function (pure)
domain__parse_settings "$raw"
}
# --- COMPOSITION ---
main() {
uc__load_settings adapter__read_config_file "/etc/myapp.conf"
}
# Use case depends on abstraction (function name parameter), not concretion
uc__process_data() {
local fetch_fn="$1" # port: fetch data
local store_fn="$2" # port: store result
local input="$3"
local raw_data
raw_data=$("$fetch_fn" "$input") || return 1
local result
result=$(domain__transform "$raw_data")
"$store_fn" "$result"
}
# Composition wires concrete adapters
main() {
uc__process_data \
adapter__fetch_from_api \
adapter__store_to_file \
"$1"
}
# Domain function populates associative array via nameref
domain__parse_record() {
local input="$1"
declare -n _out="$2" # nameref to caller's associative array
local key="${input%%:*}"
local val="${input#*:}"
_out[name]="$key"
_out[value]="$val"
# No I/O, no external commands — pure parameter expansion
}
# Caller
declare -A record
domain__parse_record "host:example.com" record
echo "${record[name]}" # "host" -- I/O happens in adapter/composition
| Layer | Style |
|-------------|---------------------------------------------------------------------|
| Domain | return 1 + descriptive stderr: echo "error: invalid format" >&2 |
| Application | Propagate domain return codes; add context |
| Adapters | Catch external failures, map to domain error codes |
| Composition | Map to exit codes; final stderr formatting |
# Domain (pure validation)
domain__validate_port() {
local port="$1"
[[ "$port" =~ ^[0-9]+$ ]] || { echo "error: port must be numeric" >&2; return 1; }
(( port >= 1 && port <= 65535 )) || { echo "error: port out of range" >&2; return 1; }
echo "$port" # return validated value
}
# Adapter (catches external failure)
adapter__check_port_open() {
local host="$1" port="$2"
timeout 5 bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null || return 1
}
| Code | Meaning | |-----:|----------------------------------------| | 0 | Success | | 1 | General error | | 2 | Invalid input / usage error | | 3 | Not found | | 4 | Conflict / precondition fail | | 70 | Unexpected internal error | | 124 | Timeout | | 126 | Permission denied | | 127 | Command not found (dependency missing) |
| Layer | Prefix | Example |
|-------------------------|---------------------|------------------------------------------------------|
| Domain | domain__ | domain__validate_email, domain__compute_checksum |
| Application (use cases) | uc__ | uc__deploy_service, uc__backup_database |
| Adapters | adapter__ | adapter__read_file, adapter__call_api |
| Composition/main | main, compose__ | main, compose__wire_production |
| Constants | UPPER_SNAKE | readonly MAX_RETRIES=3 |
Double underscore separates namespace from function name. Prevents collision with system commands.
project/
bin/
my-tool # Entry point (sources lib, calls main)
lib/
domain.sh # Pure functions: validation, transformation, computation
application.sh # Use cases: orchestration via port function references
ports.sh # Port contract documentation (comments only)
adapters/
file.sh # File I/O adapter functions
api.sh # HTTP/API adapter functions
system.sh # System command adapter functions
compose.sh # Wiring: bind adapters to ports, call use cases
tests/
test_domain.sh # Unit tests for domain (no mocking needed)
test_application.sh # Use case tests with stub adapters
test_integration.sh # Real adapters, real I/O
For single-file scripts, use comment-based section markers. See script-mode.md.
Guardrails: domain.sh sources nothing and calls no external commands. application.sh sources domain.sh only. adapters/*.sh implement port contracts. compose.sh sources everything and wires.
Source guards (prevent double-sourcing in multi-file projects):
# At the top of each library file (e.g., domain.sh)
[[ -n "${_DOMAIN_SH_LOADED:-}" ]] && return 0
readonly _DOMAIN_SH_LOADED=1
| Type | Purpose | |-----------------|------------------------------------------------------------------------------| | Unit | Domain functions with direct calls (no mocking needed — they're pure) | | Stub | Use cases with stub adapter functions (bash functions that return test data) | | Integration | Real adapters against real files/services | | E2E | Full script execution with known inputs/outputs |
# Stub adapter (replaces real I/O for testing)
stub__read_config() {
echo "host=localhost"
echo "port=8080"
}
# Test use case with stub
test_load_settings() {
local result
result=$(uc__load_settings stub__read_config "/fake/path")
[[ "$result" == *"localhost"* ]] || { echo "FAIL: expected localhost"; return 1; }
echo "PASS: test_load_settings"
}
log_info "msg" >&2 (never to stdout — that's for data)TRACE_ID via environment variable or global (set once in composition root)PS4='+ ${BASH_SOURCE}:${LINENO}: ' with set -x for debug tracingecho to terminal (only stdout for return values)set -euo pipefail at script toptrap in composition root (not scattered)domain__, uc__, adapter__)shellcheck passes with no warnings (run: shellcheck -x script.sh)| Mode | Output | Reference |
|--------------|------------------------------------------------------|----------------------------|
| GENERATE | Full project with domain/app/adapter/compose + tests | See canonical-example.md |
| REVIEW | Violations + fix checklist | See review-checklists.md |
| SCRIPT | Single file with logical layer sections | See script-mode.md |
domain__ functionsadapter__ functionsuc__ use case functionsmain() as composition root: parse args, wire adapters, call use casesset -euo pipefail and structured exit codes| Mistake | Fix |
|--------------------------------------------------|---------------------------------------------------------------------------|
| Calling curl/grep/find in domain functions | Domain is pure; wrap I/O commands in adapter functions |
| Using global variables for data flow | Use stdout + capture, or namerefs (declare -n) |
| Logging to stdout | Stdout is for data; log to stderr (>&2) |
| Hardcoding file paths in use cases | Pass paths as parameters; set defaults in composition root |
| Mixing argument parsing with business logic | Parse in adapter/composition; pass validated values to use cases |
| No set -euo pipefail | Always set at script top; handle expected failures with \| true or if |
| Random exit codes | Use structured exit code table; map in composition root |
| trap cleanup in random functions | Single trap in composition root; adapters provide cleanup functions |
| Sourcing everything at top level | Source only what each layer needs; domain sources nothing |
| eval for dynamic dispatch | Use "$fn_name" args (indirect call) — safe, no eval needed |
| File | Content |
|------------------------|--------------------------------------------------------------------------|
| script-mode.md | Single-file scripts: logical layer sections, exit codes, one-file layout |
| canonical-example.md | Complete Service Health Check example (domain through composition) |
| review-checklists.md | All review checklists for REVIEW mode output |
| Term | Definition |
|------------------------|---------------------------------------------------------------------------------------------|
| Adapter | Bash function performing I/O (file, network, system commands), implementing a port contract |
| Application Layer | Use case functions orchestrating domain + port calls (no direct I/O) |
| Composition Root | main() function: parses args, wires adapters to use cases, sets traps |
| Domain | Pure bash functions: validation, computation, transformation (no I/O, no external commands) |
| Nameref | declare -n ref="$1" — Bash 4.3+ mechanism for passing structured data without globals |
| Port | Documented function signature contract that adapters must implement |
| Function Reference | Passing a function name as a string argument for indirect call: "$fn_name" args |
development
Textual TUI framework documentation reference
testing
Use when creating new skills, editing existing skills, structuring SKILL.md files, writing skill frontmatter, testing skills with subagents, deploying skills, or verifying skills work before deployment
development
Use when building transparent remote procedure calls, distributed computing, or remote object proxying in Python with RPyC. Use when asked about rpyc.connect, rpyc.Service, netref proxies, async_(), BgServingThread, SSLAuthenticator, DeployedServer, or rpyc_classic.py.
tools
Use when choosing Python libraries for a task, when writing new Python code that needs dependencies, when reviewing Python imports for non-preferred libraries, or when unsure which library to use for JSON, HTTP, logging, TOML, YAML, compression, database access, testing, or CLI tools