STD-027: Script Extraction
Every script longer than 5 lines in a standalone project partial MUST be extracted to the project’s scripts/ directory. Bash and Python scripts are referenced via pasteable run commands. PowerShell scripts use a deployer heredoc pattern with include:: to transfer the script to the target machine. Scripts are executable artifacts, not documentation fragments.
Principles
-
Scripts are executable artifacts. A script embedded in a partial cannot be
chmod +x, tested withshellcheck, run withstrace, or versioned independently. Extraction makes it a first-class citizen. -
One source, three roles. A script in
scripts/serves as an executable (bash script.sh), a sourceable function (source script.sh), and documentation content (include::{scriptsdir}/script.sh[]). Inline duplication breaks all three. -
The run command is the documentation. The book shows how to execute. The source lives in the file. Readers who want the source open the file — the book does not duplicate it.
-
Aliases are not portable. Shell aliases (
build-adoc,d001) do not exist in non-interactive bash. Scripts MUST resolve the real path via environment variables with sensible defaults.
Requirements
-
Script blocks exceeding 5 lines in any
partials/*.adocfile MUST be extracted toscripts/as standalone files (.sh,.ps1,.py). -
Every standalone project (
data/d00x/projects/) MUST have ascripts/directory. Empty is acceptable; absent is not. -
Scripts SHOULD be named by action:
setup-postgres.sh,dcr-inventory.ps1,build-enhanced-report.py— notscript1.shorutils.ps1. -
Bash/Python: the partial MUST reference the script with a pasteable run command only. No
include::of the script source. The run command is the interface — the source lives in the file.\[source,bash] \---- bash $(find data/d000/projects/<slug>/scripts -name 'script-name.sh') \---- -
PowerShell: the partial MUST use the deployer heredoc pattern —
@'…'@ | Set-Contentwithinclude::{scriptsdir}/<name>.ps1[]pulling the script content at build time. See PowerShell — Deployer Pattern. -
The run command MUST use
$(find)for paths. No$prompt glyph. -
Bash scripts MUST be executable (
chmod +x) and MUST begin with#!/usr/bin/env bashandset -euo pipefail. -
PowerShell scripts MUST be pure module code — no deployer wrapper inside the
.ps1file. -
Scripts that call
build-adocor any shell alias MUST resolve the real path via an environment variable with a default:BUILD_ADOC="${BUILD_ADOC:-$HOME/atelier/_bibliotheca/domus-asciidoc-build/bin/build-adoc.sh}" -
Build function scripts (e.g.,
build-<project>.sh) MUST support both direct execution and sourcing:# If executed directly (not sourced), run the function if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then build-project-name "$@" fi
The Pattern
Before (non-compliant)
Script content embedded directly in a partial:
==== Postgres Setup
\[source,bash]
\----
docker run -d \
--name domus_postgres \
-e POSTGRES_USER=domus \
...
postgres:16-alpine
psql -h localhost -U domus -d domus_index -f sql/001-create-tables.sql
\----
After (compliant)
Script extracted to scripts/setup-postgres.sh, partial shows only the run command:
==== Postgres Setup
\[source,bash]
\----
bash $(find data/d000/projects/domus-index/scripts -name 'setup-postgres.sh')
\----
Build Function Pattern
Every project SHOULD have a build-<project>.sh in scripts/:
bash data/d000/projects/<slug>/scripts/build-<slug>.sh
bash data/d000/projects/<slug>/scripts/build-<slug>.sh html catppuccin
bash data/d000/projects/<slug>/scripts/build-<slug>.sh pdf navy-executive
bash data/d000/projects/<slug>/scripts/build-<slug>.sh both
Or sourced for interactive use:
source data/d000/projects/<slug>/scripts/build-<slug>.sh
build-<slug> html obsidian-amber
Starter Template
Copy this skeleton for any extracted script. Fill in <slug> and the body:
|
Three rules that have caused repeated terminal crashes and session loss:
|
#!/usr/bin/env bash
# <slug> — reusable build function
#
# Usage:
# bash scripts/build-<slug>.sh # build + open (default)
# bash scripts/build-<slug>.sh build html catppuccin # build only
# bash scripts/build-<slug>.sh html obsidian-amber # build + open
# bash scripts/build-<slug>.sh help # list variants
#
# Source for interactive use (bash OR zsh):
# source scripts/build-<slug>.sh
# build-<slug> html royal
#
# NOTE: no top-level set -euo pipefail — sourcing would kill the terminal.
# Resolve build-adoc — alias not available in non-interactive bash
BUILD_ADOC="${BUILD_ADOC:-$HOME/atelier/_bibliotheca/domus-asciidoc-build/bin/build-adoc.sh}"
build-<slug>() {
local proj="$HOME/atelier/_bibliotheca/domus-captures/data/d000/projects/<slug>"
local doc="$proj/<slug>.adoc"
# Default verb: open (build + Firefox)
local verb="open"
case "${1:-}" in
build|open) verb="$1"; shift ;;
help) verb="help"; shift ;;
esac
local format="${1:-html}"
local variant="${2:-light-cyan}"
local theme="${2:-navy-executive}"
# Render diagrams before build
local img="$proj/images"
mkdir -p "$img" "$proj/output"
for f in "$proj"/diagrams/*.d2; do
[[ -f "$f" ]] && d2 "$f" "$img/$(basename "$f" .d2).svg" 2>/dev/null
done
for f in "$proj"/diagrams/*.dot; do
[[ -f "$f" ]] && dot -Tsvg "$f" -o "$img/$(basename "$f" .dot).svg"
done
case "$format" in
html)
"$BUILD_ADOC" "$doc" html --variant "$variant" -a env-d000 || return
printf '[OK] HTML built: %s/output/<slug>.html\n' "$proj"
[[ "$verb" == "open" ]] && firefox "$proj/output/<slug>.html" >/dev/null 2>&1 &
;;
pdf)
"$BUILD_ADOC" "$doc" pdf --theme "$theme" -a env-d000 || return
printf '[OK] PDF built: %s/output/<slug>.pdf\n' "$proj"
[[ "$verb" == "open" ]] && firefox "$proj/output/<slug>.pdf" >/dev/null 2>&1 &
;;
both)
"$BUILD_ADOC" "$doc" html --variant "$variant" -a env-d000 || return
"$BUILD_ADOC" "$doc" pdf --theme "$theme" -a env-d000 || return
printf '[OK] HTML + PDF built: %s/output/\n' "$proj"
[[ "$verb" == "open" ]] && firefox "$proj/output/<slug>.html" >/dev/null 2>&1 &
;;
help|*)
printf 'Usage: build-<slug> [build|open] [html|pdf|both] [variant/theme]\n'
# ... variant/theme list ...
return 0
;;
esac
}
# If executed directly (not sourced), run the function
# Compatible with both bash and zsh
if [[ -n "${BASH_SOURCE+x}" ]]; then
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && build-<slug> "$@"
elif [[ -n "${ZSH_EVAL_CONTEXT+x}" ]]; then
[[ "$ZSH_EVAL_CONTEXT" != *:file* ]] && build-<slug> "$@"
fi
Migrating an Existing Project
Turning a non-compliant project (inline commands in README.adoc / partials) into a compliant one — a five-minute conversion for a typical project:
-
Find the offenders — inline
[source,bash]blocks over 5 lines inpartials/*.adocandREADME.adoc. -
For each, create
scripts/<action>.shfrom the Starter Template and move the body in. -
chmod +x scripts/*.sh. -
Replace each inline block with its run command —
bash $(find data/d000/projects/<slug>/scripts -name '<action>.sh'), orsource …for build functions. -
Run the Compliance checks below; all must pass before you consider the project migrated.
Exceptions
-
Educational examples (glob-mastery, regex-mastery, vim-adventures): inline code blocks that exist as teaching content — not operational scripts — MAY remain inline. The test: would you execute this as a standalone file? If no, it stays inline.
-
One-liners (5 lines or fewer): short commands like
psql -c "select version();"orGet-AzContext | Format-ListMAY remain inline. They are documentation, not scripts. -
README.adoc quick-deploy blocks: the one-paste inline command in README MAY remain as a convenience, provided the same logic also exists as a script in
scripts/. -
PowerShell ad-hoc queries: single
Invoke-AzOperationalInsightsQuerycalls with inline KQL MAY remain inline if they are one-off exploration. Queries that recur across sessions MUST be added to the$Querieshashtable in the inventory module.
Compliance
| Check | Method | Pass Criterion |
|---|---|---|
No inline scripts >5 lines |
|
Every code block >5 lines is extracted or uses |
scripts/ directory exists |
|
Directory present in every standalone project |
Bash scripts are executable |
|
Zero results |
Alias resolution |
|
Zero bare |
No bash/python source in document |
|
Zero |
PowerShell uses deployer pattern |
|
Every |
PowerShell scripts are pure modules |
|
Zero results — no deployer wrapper inside |
Shebang + strict mode (bash) |
|
Every |
d001 scripts encrypted |
|
Every script in |
Platform Adaptation
This standard applies to all script languages. The separation is universal: the script is the artifact, the document shows how to deploy it.
Bash
Run command pattern — the script exists on the local filesystem:
\[source,bash]
\----
bash $(find data/d001/projects/<slug>/scripts -name 'script-name.sh')
\----
The $(find) command resolves the absolute path portably. No $ prompt glyph.
Alias resolution — shell aliases do not exist in non-interactive bash:
| Alias | Resolution Variable |
|---|---|
|
|
|
Shell function — scripts that need |
PowerShell — Deployer Pattern
PowerShell scripts target remote Windows machines that do not have the repository. The deployer pattern uses a heredoc wrapper to transfer the script content:
Three layers:
-
The script —
scripts/<name>.ps1— pure module code. No wrapper. Functions, query libraries, connect logic. Independently dot-sourceable:. .\<name>.ps1. -
The deployer — a
Set-Contentheredoc that wraps the script for one-paste deployment to a target machine. -
The partial —
partials/<section>/<name>.adoc— shows the deployer block withinclude::pulling the script into the heredoc at build time.
Script file (scripts/dcr-inventory.ps1):
# Pure module — no wrapper, no Set-Content
# Independently dot-sourceable: . .\dcr-inventory.ps1
$Queries = @{
SyslogSources = @"
Syslog | summarize Count=count() by Computer | order by Count desc
"@
}
function Invoke-DCRQuery {
param([Parameter(Mandatory)][string]$Query)
# ...
}
function Get-SyslogSources {
Invoke-DCRQuery $Queries.SyslogSources
}
Partial (partials/operations/dcr-inventory.adoc):
==== DCR Inventory Module
\[source,powershell]
\----
$script = @'
Unresolved include directive in modules/ROOT/partials/standards/workflow/script-extraction.adoc - include::{scriptsdir}/dcr-inventory.ps1[]
'@
$script | Set-Content "$HOME\Downloads\pwsh\dcr-inventory.ps1"
\----
At build time, include:: injects the script content into the heredoc. The reader copies the entire block, pastes into PowerShell, and the script lands on the target machine ready to dot-source.
Why this works:
| Concern | Mechanism |
|---|---|
Portability |
|
Testability |
The |
Single source |
The script exists once in |
Self-documenting |
The module prints a function inventory banner on load — the user sees what’s available without reading documentation. |
PowerShell Requirements
-
PowerShell scripts in
scripts/MUST be pure module code — noSet-Contentwrapper, no$script = @'heredoc. -
The deployer wrapper (
@'…'@ | Set-Content) MUST live in the AsciiDoc partial only, usinginclude::{scriptsdir}/<name>.ps1[]to pull the script content. -
Every
.ps1module SHOULD print a function inventory banner on load (theWrite-Hostblock listing available functions). -
Query libraries MUST use a
$Querieshashtable with named KQL here-strings — functions delegate to a singleInvoke-*Querywrapper. -
All functions MUST use
-ErrorAction SilentlyContinuewith explicit null checks — no unhandled exceptions. -
Read-only inventory modules MUST NOT contain
New-Az*,Set-Az*,Update-Az*, orRemove-Az*cmdlets. Operational scripts that modify resources are separate files with explicit-DryRunsupport.
Cross-Platform Summary
| Language | Script Location | Run/Deploy Pattern | Partial Shows |
|---|---|---|---|
Bash |
|
|
Run command only |
PowerShell |
|
|
Deployer heredoc with |
Python |
|
|
Run command only (same as bash) |
Encryption
Scripts in data/d001/ projects contain sensitive values (subscription IDs, tenant IDs, workspace names). The d001 open/close workflow only handles partials/*.adoc files. PowerShell and bash scripts in scripts/ require manual encryption:
# Encrypt all ps1 scripts
for f in data/d001/projects/<slug>/scripts/*.ps1; do
encrypt-file "$f"
done
# Encrypt all sh scripts
for f in data/d001/projects/<slug>/scripts/*.sh; do
encrypt-file "$f"
done
|
Always |
Related
-
STD-008: CLI Quality — verify-change-verify, idempotency, tool affordance
-
STD-001: Project Structure — partials hold content, pages are shells
-
STD-024: AsciiDoc Build Toolchain — build-adoc script architecture