Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ test:
bats tests/test_privileged_setup.bats
bats tests/test_bling.bats
bats tests/test_bling_sh.bats
bats tests/test_bling_preexec_rearm.bats
bats tests/test_luks_tpm2.bats
bats tests/test_rechunker_group_fix.bats
bats tests/test_bling_fastfetch.bats
Expand Down
1 change: 1 addition & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Do not add exemptions for scripts with branching logic.
| `tests/test_setup_scripts.bats` | `ublue-system-setup`, `ublue-user-setup` — hook runner logic |
| `tests/test_privileged_setup.bats` | `ublue-privileged-setup` — privileged hook runner logic |
| `tests/test_bling.bats` | `ublue-bling` — shell config injection install/uninstall |
| `tests/test_bling_preexec_rearm.bats` | `bling/bash-preexec-rearm.sh` — DEBUG trap re-arm with array/scalar `PROMPT_COMMAND`, idempotency, degradation when bash-preexec is absent |
| `tests/test_luks_tpm2.bats` | `luks-tpm2-autounlock` — UUID parsing, device resolution, cryptenroll flag construction |
| `tests/test_rechunker_group_fix.bats` | `rechunker-group-fix` — group/gshadow append, duplicate detection, format |
| `tests/test_bling_fastfetch.bats` | `ublue-bling-fastfetch` — all 9 accent colors, dconf/gsettings fallback chain, FASTFETCH_FORCE_THEME override |
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@
],
"description": "Shell script authoring and testability. Use when writing or testing shell scripts under system_files/, removing scripts, or adding bats tests.",
"version": "1.0",
"last_updated": "2026-08-01",
"last_updated": "2026-08-06",
"doc_type": "reference"
},
{
Expand Down
37 changes: 36 additions & 1 deletion docs/skills/shell-scripts.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: shell-scripts
version: "1.0"
last_updated: "2026-08-01"
last_updated: "2026-08-06"
id: shell-scripts
one_line_purpose: Write and test shell scripts under system_files/.
entry_point: docs/skills/shell-scripts.md
Expand Down Expand Up @@ -438,6 +438,41 @@ grep -q "^name:!\*::" file
grep -qF "name:!*::" file
```

### Bash DEBUG traps are invisible inside functions

Without `set -o functrace`, bash does **not** inherit the `DEBUG` trap into
shell functions. Two consequences bite when testing or writing prompt hooks:

```bash
f() { echo "[$(trap -p DEBUG)]"; } # always prints [] — even when a trap is set
g() { trap - DEBUG; } # does NOT clear the caller's DEBUG trap
h() { trap 'cmd' DEBUG; } # DOES set the caller's DEBUG trap
```

So `trap -p DEBUG` is useless as a detector from inside a function, while
`trap ... DEBUG` from inside a function is a reliable way to (re-)install one.

For bats: `PROMPT_COMMAND` entries execute at **top level** in a real shell.
Simulate a prompt cycle with a top-level loop, never a helper function —
wrapping the cycle in a function hides `trap - DEBUG` clobbers entirely and
makes the test pass vacuously.

```bash
CYCLE='for __e in "${PROMPT_COMMAND[@]}"; do eval "$__e"; done'
```

See `tests/test_bling_preexec_rearm.bats` and
[#869](https://github.com/projectbluefin/common/issues/869).

### POSIX-`sh` files cannot hold bash array code

`system_files/**/*.sh` is shellchecked with the dialect from its shebang.
`bling.sh` is `#!/usr/bin/env sh`, so bash arrays, `BASH_SOURCE`, and `+=(...)`
trip SC3028/SC3030/SC3054 and fail CI. Put bash-only logic in a sibling
`#!/usr/bin/env bash` file and source it from inside the existing
`[ "${BLING_SHELL}" = "bash" ]` guard, with a `BLING_DIR` override so bats can
point at the repo tree instead of `/usr/share/ublue-os/bling`.

## Red Flags

- A shell script reads from a hardcoded `/proc`, `/dev`, or `/usr/share/...` path without an env-var override — untestable in CI
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Re-arm the bash-preexec DEBUG trap on every prompt.
#
# Fedora (bash >= 5.1) exposes PROMPT_COMMAND as an *array*. bash-preexec 0.6.0
# defers its own installation by appending a string to PROMPT_COMMAND:
#
# __bp_trap_string="$(trap -p DEBUG)"; trap - DEBUG; __bp_install
#
# __bp_install is supposed to delete that string again, but it only ever reads
# and rewrites "${PROMPT_COMMAND}" — which expands to element [0] alone. Once
# another hook (direnv, starship, mise, zoxide, vte, systemd) has pushed the
# installer into a later array element, it is never removed and therefore runs
# on *every* prompt. Its first act is `trap - DEBUG`, and __bp_install then
# returns early because PROMPT_COMMAND already contains __bp_precmd_invoke_cmd.
# From the second prompt onward the DEBUG trap is permanently empty, so every
# preexec consumer silently stops firing — atuin loads and CTRL+R works, but no
# command is ever recorded.
#
# Re-arming the trap at the end of each prompt cycle restores the invariant
# bash-preexec assumes without patching or vendoring bash-preexec itself.
#
# See: https://github.com/projectbluefin/common/issues/869
# https://github.com/rcaloras/bash-preexec/issues/188
# https://github.com/rcaloras/bash-preexec/issues/186

# Only meaningful when bash-preexec is actually loaded, and only safe when we
# are allowed to write PROMPT_COMMAND (bash-preexec bails out in that case too).
if [[ "$(type -t __bp_preexec_invoke_exec)" == "function" ]] &&
(unset PROMPT_COMMAND) 2>/dev/null; then

# Re-install the exact trap bash-preexec installs in __bp_install. A prior,
# non-bash-preexec DEBUG trap is not lost: bash-preexec preserves it as
# __bp_original_debug_trap inside preexec_functions.
__bling_rearm_bp_debug_trap() {
trap '__bp_preexec_invoke_exec "$_"' DEBUG
}

# Idempotent — sourcing bling.sh twice must not queue the hook twice.
if [[ "${PROMPT_COMMAND[*]-}" != *__bling_rearm_bp_debug_trap* ]]; then
if ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1))); then
PROMPT_COMMAND+=('__bling_rearm_bp_debug_trap')
elif [[ -n "${PROMPT_COMMAND:-}" ]]; then
PROMPT_COMMAND="${PROMPT_COMMAND}"$'\n'"__bling_rearm_bp_debug_trap"
else
PROMPT_COMMAND="__bling_rearm_bp_debug_trap"
fi
fi
fi
10 changes: 10 additions & 0 deletions system_files/shared/usr/share/ublue-os/bling/bling.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,13 @@ if command -v mise >/dev/null 2>&1; then
fi
fi
fi

# Keep bash-preexec's DEBUG trap alive. Must stay last so the re-arm hook is the
# final PROMPT_COMMAND entry, after every hook above has queued its own.
# See: https://github.com/projectbluefin/common/issues/869
if [ "${BLING_SHELL}" = "bash" ]; then
BLING_REARM="${BLING_DIR:-/usr/share/ublue-os/bling}/bash-preexec-rearm.sh"
# shellcheck source=/dev/null
[ -f "${BLING_REARM}" ] && . "${BLING_REARM}"
unset BLING_REARM
fi
Loading
Loading