Styleguides/bash-scripting-styleguide/bash-scripting-styleguide.md

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:

  1. Detect the running architecture (uname -m)
  2. Consult ce-index.conf for known arch-specific availability
  3. Test that each required package is available for that architecture before attempting installation
  4. Log clearly which packages are unavailable and why
  5. 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 vim is noise. # vim is the CE OS default editor — required for all tiers is 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 function keyword — use name() { style
  • Every function documents its arguments with ${VAR:?message} or explicit checks
  • All functions return 0 on 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:

  1. Consult ce-index.conf for known arch-specific status
  2. Alpine stable (main)
  3. Alpine community
  4. Alpine edge
  5. 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.conf and 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.


  1. 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. ↩︎