Update Aliases Follow-up: Remove Duplication, Cover Skip Paths Design
Problem
PR #143 (closed issue #142) added _up_sync_aliases to regenerate bin/aliases.sh during bin/update.sh. The implementation works and passes the issue’s acceptance criteria, but a follow-up audit found three issues:
- Duplicate prefix-detection.
bin/lib/update.sh:_up_aliases_read_prefix(lines 268-289) reimplements the same logic asbin/install/aliases.sh:_aliases_read_existing_prefix(line 199), with subtle differences:_up_aliases_read_prefixparses the# Prefix:header with a robust whitespace-trim and adds an alias-name-parse fallback (alias <prefix>-check-health=...)._aliases_read_existing_prefixparses the same header with a single-space assumption and has no fallback._aliases_mainin the install module already calls_aliases_read_existing_prefixwhen--prefixis omitted (lines 277-286), so the update path doesn’t need its own prefix reader.
-
Shell-profile side effect on every update.
_up_sync_aliasescallsbin/install.sh aliases --prefix <prefix>, which goes through_aliases_main→_aliases_install_source_line. That function rewrites the marker line in.bashrc/.zshrcon every update. Idempotent (replaces with identical content when path is unchanged), but the file gets touched on disk, changing mtime and triggering backup/editor “file changed externally” alerts. - Test coverage gaps. Five code paths through
_up_sync_aliasesexist; only 2 are tested (primary prefix detection + dry-run). The fallback prefix detection (alias <prefix>-check-healthparse), the two skip paths (aliases.shmissing; prefix cannot be detected), and the regen-failure path are all untested.
The audit was filed against the merged state; this PR addresses items 1-3.
Scope
In scope:
- Merge the prefix-detection logic into the single canonical location (
_aliases_read_existing_prefixinbin/install/aliases.sh), gaining the alias-name fallback there. - Delete
_up_aliases_read_prefixfrombin/lib/update.sh(no longer needed). - Add a
--no-profileflag to_aliases_mainthat suppresses_aliases_install_source_line. Documented in_aliases_show_help. - Simplify
_up_sync_aliasesto callbin/install.sh aliases --no-profile— no manual prefix detection, no--prefixargument, no_up_aliases_read_prefixcall. The install module handles prefix detection (now with the robust fallback) on its own. - Add tests for the missing paths: alias-name fallback in
_aliases_read_existing_prefix; empty-return when both methods fail;--no-profileskips source-line install; update step skip-on-missing-file; update step regen-failure path.
Out of scope:
- Step placement in
run_update(audit item 3 — defensible as-is). - Heredoc / copilot firewall note (audit item 5 — operational).
- Changes to other update steps (
_up_sync_env,_up_sync_deps,_up_migrate,_up_restart). - Changes to other Django commands, BATS infrastructure, or CI.
Approach
The user’s “don’t repeat ourselves” direction makes the single-public-entry-point approach the right choice. Three approaches considered during brainstorming:
- A: Add a
--no-profileflag and delete_up_aliases_read_prefix. ← chosen. - B: Extract
_aliases_generateto a separatebin/lib/aliases.shlibrary. Cleaner long-term separation but bigger refactor for one consumer. - C: Restructure the template to not auto-run
_aliases_mainon source. Subtle blast radius ininstall.sh.
A wins because the install.sh aliases entry already does almost everything (_aliases_main reads the existing prefix, falls back interactively or via the existing prefix when not given, calls _aliases_generate to regenerate, then optionally calls _aliases_install_source_line). The only missing piece for the update use case is “skip the source-line install” — a one-flag addition.
bin/install/aliases.sh changes
-
Enhance
_aliases_read_existing_prefix(line 199). Current implementation only parses the# Prefix:header. Add a fallback that parses analias <prefix>-check-health=line when the header is missing/corrupt:_aliases_read_existing_prefix() { if [[ -f "$ALIASES_FILE" ]]; then local line line="$(grep -m1 '^# Prefix:' "$ALIASES_FILE" 2>/dev/null || true)" if [[ -n "$line" ]]; then # Trim leading/trailing whitespace after the colon. line="${line#*:}" line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" echo "$line" return 0 fi # Fallback: extract prefix from the first 'alias <prefix>-check-health=' line. line="$(grep -m1 '^alias [^=]*-check-health=' "$ALIASES_FILE" 2>/dev/null || true)" if [[ -n "$line" ]]; then line="${line#alias }" line="${line%%=*}" echo "${line%-check-health}" return 0 fi fi echo "" }The robust trim replaces the existing
${line#*: }(which assumed a single space after the colon). The fallback mirrors the logic from the now-deleted_up_aliases_read_prefix. -
Add
--no-profileflag to_aliases_main(line 215). New local variable in the arg-parse loop, new case branch, new guard around_aliases_install_source_line:_aliases_main() { local prefix="" local action="setup" local skip_source_line=false local -a args=("$@") local i=0 while [[ $i -lt ${#args[@]} ]]; do case "${args[$i]}" in --prefix) ... ;; --remove) action="remove"; i=$((i + 1)) ;; --list) action="list"; i=$((i + 1)) ;; --help|-h) action="help"; i=$((i + 1)) ;; --no-profile) skip_source_line=true; i=$((i + 1)) ;; *) ... ;; esac done case "$action" in ... setup) ... info "Using prefix: $prefix" _aliases_generate "$prefix" export ALIAS_PREFIX="$prefix" [ "$skip_source_line" = false ] && _aliases_install_source_line ;; esac } -
Update
_aliases_show_helpto document the new flag.
bin/lib/update.sh changes
-
Delete
_up_aliases_read_prefix(lines 268-289). ~22 lines gone. -
Simplify
_up_sync_aliases(lines 292-321):_up_sync_aliases() { local aliases_file="$BIN_DIR/aliases.sh" if [ ! -f "$aliases_file" ]; then _up_log "INFO" "Aliases not configured, skipping aliases sync" return 0 fi _up_log "INFO" "Regenerating aliases from install template" if [ "$_up_dry_run" = true ]; then _up_log "INFO" "Dry-run: would run install.sh aliases --no-profile" return 0 fi if ! (cd "$PROJECT_DIR" && "$BIN_DIR/install.sh" aliases --no-profile </dev/null); then _up_log "WARN" "Alias regeneration failed; keeping existing aliases" return 0 fi _up_log "OK" "Aliases regenerated from install template" return 0 }Net: the function shrinks from ~30 lines to ~20. No more prefix detection, no more
--prefixargument to construct.install.sh aliases(via_aliases_main→_aliases_read_existing_prefix) handles prefix discovery on its own.
Edge cases
- Missing
bin/aliases.sh—_up_sync_aliasesshort-circuits with an INFO log; never invokesinstall.sh. Same behavior as before. - Corrupted
# Prefix:header — the new fallback in_aliases_read_existing_prefixparses thealias <prefix>-check-health=line. If both methods fail, returns empty. Then_aliases_mainline 278 falls back to the hardcoded"sm"default — same as today. - Non-interactive stdin (
bin/update.shalways runs non-interactively from cron or scripts)._aliases_mainline 280-286 detects[[ -t 0 ]]is false and uses the fallback prefix without prompting. The</dev/nullredirect in_up_sync_aliasesguarantees stdin is non-interactive. install.sh aliases --no-profileinvoked manually by an operator. The flag is documented in_aliases_show_help; it’s a valid use case for someone who wants the file regenerated without touching their profile.- Dry-run mode — logs the intended
install.sh aliases --no-profileinvocation and returns. No filesystem changes. - Concurrent runs —
install.sh aliasesusescat > $ALIASES_FILE <<..., which is atomic-ish on POSIX filesystems (the file is created with the full content). No locking, but the regen is small enough that a race is improbable. - First-ever invocation when
aliases.shdoesn’t exist yet —_up_sync_aliasesskips. User runsbin/install.sh aliasesmanually first. After that, every subsequent update keeps the file in sync. - User removed the source line manually from their profile (without
--remove) — every update would re-add it under the current implementation. After this PR,--no-profilemeans the profile is never touched by the update path, even if the user wants the source line. Existing source line in the profile keeps pointing at the (now-rewritten)bin/aliases.sh, so things continue to work. This is the correct behavior — update should never modify the user’s shell profile. _aliases_install_source_lineis still invoked bybin/install.sh aliases(the interactive flow) — users runningbin/install.sh aliasesmanually still get their profile updated. Only the update path skips the profile manipulation. The split is correct.
Testing
Tests for bin/install/aliases.sh
Add to bin/tests/test_install.bats (or create bin/tests/test_install_aliases.bats if you prefer separation). Five tests:
_aliases_read_existing_prefix returns prefix from # Prefix: header— fixture file with# Prefix: maint, function returns"maint"._aliases_read_existing_prefix falls back to alias-name parsing when header missing— fixture file with NO# Prefix:line, but withalias custom-check-health='...'. Function returns"custom"._aliases_read_existing_prefix returns empty when both methods fail— fixture file with no header and no-check-healthalias. Function returns empty string._aliases_main --no-profile regenerates aliases but does not modify profile— fixture: a tempHOMEwith empty.bashrc. Run_aliases_main --prefix sm --no-profile. Assertaliases.shwas created. Assert.bashrcis unchanged (still empty)._aliases_main --prefix sm (without --no-profile) DOES modify profile— sanity check: same fixture, without the flag,.bashrcnow contains the source line. Locks the default behavior.
Tests for bin/lib/update.sh
Update bin/tests/test_update.bats:
- Delete the existing “update lib reads alias prefix from generated aliases file” test — function it tests (
_up_aliases_read_prefix) is gone. The equivalent behavior is tested in test_install.bats (test 1 above). - Keep the dry-run test, update the expected log message — was
"Dry-run: would run install.sh aliases --prefix sm", becomes"Dry-run: would run install.sh aliases --no-profile"(no prefix argument any more). - Add: skip path when
bin/aliases.shdoesn’t exist — tempBIN_DIRwith noaliases.sh. Run_up_sync_aliases. Assert exit 0 and--partial "Aliases not configured"log. - Add: regen failure path — temp
BIN_DIRwith analiases.shand a fakeinstall.shthat exits 1. Run_up_sync_aliases. Assert exit 0 (best-effort) and--partial "Alias regeneration failed"log.
Coverage
After the changes, every branch of _up_sync_aliases is exercised:
- File missing → tested (new test 3 above).
- Dry-run → tested (existing test).
- Regen failure → tested (new test 4 above).
- Regen success — not directly tested in BATS (would require a fully working
install.shsetup), but the dry-run + skip + failure paths cover everything except the happy path’s command output. Acceptable for a smoke test.
Every branch of _aliases_read_existing_prefix is exercised:
- Header present → tested (test 1).
- Header missing, alias fallback present → tested (test 2).
- Both missing → tested (test 3).
--no-profile flag is covered both positively (test 4: profile NOT modified) and negatively (test 5: profile IS modified without the flag).
Notes for implementation
- Single PR, one logical commit. The refactor and the test additions are tightly coupled — splitting would leave intermediate states with deleted helpers and untested new behavior.
bin/aliases.shis gitignored — don’t commit a regenerated local copy. The template (bin/install/aliases.sh) is what gets committed.- Match existing log style.
_up_log "INFO" ...,_up_log "OK" ...,_up_log "WARN" ...are the patterns inbin/lib/update.sh. Theinfo/success/warn/errorfunctions inbin/install/aliases.shcome fromlib/logging.sh. Use the right helper for each file. - No interactive prompts in the update path. The
</dev/nullredirect on theinstall.sh aliasescall already prevents prompts, but_aliases_main’s[[ -t 0 ]]guard at line 280 also detects non-interactive stdin and uses the fallback prefix automatically. Both defenses in place. _aliases_show_helpis in the same file (around line 52). Add a single line for--no-profile:# --no-profile Regenerate aliases file but do not modify shell profile- Don’t change
bin/install.shitself — the only changes are in the sourcedbin/install/aliases.shmodule. - Backward compatibility for
install.sh aliases: existing invocations (no--no-profile) keep the old behavior (profile modified). The flag is additive; no breaking change for any caller.