#!/bin/bash # generate-modules.sh — regenerate all sentinel regions from modules.conf # # Usage: # ./generate-modules.sh — apply changes in-place # ./generate-modules.sh --dry-run — print a unified diff, make no changes # # Sentinel format in target files: # bash/sh: # BEGIN GENERATED MODULES: / # END GENERATED MODULES: # markdown: / set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SETUP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CONF="$SETUP_DIR/modules.conf" TUI="$SETUP_DIR/tui-install.sh" AF="$SETUP_DIR/generate-answerfile.sh" DOCS="$SETUP_DIR/../docs/md/modules.md" DRY_RUN=false [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true # ── Parse modules.conf ──────────────────────────────────────────────────────── declare -a M_IDS M_CATS M_DESCS M_DEFAULTS M_EXCLUDES skipped=() while IFS='|' read -r id cat desc def excl; do # Strip leading/trailing whitespace from each field id="${id#"${id%%[![:space:]]*}"}"; id="${id%"${id##*[![:space:]]}"}" [[ "$id" =~ ^# || -z "$id" ]] && continue M_IDS+=("$id") M_CATS+=("$cat") M_DESCS+=("$desc") M_DEFAULTS+=("${def:-off}") M_EXCLUDES+=("${excl:-}") done < "$CONF" # Identify incomplete entries (missing category or description) — skip them. declare -a ACTIVE_IDS for i in "${!M_IDS[@]}"; do if [[ -z "${M_CATS[$i]}" || -z "${M_DESCS[$i]}" ]]; then skipped+=("${M_IDS[$i]}") else ACTIVE_IDS+=("$i") fi done if (( ${#skipped[@]} > 0 )); then printf '[warn] skipping %d stub(s) with empty category/description:\n' "${#skipped[@]}" >&2 printf ' %s\n' "${skipped[@]}" >&2 fi # ── Build generated sections ────────────────────────────────────────────────── # -- module-counters (tui-install.sh: count_steps function body) -------------- gen_counters="" for i in "${ACTIVE_IDS[@]}"; do id="${M_IDS[$i]}" gen_counters+=" [[ \" \$a \" == *\" ${id} \"* ]] && TOTAL=\$(( TOTAL + 1 ))\n" done # -- module-checklist (tui-install.sh and generate-answerfile.sh) ------------- # Builds the full SELECTED_APPS=$(dialog ...) call including open/close. build_checklist_tui() { local out out=' SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \\\n' out+=' --title " Applications " \\\n' out+=' --checklist "Optional applications — installed after base components:" "$_APP_H" 76 "$_APP_LIST_H" \\\n' for i in "${ACTIVE_IDS[@]}"; do local id="${M_IDS[$i]}" desc="${M_DESCS[$i]}" def="${M_DEFAULTS[$i]}" local padded padded=$(printf '%-20s' "$id") out+=" \"${id}\" \"${padded} ${desc}\" ${def} \\\\\n" done out+=' 3>&1 1>&2 2>&3) || SELECTED_APPS=""\n' printf '%s' "$out" } build_checklist_af() { local count="${#ACTIVE_IDS[@]}" local out out=" AF_APPS=\$(dialog --backtitle \"\$BACKTITLE\" \\\\\n" out+=" --title \" Applications \" \\\\\n" out+=" --checklist \"Optional applications — installed after base components:\" 40 76 ${count} \\\\\n" for i in "${ACTIVE_IDS[@]}"; do local id="${M_IDS[$i]}" desc="${M_DESCS[$i]}" def="${M_DEFAULTS[$i]}" local padded padded=$(printf '%-20s' "$id") out+=" \"${id}\" \"${padded} ${desc}\" ${def} \\\\\n" done out+=' 3>&1 1>&2 2>&3) || AF_APPS=""\n' printf '%s' "$out" } gen_checklist_tui=$(build_checklist_tui) gen_checklist_af=$(build_checklist_af) # -- module-summary (tui-install.sh: confirmation dialog) --------------------- gen_summary="" for i in "${ACTIVE_IDS[@]}"; do id="${M_IDS[$i]}" gen_summary+=" [[ \" \$SELECTED_APPS \" == *\" ${id} \"* ]] && SUMMARY+=\" ✦ ${id}\\\\n\"\n" done # -- module-conflicts (tui-install.sh: before dispatch) ----------------------- gen_conflicts="" for i in "${ACTIVE_IDS[@]}"; do id="${M_IDS[$i]}" excl="${M_EXCLUDES[$i]}" [[ -z "$excl" ]] && continue IFS=',' read -ra excl_list <<< "$excl" for eid in "${excl_list[@]}"; do eid="${eid#"${eid%%[![:space:]]*}"}"; eid="${eid%"${eid##*[![:space:]]}"}" gen_conflicts+="if [[ \" \$SELECTED_APPS \" == *\" ${id} \"* && \" \$SELECTED_APPS \" == *\" ${eid} \"* ]]; then\n" gen_conflicts+=" warn \"'${id}' and '${eid}' are mutually exclusive — deselecting '${eid}'\"\n" gen_conflicts+=" SELECTED_APPS=\" \$SELECTED_APPS \"\n" gen_conflicts+=" SELECTED_APPS=\"\${SELECTED_APPS/ ${eid} / }\"\n" gen_conflicts+=" SELECTED_APPS=\"\${SELECTED_APPS# }\"\n" gen_conflicts+=" SELECTED_APPS=\"\${SELECTED_APPS% }\"\n" gen_conflicts+="fi\n" done done # -- module-dispatch (tui-install.sh: installation section) ------------------- gen_dispatch="" for i in "${ACTIVE_IDS[@]}"; do id="${M_IDS[$i]}" gen_dispatch+="[[ \" \$SELECTED_APPS \" == *\" ${id} \"* ]] && run_module \"${id}\" \"\$APPS/${id}.sh\"\n" done # -- per-category doc tables (docs/md/modules.md) ----------------------------- declare -A gen_docs_cat for i in "${ACTIVE_IDS[@]}"; do id="${M_IDS[$i]}" cat="${M_CATS[$i]}" desc="${M_DESCS[$i]}" gen_docs_cat[$cat]+="| \`${id}\` | ${desc} |\n" done build_doc_table() { local cat="$1" local rows="${gen_docs_cat[$cat]:-}" if [[ -z "$rows" ]]; then printf '' return fi printf '| ID | Description |\n|----|-------------|\n' printf '%b' "$rows" } # ── Splice helper (uses python3 for safe multiline replacement) ─────────────── splice() { local file="$1" tag="$2" content="$3" style="${4:-bash}" local tmpfile tmpfile=$(mktemp) printf '%b' "$content" > "$tmpfile" if $DRY_RUN; then python3 - "$file" "$tag" "$tmpfile" "$style" "dry" <<'PYEOF' import sys, re, difflib filepath, tag, tmpfile, style, _ = sys.argv[1:] with open(tmpfile) as f: new_content = f.read() if style == 'md': begin = f'' end = f'' else: begin = f'# BEGIN GENERATED MODULES: {tag}' end = f'# END GENERATED MODULES: {tag}' with open(filepath) as f: original = f.read() pattern = re.compile(re.escape(begin) + r'\n.*?' + re.escape(end), re.DOTALL) if not pattern.search(original): print(f'ERROR: sentinel not found in {filepath}: {begin}', file=sys.stderr) sys.exit(1) replacement = begin + '\n' + new_content + end updated = pattern.sub(lambda m: replacement, original) if updated == original: print(f' (no change) {filepath} [{tag}]', file=sys.stderr) sys.exit(0) diff = difflib.unified_diff( original.splitlines(keepends=True), updated.splitlines(keepends=True), fromfile=f'{filepath}', tofile=f'{filepath} (generated)', ) sys.stdout.writelines(diff) PYEOF else python3 - "$file" "$tag" "$tmpfile" "$style" <<'PYEOF' import sys, re filepath, tag, tmpfile, style = sys.argv[1:] with open(tmpfile) as f: new_content = f.read() if style == 'md': begin = f'' end = f'' else: begin = f'# BEGIN GENERATED MODULES: {tag}' end = f'# END GENERATED MODULES: {tag}' with open(filepath) as f: original = f.read() pattern = re.compile(re.escape(begin) + r'\n.*?' + re.escape(end), re.DOTALL) if not pattern.search(original): print(f'ERROR: sentinel not found in {filepath}: {begin}', file=sys.stderr) sys.exit(1) replacement = begin + '\n' + new_content + end updated = pattern.sub(lambda m: replacement, original) if updated == original: print(f' (no change) {filepath} [{tag}]', file=sys.stderr) sys.exit(0) with open(filepath, 'w') as f: f.write(updated) print(f' (updated) {filepath} [{tag}]', file=sys.stderr) PYEOF fi rm -f "$tmpfile" } # ── Apply all regions ───────────────────────────────────────────────────────── echo "==> tui-install.sh" splice "$TUI" "module-counters" "$gen_counters" splice "$TUI" "module-checklist" "$gen_checklist_tui" splice "$TUI" "module-summary" "$gen_summary" splice "$TUI" "module-conflicts" "$gen_conflicts" splice "$TUI" "module-dispatch" "$gen_dispatch" echo "==> generate-answerfile.sh" splice "$AF" "module-checklist" "$gen_checklist_af" echo "==> docs/md/modules.md" for cat in ai networking dev system gaming notes media graphics video audio browsers editors virt productivity infra; do table=$(build_doc_table "$cat") [[ -z "$table" ]] && continue splice "$DOCS" "$cat" "${table}"$'\n' md done echo "Done."