testsAndMisc/docs/shell-split-recipes.md
Krzysztof kuhy Rudnicki dadd33f82d
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Split install_joplin.sh and establish the shell-split pattern
299 -> 103, with the --with-server half in lib/joplin_server.sh. First
of the 84 linux_configuration files, done slowly to settle the pattern.

Three things bash -n and shellcheck do not catch, all found here:

- SCRIPT_DIR from "$0" breaks when the entry script is itself sourced.
  Use "${BASH_SOURCE[0]}"; keep readlink -f for the one symlinked script.
- A lib needs a shebang AND +x: without the shebang shellcheck raises
  SC2148, with it the shebang-executable hook fails. Match common.sh.
- Only a stubbed run proves the source line resolves. Copy entry+lib to
  a mktemp -d, sed `main` into a declare -F probe, run that. No
  installer step executes.

Also enumerates which scripts escape the repo: three copied out by
install_pacman_hooks.sh, one symlink, everything else generated. All
recorded in docs/shell-split-recipes.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:33:59 +02:00

4.1 KiB

Shell split recipes for the 250-line cap

Split out of refactor_claude_todo_resume.md (which is itself capped). Applies to linux_configuration/ and phone_focus_mode/.

linux_configuration and phone_focus_mode have no test suite and no coverage gate, so none of the above transfers. Decision 6 governs: bash -n + shellcheck + systemctl cat path checks, and never execute an enforcement script that mutates the live system or the phone.

  • After each split, bash -n and shellcheck the entry script and every new lib. Neither catches a source line that resolves to the wrong path — both passed clean on a split whose lib was unreachable at runtime.
  • So the real check is a stubbed run: copy the entry script and its libs to a mktemp -d, sed the final main invocation into a declare -F probe, and run it. That executes the source lines and nothing else. This caught the bug below; no static check did.

Use ${BASH_SOURCE[0]}, not $0

The repo's existing convention is SCRIPT_DIR="$(dirname "$(readlink -f "$0")")", which works only while the script is executed directly. $0 is the sourcing script, so the moment an entry script is itself sourced, SCRIPT_DIR points somewhere else and the lib is not found. Write new ones as:

SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"

readlink -f is still required — it is what makes the one symlinked script (/usr/local/bin/start-player2) resolve back into the repo.

Shape of a new lib (two hooks disagree unless you get this right)

Follow scripts/lib/common.sh: shebang present, file executable. The two obvious-looking alternatives each fail a hook:

  • No shebang → shellcheck SC2148 ("target shell unknown").
  • Shebang but not executable → check-shebang-scripts-are-executable.

So: #!/bin/bash, chmod +x, and no set -euo pipefail — a sourced lib inherits the caller's strict mode, and redeclaring it is duplicated boilerplate that feeds jscpd. Say so in the lib's header comment.

  • Two live units point at repo paths and break if a file moves or is renamed. systemctl cat them after touching either:

    • /etc/systemd/system/dns-blocklist-refresh.service → scripts/single_use/features/setup_dns_blocker.sh refresh
    • /etc/systemd/system/media-organizer.service → scripts/single_use/utils/organize_downloads.sh

    Every other in-repo unit references an installed copy under /usr/local/, so those are only at risk if the installer is what you split.

Which scripts escape the repo (enumerated 2026-08-16)

A split is only safe if the file still resolves its libs from wherever it actually runs. Two ways a script leaves the repo, both silent when broken:

  • Copied out — only three, all under scripts/periodic_background/hosts/guard/pacman-hooks/, installed to /usr/local/share/hosts-guard/ by hosts/guard/install_pacman_hooks.sh. Splitting any of those three means the installer must copy the new libs too, in the same commit — the same trap as phone_focus_mode/deploy.sh.
  • Symlinked in — exactly one: /usr/local/bin/start-player2 → scripts/gaming/start-player2.sh. ${BASH_SOURCE[0]%/*} resolves to /usr/local/bin there, so a lib path must go through readlink -f "${BASH_SOURCE[0]}" first.

Everything else under /usr/local/ is generated by an installer (heredoc or cat >), not copied, so splitting the generator is safe as long as the generated content is unchanged. Re-run both checks if that assumption is ever in doubt:

find /usr/local/bin /usr/local/sbin -maxdepth 1 \( -type l -o -name '*.sh' \) \
  -exec readlink -f {} \; | grep testsAndMisc
grep -rn -E '^\s*(sudo )?(install|cp)\s.*(SCRIPT_DIR|REPO).*/usr/local/' \
  linux_configuration --include='*.sh'
  • Prefer files that nothing references first — pure lint risk.
  • Watch jscpd on the first multi-lib commit: repeated set -euo pipefail + source-guard headers across several new libs is exactly what pushes duplication toward the 2% threshold, and it only fires on the real git commit.