47 KiB
CE OS Script Style Guide — Bash
Purpose
This document defines the mandatory style and structural standards for all Bash scripts produced under the CE OS / Cervello Elettrico umbrella. Every script — installer, utility, tier component, library, or one-off tool — must conform to these rules before it is considered fit for review, testing, or promotion.
A Python style guide will be produced as a companion to this document. Python scripts should follow this Bash guide as closely as the language permits; deviations are documented in the Python guide with explicit justification.
These rules encode hard-won lessons from real hardware deployment across Alpine, Debian, and NixOS on armv6/armhf, arm64, x86, and x86-64. They exist because silent failures, missed cleanups, and untested assumptions have caused real problems on real machines. Follow them without exception.
CE OS scripts are also teaching documents. An enthusiast-level reader — someone eager to learn but not a professional developer — should be able to follow any script and understand what it does and why. Clarity is not optional.
Part 1 — Universal Rules
These rules apply to every Bash script regardless of its role (orchestrator, subscript, or library).
1.1 Attribution header
Every script — without exception — carries both attributions in its header:
Built standing on the shoulders of billions of dwarves
Created by John A. Hoeven with the ethical assistance of Claude AI
The dwarves come first. This is non-negotiable. It appears in every file, every time.
1.2 Version and status
Every script header includes version and status:
Version: v0.0.1 | Status: DEVELOPMENT
Version tracks follow CE OS conventions:
| Stage | Format | Increment rule |
|---|---|---|
| Development | v0.0.x |
Third point on every meaningful edit |
| Testing | v0.x.x |
Second point on milestones |
| Stable | vX.Y |
Major/minor on scope/compatibility |
1.3 Licence
The default licence for all CE OS scripts is The Unlicense. Every script header must declare its licence:
# Licence: The Unlicense — https://unlicense.org
Commercial use is explicitly permitted. No conditions, no attribution
requirement, no notification needed. Do what you want with it.
We do ask, however, that you voluntarily attribute the dwarves — the
billions of contributors whose work made yours possible. It costs
nothing and means everything.
Any deviation from The Unlicense requires an explicit explanation in the script header:
# Licence: <licence name> — <URL>
# Licence note: <concise reason why The Unlicense is not used here>
A script with no licence declaration is incomplete.
1.4 Update and upgrade first
Every script that installs or modifies packages must run the OS package manager's update and upgrade sequence as its very first system-touching action — before any package installation, before any capability test.
The exact command depends on the detected OS (see §2.6). No exceptions. A script that installs packages without first updating the package index is defective.
1.5 Architecture awareness
Every script that installs packages must:
- Detect the running architecture (
uname -m) - Consult
ce-index.conffor known arch-specific availability - Test that each required package is available for that architecture before attempting installation
- Log clearly which packages are unavailable and why
- Never silently skip a package — always surface the gap to the OHIOD1.
1.6 Test before, during, and after
Testing is not optional and not an afterthought. Every script has three explicit test gates:
- Pre-flight — before any system changes. Checks environment, dependencies, architecture, disk space, connectivity. All warn-only. No changes made at this stage.
- Inline — after each significant operation. Verifies the operation succeeded before proceeding. Hard-fail on critical operations; warn-only on optional components.
- Post-install — after all operations complete. Verifies installed tools, written config files, service state. Hard-fail if required components are absent.
Test results are written to the log file with timestamps. A script with no test gates is incomplete.
1.7 Cleanup before exit
Every script registers a cleanup handler that runs on all exits — normal, error, and signal. The cleanup handler must:
- Remove all temporary files and directories created by the script
- Clear the package manager's download cache
- Remove any lock files created by the script
- Log that cleanup completed
A script that leaves debris is defective.
1.8 No silent failures
Every error is logged and surfaced. A script that swallows errors and reports success is worse than one that fails loudly. If an operation cannot be completed, say so clearly, log it, and either hard-fail or warn — never silently skip.
1.9 Credentials
Never write real credentials in any script, config file, or comment.
Use [PLACEHOLDER] format always. This applies to passwords, tokens,
API keys, private keys, and any other secret material.
1.10 Never run as root
CE OS scripts run as the OHIOD user and invoke the
detected privilege tool (doas or sudo) for privileged operations.
Scripts must not require or assume direct root execution. If a script is
accidentally run as root, it should warn and exit.
The privilege tool is detected by bootstrap.sh and written to
ce_env.conf as CE_PRIV. All scripts source this value — never
hardcode doas or sudo.
1.11 Script length limits
CE OS Bash scripts follow these length limits. Lines are counted as written, including comments — comments are part of the script, not overhead; empty/blank lines are not included in the line count.
| Script role | Soft limit | Hard limit | Notes |
|---|---|---|---|
| Subscript | 150 lines | 200 lines | Split or extract at hard limit |
| Library | 150 lines | 200 lines | Split by function group if needed |
| Orchestrator | None | None | Coordinators by nature; no logic to limit |
Soft limit (150): Review the script. Can a function be extracted to a library? Can a phase be split into a subscript? If yes, do it. If the script is genuinely cohesive at this length, proceed — but document why in the header.
Hard limit (200): Split or extract. This is the default action. No argument is needed — just do it.
Hard limit override: In rare cases, keeping a script intact beyond 200 lines is the correct decision. This is permitted, but must be declared explicitly in the script header:
# Line limit override: <concise reason why splitting would harm clarity or correctness>
A silent override is not an override — it is a defect.
Orchestrators are excluded from all limits. An orchestrator that sources five libraries and calls twelve subscripts in sequence may be 300 lines of clean, readable flow control. That is correct. Logic belongs in libraries and subscripts; the orchestrator reads like a table of contents.
1.12 Scripts as teaching documents
CE OS scripts are read by people learning the system, not only by people maintaining it. Write accordingly.
- Comments explain intent, not mechanics.
# Install vimis noise.# vim is the CE OS default editor — required for all tiersis information. - Complex functions belong in libraries. A 50-line function embedded in a subscript forces the reader to context-switch mid-flow. Extracted to a library with a clear header comment, it can be read and understood in isolation.
- If a function needs a comment block longer than itself, extract it. The comment is telling you the function is too complex to live inline.
- The orchestrator is the lesson plan. A reader following an orchestrator should understand the full install flow without opening a single sourced file. Each call to a subscript or library function should be self-evident from its name and any inline comment.
- Target register: enthusiast level. Make It Understandable and Obvious. A competent but non-professional reader, eager to learn, should be able to follow the logic. If they cannot, the script needs work — not the reader.
Part 2 — Bash Style Guide
2.1 Shebang and script class
CE OS uses two Bash script classes. Choose the correct one:
Class A — System scripts
bootstrap.sh, OpenRC service scripts, /etc/local.d/ scripts, and
anything that runs before the CE OS environment is confirmed present.
Class A scripts are POSIX sh / ash throughout — no bash features permitted.
#!/bin/sh
# POSIX sh / ash only. No bash features.
Class B — CE OS scripts All installer scripts, operational scripts, tier components, libraries, and user-facing tools. Everything written for CE OS day-to-day use.
#!/usr/bin/env bash
# Bash features permitted. BusyBox tools on PATH on Alpine.
When in doubt, write Class B. Use Class A only when you have a specific reason to require ash compatibility.
2.2 Full header block
Every CE OS Bash script carries this header in full:
#!/usr/bin/env bash
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# <script-name>.sh
# /opt/ceos/scripts/<script-name>.sh
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: <orchestrator | subscript | library>
# ---------------------------------------------------------------------------
# Purpose: One-line description of what this script does.
# Target: Which nodes / contexts this runs on (e.g. Alpine aarch64 Pi 5)
# Entry: How it is invoked (e.g. doas bash phase1.sh, or ./tool.sh --flag)
# Depends: ce-common-lib.sh, ce-test-lib.sh (list sourced libraries)
# ---------------------------------------------------------------------------
# Phases:
# 0 — Pre-flight (environment checks, no system changes)
# 1 — <description>
# 2 — <description>
# N — Cleanup (always runs via trap)
# ---------------------------------------------------------------------------
All fields are mandatory. Leave none blank. If a field does not apply,
write N/A — do not delete the line.
2.3 Strict mode — selectively applied
Do not use set -e in CE OS scripts. It causes unpredictable exits
on operations that are allowed to fail (package availability checks,
optional installs, test assertions). CE OS scripts handle errors explicitly.
Use these instead:
set -u # Treat unset variables as errors
set -o pipefail # Propagate pipe failures
Apply set -u and set -o pipefail after sourcing libraries. Handle
each potential failure explicitly with conditional checks.
2.4 Variables and naming
# Constants — UPPER_SNAKE_CASE
readonly SCRIPT_VERSION="v0.0.1"
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_DIR="${HOME}/.local/logs/ceos_installer"
readonly ARCH="$(uname -m)"
# Local variables — lower_snake_case
local pkg_name=""
local install_result=0
# Loop variables — short, descriptive
for pkg in "${packages[@]}"; do ...
# Arrays — UPPER for constants, lower for mutable
readonly REQUIRED_PKGS=( bash vim tmux )
available_pkgs=()
Always quote variable expansions: "${VAR}" not $VAR. Always.
2.5 Functions
# Function declaration style — no function keyword
check_architecture() {
local expected_arch="${1:?check_architecture: expected_arch required}"
local actual_arch
actual_arch="$(uname -m)"
if [[ "${actual_arch}" != "${expected_arch}" ]]; then
log_warn "Architecture mismatch: expected ${expected_arch}, got ${actual_arch}"
return 1
fi
log_info "Architecture confirmed: ${actual_arch}"
return 0
}
Rules:
- No
functionkeyword — usename() {style - Every function documents its arguments with
${VAR:?message}or explicit checks - All functions
return 0on success, non-zero on failure - Local variables declared with
local— never pollute global scope - Functions are grouped: library sourcing, then utilities, then phases, then main
2.6 Package manager abstraction
By the time any Class B script runs, bootstrap.sh has already detected
the package manager and written it to ce_env.conf. Class B scripts source
this file and load the correct package manager library — they do not detect
the package manager themselves.
# Source the bootstrap environment — always the first sourcing action
source /tmp/ce_env.conf || {
echo "ERROR: ce_env.conf not found. Was bootstrap.sh run first?" >&2
exit 1
}
# Load the correct package manager library for this distro
source "${LIB_DIR}/pkg/${CE_PKG_LIB}" || {
echo "ERROR: Package manager library not found: ${CE_PKG_LIB}" >&2
exit 1
}
Each package manager library (lib/pkg/apk.sh, lib/pkg/apt.sh, etc.)
exposes the same interface:
pkg_update() # Update package index
pkg_upgrade() # Upgrade installed packages
pkg_install() <pkg> # Install a package
pkg_available() <pkg> # Test availability — returns 0 if found
pkg_cache_clean() # Clear download cache
This means all higher-level scripts call pkg_install vim regardless of
distro — the library handles the distro-specific implementation.
2.7 Privilege tool abstraction
bootstrap.sh detects doas or sudo and writes CE_PRIV to
ce_env.conf. Never hardcode either tool:
# Correct — uses detected privilege tool
${CE_PRIV} pkg_install vim
# Wrong — assumes Alpine/CE OS default
doas pkg_install vim
# Wrong — assumes most other distros
sudo pkg_install vim
2.8 Init system abstraction
bootstrap.sh detects OpenRC or systemd and writes CE_INIT to
ce_env.conf. Load the correct init library:
source "${LIB_DIR}/init/${CE_INIT_LIB}" || {
echo "ERROR: Init library not found: ${CE_INIT_LIB}" >&2
exit 1
}
Each init library exposes the same interface:
svc_enable() <service> # Enable service at boot
svc_start() <service> # Start service now
svc_stop() <service> # Stop service
svc_restart() <service> # Restart service
svc_status() <service> # Query service status
2.9 Userland abstraction
bootstrap.sh detects BusyBox or GNU userland and writes CE_USERLAND
to ce_env.conf. For Class B scripts running on Alpine, prefer
BusyBox-safe patterns regardless — this keeps scripts portable across
both userlands without branching:
| Avoid (GNU-only) | Use instead (BusyBox-safe) |
|---|---|
grep -P |
grep -E |
sed 's/\w//' |
sed 's/[[:alnum:]_]//' |
find -printf |
find ... -exec basename {} \; or pipe to awk |
stat -c "%Y" f |
stat -t f | awk '{print $12}' |
dd status=progress |
plain dd, or pv if progress needed |
ping in scripts |
curl --silent --max-time 5 --output /dev/null URL |
When a GNU-only feature is genuinely required and has no BusyBox-safe
equivalent, gate it on CE_USERLAND:
if [[ "${CE_USERLAND}" == "gnu" ]]; then
# GNU-specific implementation
else
# BusyBox-safe fallback
fi
Document the reason in a comment. Do not gate silently.
2.10 Architecture-aware package testing
Before installing any package, test availability for the running architecture. The search order for Alpine is:
- Consult
ce-index.conffor known arch-specific status - Alpine stable (main)
- Alpine community
- Alpine edge
- CE APK repo on Forgejo (
CE_APK_REPO)
Set the CE repo URL as a constant at the top of any script that may need it:
# CE APK repository — packages built by Cervello Elettrico
# Replace [PLACEHOLDER] with the live Forgejo APK repo URL when available
readonly CE_APK_REPO="[PLACEHOLDER]"
test_package_available() {
local pkg="${1:?test_package_available: pkg required}"
local arch="${ARCH}"
# Check index for known unavailability before hitting live repos
local index_key="CE_PKG_${pkg}_${arch}"
local index_val="${!index_key:-}"
if [[ "${index_val}" == "unavailable" ]]; then
log_warn " KNOWN UNAVAILABLE (index): ${pkg} on ${arch}"
return 1
fi
case "${PKG_MANAGER}" in
apk)
# Search official Alpine repos (stable, community, edge)
if apk search -x "${pkg}" 2>/dev/null | grep -q "^${pkg}-"; then
log_info " FOUND (Alpine repo): ${pkg}"
return 0
fi
# Fall back to CE APK repo on Forgejo
if [[ -n "${CE_APK_REPO:-}" ]] && \
curl --silent --max-time 5 \
"${CE_APK_REPO}/${arch}/APKINDEX.tar.gz" \
| tar -xzO 2>/dev/null \
| grep -q "^P:${pkg}$"; then
log_info " FOUND (CE repo): ${pkg}"
return 0
fi
;;
apt)
apt-cache show "${pkg}" >/dev/null 2>&1 && return 0
;;
nix)
nix-env -qaP "${pkg}" >/dev/null 2>&1 && return 0
;;
esac
log_warn "Package '${pkg}' not available for ${arch} on ${PKG_MANAGER}"
log_warn "If you believe this is incorrect, please file a bug report at:"
log_warn " ${CE_BUG_REPORT_URL}"
log_warn "Include: package=${pkg} arch=${ARCH} distro=${CE_DISTRO} scriptset=${CE_SCRIPTSET} date=$(date '+%Y-%m-%d')"
return 1
}
install_packages() {
local -n pkg_list="${1:?install_packages: pkg_list required}"
local failed=()
local unavailable=()
for pkg in "${pkg_list[@]}"; do
if ! test_package_available "${pkg}"; then
unavailable+=( "${pkg}" )
continue
fi
if ! ${CE_PRIV} pkg_install "${pkg}"; then
log_error "Failed to install: ${pkg}"
failed+=( "${pkg}" )
else
log_info "Installed: ${pkg}"
fi
done
if [[ ${#unavailable[@]} -gt 0 ]]; then
log_warn "Unavailable for ${ARCH}: ${unavailable[*]}"
log_warn "These packages were skipped. Review CE repo for alternatives."
fi
if [[ ${#failed[@]} -gt 0 ]]; then
log_error "Installation failures: ${failed[*]}"
return 1
fi
return 0
}
2.11 Logging
Source ce-common-lib.sh for the standard logging functions. If writing
a standalone script that cannot source ce-common-lib.sh, implement
these four levels minimally:
# Minimal logging — use ce-common-lib.sh log functions when available
_log() {
local level="${1}"
local msg="${2}"
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
printf '[%s] [%s] %s\n' "${ts}" "${level}" "${msg}" | tee -a "${LOG_FILE}"
}
log_info() { _log "INFO " "${1}"; }
log_warn() { _log "WARN " "${1}" >&2; }
log_error() { _log "ERROR" "${1}" >&2; }
log_debug() { [[ "${CE_DEBUG:-0}" == "1" ]] && _log "DEBUG" "${1}"; }
Log file location: ~/.local/logs/ceos_installer/ (XDG compliant).
Log file name: <script-name>-<YYYY-MM-DD>.log.
Possible new logging scheme: /home/[user]/[subdirectory of choice]/[scriptset directory]/[script log time datestamp].log
2.12 Cleanup trap
Register the cleanup trap immediately after defining the cleanup function, before any work begins:
# ── Cleanup ────────────────────────────────────────────────────────────────
TEMP_DIR=""
cleanup() {
local exit_code="${?}"
log_info "Running cleanup..."
# Remove temp files
if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then
rm -rf "${TEMP_DIR}"
log_info "Removed temp dir: ${TEMP_DIR}"
fi
# Clear package manager cache
pkg_cache_clean 2>/dev/null || true
log_info "Cleanup complete (exit code: ${exit_code})"
}
trap cleanup EXIT INT TERM HUP
# Create temp dir only after trap is registered
TEMP_DIR="$(mktemp -d)"
2.13 Test gates — structure
# ── Pre-flight ─────────────────────────────────────────────────────────────
phase_preflight() {
log_info "=== PRE-FLIGHT ==="
# Each check: run_test <description> <command> [warn|fail]
run_test "bash version >= 4" '[[ "${BASH_VERSINFO[0]}" -ge 4 ]]' fail
run_test "running as non-root" '[[ "${EUID}" -ne 0 ]]' fail
run_test "internet reachable" \
'curl --silent --max-time 5 --output /dev/null https://cervelloelettrico.it' \
warn
run_test "architecture detected" '[[ -n "${ARCH}" ]]' fail
run_test "disk space >= 500MB" \
'[[ $(df / --output=avail | tail -1) -ge 512000 ]]' \
warn
log_info "Pre-flight complete"
}
# ── Inline test (example) ──────────────────────────────────────────────────
phase_install_vim() {
log_info "=== INSTALLING VIM ==="
# vim is the CE OS default editor — required for all tiers
${CE_PRIV} pkg_install vim || { log_error "vim install failed"; return 1; }
# Inline test — verify immediately after install
run_test "vim installed" 'command -v vim >/dev/null 2>&1' fail
log_info "vim install verified"
}
# ── Post-install ───────────────────────────────────────────────────────────
phase_verify() {
log_info "=== POST-INSTALL VERIFICATION ==="
local required_tools=( vim tmux starship bash )
for tool in "${required_tools[@]}"; do
run_test "${tool} available" "command -v ${tool} >/dev/null 2>&1" fail
done
log_info "All required tools verified"
}
The run_test function lives in ce-test-lib.sh. When writing a
standalone script, implement a minimal version:
run_test() {
local desc="${1}"
local cmd="${2}"
local mode="${3:-fail}" # fail | warn
if eval "${cmd}" >/dev/null 2>&1; then
log_info " PASS: ${desc}"
return 0
else
if [[ "${mode}" == "fail" ]]; then
log_error " FAIL: ${desc}"
exit 1
else
log_warn " WARN: ${desc}"
return 1
fi
fi
}
2.14 User confirmation
One confirmation prompt before system modifications begin. No more:
confirm_proceed() {
local prompt="${1:-Proceed with installation?}"
printf '\n%s [y/N] ' "${prompt}"
read -r response
case "${response}" in
[yY]|[yY][eE][sS]) return 0 ;;
*) log_info "Aborted by OHIOD Unit ID ${USER}"
exit 0 ;;
esac
}
2.15 Script architecture — orchestrators, subscripts, and libraries
CE OS Bash scripts follow a three-layer architecture. Understanding which layer a script belongs to determines its structure, its line limits, and what it is permitted to contain.
The three layers
Libraries (ce-*-lib.sh and variant libraries)
A library is a collection of related functions and constants that other scripts source. Libraries contain no executable code at the top level — they are sourced, not run. Running a library directly should produce no output and no side effects.
Extract a function to a library when:
- More than one script needs it (reuse)
- The function is complex enough that embedding it disrupts the flow of the script it lives in (clarity)
- The function requires a comment block longer than itself to be understood (the comment is telling you it belongs in isolation)
Naming: ce-<domain>-lib.sh for shared libraries; variant libraries
live in domain subdirectories (lib/pkg/apk.sh, lib/init/openrc.sh).
#!/bin/sh
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# ce-pkg-lib.sh
# /opt/ceos/lib/ce-pkg-lib.sh
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: Library — Package manager detection and operations
# ---------------------------------------------------------------------------
# Purpose: Package manager detection, update, and availability testing.
# Used by: ce-install.sh, ce-base.sh, ce-minimal.sh
# ---------------------------------------------------------------------------
# This file is a library. Source it — do not run it directly.
# ---------------------------------------------------------------------------
# Guard against double-sourcing
[[ -n "${CE_PKG_LIB_LOADED:-}" ]] && return 0
readonly CE_PKG_LIB_LOADED=1
detect_package_manager() { ... }
test_package_available() { ... }
run_package_update() { ... }
Load-order dependencies in library subdirectories
When libraries within a subdirectory depend on each other, numbered
prefixes (01_, 02_) show load order. This is not sufficient
documentation on its own. The dependency must also be declared explicitly
in the Depends: header field of the dependent file:
# Depends: 01_core.sh — CE_PRIV and CE_USERLAND must be set before this loads
Numbered prefixes show order. The Depends: field explains why.
Subscripts (ce-*.sh)
A subscript owns a single phase or a coherent group of related operations. It accepts arguments, performs its work, and returns a meaningful exit code. Subscripts may be run directly for testing and debugging — this is a feature, not a side effect.
Naming: ce-<phase>.sh or ce-<domain>.sh — e.g. ce-base.sh,
ce-minimal.sh, ce-audio.sh.
Subject to the 150-line soft / 200-line hard limit. If a subscript is growing beyond this, extract complex functions to a library first, then consider whether the phase itself should be split.
Orchestrators (ce-install.sh or similar)
An orchestrator coordinates subscripts and libraries. It contains:
- Sourcing of
ce_env.confand required libraries - Calling subscripts in sequence
- Inter-phase safety gates (explicit
[y/N]confirmation before each tier) - The master cleanup trap
- Flow control based on subscript exit codes
An orchestrator contains no complex logic of its own. If something complex needs to happen, it lives in a subscript or library. The orchestrator calls it. A reader following the orchestrator should understand the complete flow without opening any sourced file.
Orchestrators have no line limit. A 300-line orchestrator that sources six libraries and calls ten subscripts in sequence is correct and expected.
Sourcing libraries
# Source the bootstrap environment first — always
source /tmp/ce_env.conf || {
echo "ERROR: ce_env.conf not found. Was bootstrap.sh run first?" >&2
exit 1
}
# Resolve library directory relative to script location
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LIB_DIR="${SCRIPT_DIR}/lib"
# shellcheck source=lib/ce-common-lib.sh
source "${LIB_DIR}/ce-common-lib.sh" || {
echo "ERROR: ce-common-lib.sh not found at ${LIB_DIR}" >&2
exit 1
}
# Load variant libraries resolved by bootstrap
# shellcheck source=lib/pkg/apk.sh
source "${LIB_DIR}/pkg/${CE_PKG_LIB}" || {
echo "ERROR: Package library not found: ${CE_PKG_LIB}" >&2
exit 1
}
Always validate that sourced files exist and loaded correctly. A missing library is a hard failure — never silently proceed without it.
Directory layout
ceos-alpine-installer/
├── bootstrap.sh # Class A — POSIX sh throughout
├── ce-install.sh # Orchestrator
├── ce-base.sh # Tier 0 subscript
├── ce-minimal.sh # Tier 1 subscript
├── ce-basic.sh # Tier 2 subscript
├── deploy.sh # Deployment wrapper
├── ce-manifest.conf # Script set target declaration
├── ce-index.conf # Arch/distro package and library index
├── lib/
│ ├── ce-common-lib.sh # Logging, run_test, confirm_proceed
│ ├── ce-test-lib.sh # Test gate framework
│ ├── base/ # Tier 0 component libraries
│ │ ├── 01_core.sh
│ │ └── 02_doas.sh
│ ├── minimal/ # Tier 1 component libraries
│ │ ├── 01_shell.sh
│ │ └── 02_terminal.sh
│ ├── basic/ # Tier 2 component libraries
│ │ ├── 01_comms.sh
│ │ └── 02_monitor.sh
│ ├── pkg/ # Package manager variant libraries
│ │ ├── apk.sh # Alpine
│ │ └── apt.sh # Debian
│ ├── init/ # Init system variant libraries
│ │ ├── openrc.sh # Alpine
│ │ └── systemd.sh # Debian
│ └── priv/ # Privilege tool variant libraries
│ ├── doas.sh # CE OS default
│ └── sudo.sh # Debian and others
├── spec/
│ └── install_spec.md # Human-readable install specification
└── tests/
├── test_base.sh
├── test_minimal.sh
└── test_basic.sh
Summary table
| Layer | Naming | Contains | Line limit |
|---|---|---|---|
| Library | ce-*-lib.sh |
Functions and constants only | 150 soft / 200 hard |
| Variant lib | lib/<domain>/*.sh |
Distro-specific implementations | 150 soft / 200 hard |
| Subscript | ce-*.sh |
Single phase or domain logic | 150 soft / 200 hard |
| Orchestrator | ce-install.sh |
Flow, gates, sourcing, trap only | None |
2.16 Bootstrap — bootstrap.sh
bootstrap.sh is the sole Class A script in a CE OS installer set. It is
POSIX sh / ash throughout — no bash features, no CE OS libraries, no
sourcing of ce_env.conf (it creates it). It has one job per phase:
Phase 1 — Ensure bash is present
#!/bin/sh
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# bootstrap.sh
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: Bootstrap — Class A, POSIX sh throughout
# ---------------------------------------------------------------------------
# Purpose: Ensure bash is present, detect environment, write ce_env.conf,
# validate compatibility, then hand off to ce-install.sh.
# ---------------------------------------------------------------------------
command -v bash >/dev/null 2>&1 || {
echo "bash not found — installing..."
# Use whatever is available — no CE_PRIV yet
if command -v apk >/dev/null 2>&1; then
apk add bash
elif command -v apt-get >/dev/null 2>&1; then
apt-get install -y bash
else
echo "ERROR: Cannot install bash — no supported package manager found" >&2
exit 1
fi
}
Phase 2 — Read the script set manifest
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "${SCRIPT_DIR}/ce-manifest.conf" || {
echo "ERROR: ce-manifest.conf not found" >&2
exit 1
}
Phase 3 — Detect environment
# Distro
CE_DISTRO="unknown"
if [ -f /etc/os-release ]; then
. /etc/os-release
case "${ID:-}" in
alpine) CE_DISTRO="alpine" ;;
debian|ubuntu) CE_DISTRO="debian" ;;
nixos) CE_DISTRO="nixos" ;;
esac
fi
# Init system
CE_INIT="unknown"
command -v rc-service >/dev/null 2>&1 && CE_INIT="openrc"
command -v systemctl >/dev/null 2>&1 && CE_INIT="systemd"
# Privilege tool
CE_PRIV="unknown"
command -v doas >/dev/null 2>&1 && CE_PRIV="doas"
command -v sudo >/dev/null 2>&1 && CE_PRIV="sudo"
# Userland
CE_USERLAND="gnu"
ls --version 2>&1 | grep -q BusyBox && CE_USERLAND="busybox"
# Architecture
CE_ARCH="$(uname -m)"
Phase 4 — Validate against manifest and hard fail if unsupported
# Check distro compatibility
distro_supported=0
for d in ${CE_SUPPORTED_DISTROS}; do
[ "${CE_DISTRO}" = "${d}" ] && distro_supported=1 && break
done
if [ "${distro_supported}" -eq 0 ]; then
cat <<EOF
Cervello Elettrico has determined that this use case is best served by
${CE_DISTRO_TIER1} and can also be used with ${CE_DISTRO_TIER2} with
success, but other distros will need special attention that we are
unable to provide.
We recommend that you either install a compatible base distro, or modify
the scripts to suit your own choices. Our scripts are written to be read
and understood — not just run. If you are comfortable with the shell,
the comments are your map. Fork it, fix it, make it your own.
Detected distro: ${CE_DISTRO}
Script set: ${CE_SCRIPTSET}
EOF
exit 1
fi
Phase 5 — Resolve variant library paths from index
. "${SCRIPT_DIR}/ce-index.conf" || {
echo "ERROR: ce-index.conf not found" >&2
exit 1
}
# Resolve library paths for detected environment
eval "CE_PKG_LIB=\${CE_LIB_pkg_${CE_DISTRO}:-}"
eval "CE_INIT_LIB=\${CE_LIB_init_${CE_DISTRO}:-}"
eval "CE_PRIV_LIB=\${CE_LIB_priv_${CE_DISTRO}:-}"
[ -n "${CE_PKG_LIB}" ] || { echo "ERROR: No package library for ${CE_DISTRO}" >&2; exit 1; }
Phase 6 — Write ce_env.conf
cat > /tmp/ce_env.conf <<EOF
# CE OS environment — written by bootstrap.sh — do not edit manually
CE_SCRIPTSET="${CE_SCRIPTSET}"
CE_DISTRO="${CE_DISTRO}"
CE_INIT="${CE_INIT}"
CE_PRIV="${CE_PRIV}"
CE_USERLAND="${CE_USERLAND}"
CE_ARCH="${CE_ARCH}"
CE_PKG_LIB="${CE_PKG_LIB}"
CE_INIT_LIB="${CE_INIT_LIB}"
CE_PRIV_LIB="${CE_PRIV_LIB}"
CE_BUG_REPORT_URL="${CE_BUG_REPORT_URL}"
EOF
Phase 7 — Hand off to orchestrator
exec bash "${SCRIPT_DIR}/ce-install.sh" "$@"
bootstrap.sh does nothing else. All further logic belongs in the
orchestrator or its libraries.
2.17 Script set manifest — ce-manifest.conf
ce-manifest.conf sits at the root of each installer set alongside
bootstrap.sh. It is a POSIX sh-sourceable file declaring what the
script set supports and how to communicate its opinions.
# ce-manifest.conf — CE OS Workstation installer set
# Sourced by bootstrap.sh — POSIX sh only, no bash features
CE_SCRIPTSET="workstation"
# Tier 1: fully supported and tested
CE_DISTRO_TIER1="Alpine Linux"
# Tier 2: supported, minor issues possible
CE_DISTRO_TIER2="Debian"
# Machine-readable support lists (space-separated)
CE_SUPPORTED_DISTROS="alpine debian"
CE_UNSUPPORTED_DISTROS="nixos"
CE_SUPPORTED_ARCHES="aarch64 armhf x86_64"
CE_UNSUPPORTED_ARCHES="armv6 x86"
# Per-arch notes — surfaced on hard fail
CE_ARCH_NOTE_armv6="armv6 cannot run the AI stack or several Tier 2 packages"
CE_ARCH_NOTE_x86="x86 is missing several Tier 2 packages in Alpine repos"
# Bug report URL — Forgejo issue tracker for this script set
CE_BUG_REPORT_URL="[PLACEHOLDER]"
Cervello Elettrico is opinionated about targets — two supported tiers plus the FFM path for everything else. This is more generous than most projects, which support a single target. The manifest makes the opinion explicit and gives the OHIOD everything they need to make an informed choice.
2.18 Package and library index — ce-index.conf
ce-index.conf sits at the root of each installer set. It is a
POSIX sh-sourceable file declaring known package availability by arch
and library variant paths by distro.
# ce-index.conf — CE OS Workstation installer set
# Sourced by bootstrap.sh — POSIX sh only, no bash features
# Package availability index
# Format: CE_PKG_<pkgname>_<arch>="available|edge|ce-repo|unavailable"
# Only entries that deviate from 'available' need listing.
# Packages not listed are assumed available — the live search will confirm.
# When a package search fails unexpectedly, file a bug report so this
# index can be updated.
CE_PKG_glow_x86_64="edge"
CE_PKG_glow_aarch64="edge"
CE_PKG_glow_armhf="unavailable"
CE_PKG_glow_x86="unavailable"
# Library variant index
# Format: CE_LIB_<domain>_<distro>="filename.sh"
CE_LIB_pkg_alpine="apk.sh"
CE_LIB_pkg_debian="apt.sh"
CE_LIB_init_alpine="openrc.sh"
CE_LIB_init_debian="systemd.sh"
CE_LIB_priv_alpine="doas.sh"
CE_LIB_priv_debian="sudo.sh"
The index is the authoritative record of known deviations from standard availability. The live package search is the fallback for anything not listed. When a live search fails on a package that should be available, that is the signal to file a bug report and update the index — one of the few bug reports that directly improves the installer for everyone.
Part 3 — Python Style Guide
CE OS Python scripts follow this Bash style guide as closely as the
language permits. The principles are identical: attribution headers,
test gates, cleanup on exit, no silent failures, credentials as
[PLACEHOLDER], and scripts as teaching documents.
Where Python requires a different approach, the deviation is documented here with an explicit justification. Do not invent deviations — if something works the same way in Python as in Bash, do it the same way.
3.1 Attribution and licence header
Every Python script carries the same attribution as Bash, adapted for Python comment syntax:
#!/usr/bin/env python3
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# script_name.py
# /opt/ceos/scripts/script_name.py
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: <orchestrator | module | utility>
# ---------------------------------------------------------------------------
# Purpose: One-line description.
# Target: Which nodes / contexts (e.g. Alpine aarch64, Debian amd64)
# Entry: How invoked (e.g. python3 script_name.py --flag)
# Depends: List non-stdlib imports and their install source
# ---------------------------------------------------------------------------
3.2 Where Python deviates from Bash — and why
| Bash pattern | Python equivalent | Reason for deviation |
|---|---|---|
trap cleanup EXIT INT TERM |
atexit.register() + try/finally |
Python has no trap; atexit is the idiomatic equivalent |
set -u |
Type hints + explicit checks | Python raises NameError on undefined names; set -u has no direct equivalent but type hints and explicit validation serve the same intent |
set -o pipefail |
subprocess.run(check=True) |
Pipe failures are caught per-call in Python, not globally |
source library.sh |
import ce_module or from ce_module import fn |
Python modules are the library equivalent; sourcing does not exist |
local var |
Variables scoped within functions naturally | Python function scope is implicit; local has no equivalent and is not needed |
| Orchestrator shell script | main() calling phase functions |
Python orchestration lives in main() within a single file, or in a __main__.py for packages |
[[ condition ]] |
if condition: |
Standard Python conditionals |
3.3 Mandatory patterns
main() guard — always:
def main() -> None:
...
if __name__ == "__main__":
main()
Never put executable code at module level outside main() and constant
definitions. This makes the script safe to import for testing — the same
reason Bash libraries have no top-level executable code.
Type hints — always:
def check_package_available(pkg: str, arch: str) -> bool:
...
Type hints serve the same intent as ${VAR:?message} in Bash — they
document what a function expects and make failures explicit.
Specific exception handling — always:
# Correct
try:
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
log.error(f"Command failed: {e.stderr.strip()}")
sys.exit(1)
# Wrong — never use bare except
try:
...
except:
pass
Bare except is the Python equivalent of swallowing errors silently.
It is a defect by the same rule as §1.8.
Cleanup with atexit:
import atexit, shutil, tempfile
_tmp_dir: str | None = None
def _cleanup() -> None:
if _tmp_dir and os.path.isdir(_tmp_dir):
shutil.rmtree(_tmp_dir, ignore_errors=True)
log.info("Cleanup complete")
atexit.register(_cleanup)
Register cleanup before any work begins — same rule as the Bash trap.
Line limits: Python functions consume lines faster than Bash due to type hints, docstrings, and explicit error handling. Line limits for Python will be established by the dedicated Python style guide once sufficient CE OS Python code exists to calibrate them from experience.
Part 4 — Quick Reference
4.1 Mandatory checklist — every Bash script
Before any script is considered ready for review, verify all of the following:
[ ] Attribution header present: dwarves first, then John A. Hoeven / Claude AI
[ ] Licence declared (The Unlicense or deviation with explanation)
[ ] Version and Status in header
[ ] Script role declared (orchestrator / subscript / library)
[ ] Line limit override comment present if >200 lines (subscripts and libraries)
[ ] ce_env.conf sourced as first action (Class B scripts)
[ ] Correct variant libraries loaded via ce_env.conf paths
[ ] Package manager used via pkg_* interface, not directly
[ ] Privilege tool used via ${CE_PRIV}, never hardcoded
[ ] Init system used via svc_* interface, not directly
[ ] doas/sudo pkg_update && pkg_upgrade runs first (if installing packages)
[ ] Architecture detected (uname -m) before any package operations
[ ] ce-index.conf consulted before live package search
[ ] Each package tested: index → Alpine stable → community → edge → CE repo
[ ] Package unavailability logs bug report URL with full context
[ ] Pre-flight test gate implemented (warn-only, no changes)
[ ] Inline tests after each significant operation
[ ] Post-install verification gate implemented
[ ] Cleanup trap registered before any work begins
[ ] Cleanup removes: temp files, pkg cache, lock files
[ ] Cleanup runs on EXIT INT TERM HUP
[ ] No silent failures — every error logged and surfaced
[ ] No set -e — errors handled explicitly
[ ] No credentials in code — [PLACEHOLDER] only
[ ] No direct root execution — ${CE_PRIV} for privileged ops
[ ] Single confirmation prompt before system changes begin
[ ] Log file written to ~/.local/logs/ceos_installer/
[ ] Comments explain intent, not mechanics
[ ] Complex functions extracted to library if they disrupt script flow
[ ] Load-order dependencies declared in Depends: field (not just numbered prefix)
4.2 Header templates — copy/paste
Bootstrap (Class A — POSIX sh throughout):
#!/bin/sh
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# bootstrap.sh
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: Bootstrap — Class A, POSIX sh throughout
# ---------------------------------------------------------------------------
# Purpose: Ensure bash present, detect environment, write ce_env.conf,
# validate compatibility, hand off to ce-install.sh.
# ---------------------------------------------------------------------------
Orchestrator:
#!/usr/bin/env bash
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# ce-install.sh
# /opt/ceos/scripts/ce-install.sh
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: Orchestrator
# ---------------------------------------------------------------------------
# Purpose:
# Target:
# Entry:
# Sources: ce-common-lib.sh, ce-test-lib.sh
# Calls: ce-base.sh, ce-minimal.sh, ce-basic.sh
# ---------------------------------------------------------------------------
# Phases:
# 0 — Pre-flight (environment checks, no system changes)
# 1 — Base (Tier 0)
# 2 — Minimal (Tier 1)
# 3 — Basic (Tier 2)
# N — Cleanup (always runs via trap)
# ---------------------------------------------------------------------------
Subscript:
#!/usr/bin/env bash
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# ce-base.sh
# /opt/ceos/scripts/ce-base.sh
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: Subscript — Tier 0 Base installation
# ---------------------------------------------------------------------------
# Purpose:
# Target:
# Entry:
# Depends: ce-common-lib.sh, ce-pkg-lib.sh
# ---------------------------------------------------------------------------
Library:
#!/usr/bin/env bash
# Built standing on the shoulders of billions of dwarves
# Created by John A. Hoeven with the ethical assistance of Claude AI
# Licence: The Unlicense — https://unlicense.org
# ---------------------------------------------------------------------------
# ce-pkg-lib.sh
# /opt/ceos/lib/ce-pkg-lib.sh
# Version: v0.0.1 | Status: DEVELOPMENT
# Role: Library — Package manager detection and operations
# ---------------------------------------------------------------------------
# Purpose:
# Used by:
# ---------------------------------------------------------------------------
# This file is a library. Source it — do not run it directly.
# ---------------------------------------------------------------------------
Line limit override (add to header when applicable):
# Line limit override: <concise reason>
Built standing on the shoulders of billions of dwarves
Created by John A. Hoeven with the ethical assistance of Claude AI
Licence: The Unlicense — commercial use explicitly permitted.
We ask that you voluntarily attribute the dwarves.
-
OHIOD — Organic Humanoid Input/Output Device. The person at the keyboard. CE OS uses this term throughout to remind both the system and its developers that the human is the principal, not the machine. ↩︎