feat(scripts): manage personal CLI scripts via bin/ and symlinks - #11
Open
jiunbae wants to merge 5 commits into
Open
feat(scripts): manage personal CLI scripts via bin/ and symlinks#11jiunbae wants to merge 5 commits into
jiunbae wants to merge 5 commits into
Conversation
Add a `scripts` component that links the scripts in bin/ into ~/.local/bin. That directory precedes ~/.scripts on PATH, so the repo copy overrides an older one (e.g. shared over NFS) without deleting it, and removing a link rolls straight back to that copy. Rewrite subd (v2). It ran every command through an interactive shell, which cost ~880ms of rc/prompt startup per directory and emitted noise that a broad sed filter then stripped — along with real output such as `[2026-08-24 ERROR] db connection failed`. It also flattened the command into a string, so quoting and globs were corrupted, and it always exited 0 regardless of what the child commands did. v2 execs argv directly with no shell in between, keeps -i as an opt-in for alias expansion, propagates a non-zero exit code, and validates --depth (`-d abc` silently ran nothing before). Adds --parallel/-j, --only-git, --timeout, --dry-run, -x, --prune, and TTY-aware color. On a 26-directory tree, `git status --short` goes from 30.0s to 0.24s, or 0.05s with --only-git --parallel; output is byte-identical to v1 once the prompt noise is removed. Work scripts stay out of this repo since it is public: the callabo and rt-* tooling, a bootstrap script wired to internal NFS and /opt/tools paths, and an ssh helper keyed to office/IDC host groups. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`set -u` made bash 3.2 and 4.3 treat an empty array expansion as an
unbound variable, and `pre` is empty whenever --timeout is unset — that
is every default invocation. subd died on the first directory with
`pre[@]: unbound variable`, then again on `kids[@]` while walking the
tree. macOS ships bash 3.2, and the `scripts` component is part of
--all, so this broke every macOS install.
Expand possibly-empty arrays as ${arr[@]+"${arr[@]}"}, which is safe on
every version, and fall back to batch waiting where `wait -n` (bash
4.3+) is unavailable.
Verified on 3.2.57, 4.3.48, 4.4.23 and 5.0.18: sequential and parallel
runs both complete with no unbound-variable errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping a script from bin/ left its symlink behind in ~/.local/bin, pointing at a path that no longer exists — a dangling entry on PATH that no later run cleaned up. Removing rt-*, install.sh and ssh-list from bin/ during this branch's own review required deleting five links by hand. Remove links that point into our bin/ and no longer resolve, so re-running the component converges instead of accumulating. bin/ is a PATH directory, so a file without the execute bit would link in as an unrunnable command. Skip those with a warning naming the fix rather than linking them silently. Report linked, current, skipped and pruned separately; the previous counter conflated "already current" with "skipped". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
install_scripts returned 1 when bin/ was missing, and install.sh runs under `set -euo pipefail` with an ERR trap, so one absent directory aborted the whole run — `./install.sh scripts ssh` exited 1 with ssh never reaching its module. modules/ssh.sh:76 already handles the same case with log_warn and return 0; match it. bundle.sh embedded install.sh, lib/, modules/ and configs/ but not bin/, so the bundled installer shipped modules/scripts.sh with nothing for it to link — the exact failure its own comment warns about. Embed bin/ alongside the other trees. The idempotence check only recognised symlinks, so under --copy, which bundle.sh always passes, every re-run backed up and rewrote all of bin/ and left executable .backup.<ts> files in a PATH directory. Compare content in copy mode instead. In subd, the `gitstatus` branch of NOISE_RE had no anchor, so under -i any line containing that substring was dropped — `git log` output mentioning gitstatus vanished, which is the defect v2 was meant to end. Anchor it to the p10k banner. Child commands also inherited subd's own stdin, so in sequential mode the first directory drained it and the rest read nothing, differing from parallel mode where bash redirects async commands from /dev/null; give every child /dev/null. And "no matching directories" returned 0, reporting success for a run that executed nothing: return 3, as grep does for no matches. pip-install.sh never validated its argument, so a bare invocation expanded "$TARGET_DIR"/* to /* and ran pip install -e against every top-level directory. It also discarded all pip diagnostics, always exited 0, and used a regex membership test where a literal one was meant. It is now on PATH under its own name, so guard the argument, let pip's output through, and propagate failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These came over from ~/.scripts unchanged. Linking them onto PATH under a component that also runs on macOS made their existing gaps reachable, so fix them here rather than shipping them as-is. mkln built relative links with `realpath --relative-to`, a GNU coreutils flag macOS does not have — there, realpath is absent from the base system and the BSD build rejects the flag, so every link target came out empty while the script still reported success. Compute the relative path in shell instead. An empty source directory also left a link literally named `*`, since the unmatched glob was used as a filename; `ln -s` failures were ignored the same way. Guard with nullglob, count failures, and drop the unused absolute_source_dir. machine-info assumed lscpu, free and nvidia-smi all exist. Without lscpu the core arithmetic failed outright; without nvidia-smi it printed the empty `GPU: ()`; and its single-line awk reformatter mangled every GPU model after the first on heterogeneous hosts. Read from sysctl/vm_stat on Darwin, degrade to "unknown"/"unavailable"/"none detected" when a tool is missing, and print one line per GPU model. Core count now multiplies by socket count, which the old expression omitted. get_audio_duration.sh emitted a data row for the unexpanded glob when a directory held no .wav, folding a phantom file into the CSV a consumer reads; convert_sample_rate.sh next door already guards this. mp4-to-wav.sh exited 0 after failing every conversion, so `mp4-to-wav.sh dir && train` proceeded with no WAVs. In subd, trim_blank_edges forked awk per directory even though `$(...)` has already stripped trailing newlines; skip it when the output has no whitespace edge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
scriptscomponent that linksbin/into~/.local/binviabackup_and_linksubd(v2): direct argv exec, exit-code propagation,--parallel,--only-git,--timeout,--dry-runWhy subd was rewritten
v1 ran every command through
$SHELL -i, which cost ~880ms of rc/prompt startup per directory. The prompt noise that produced was stripped by a broadsedfilter that also deleted real output — a line like[2026-08-24 ERROR] db connection failednever reached the terminal. It flattened the command into a string (${COMMAND[*]}), so quoting and globs were corrupted, and it always exited 0 no matter what the child commands did.v2 execs argv directly with no shell in between and keeps
-ias an opt-in for alias expansion.--depthis validated (-d abcpreviously ran nothing and reported success), failures propagate to the exit code, and colour is TTY-aware.On a 26-directory tree,
git status --shortgoes from 30.0s to 0.24s, or 0.05s with--only-git --parallel. Output is byte-identical to v1 once the prompt noise is removed.Portability
set -umakes bash 3.2 and 4.3 treat an empty array expansion as an unbound variable, and subd'sprearray is empty whenever--timeoutis unset — every default invocation. macOS ships bash 3.2 and thescriptscomponent is part of--all, so subd died on the first directory of every macOS install. Possibly-empty arrays now expand as${arr[@]+"${arr[@]}"}, andwait -n(bash 4.3+) falls back to batch waiting. Verified on 3.2.57, 4.3.48, 4.4.23 and 5.0.18.Review pass
A review of the branch turned up several things worth calling out, all fixed here:
install_scriptsreturned 1 whenbin/was missing; under install.sh'sset -euo pipefailand ERR trap that aborted the whole run, so./install.sh scripts sshexited 1 with ssh never reaching its module.modules/ssh.sh:76already handles the same case withlog_warn+return 0.bundle.shembeddedinstall.sh,lib/,modules/andconfigs/but notbin/— the bundled installer shippedmodules/scripts.shwith nothing to link, which is the failure its own comment warns about.--copy(whichbundle.shalways passes) every re-run backed up and rewrote all ofbin/, leaving executable.backup.<ts>files in a PATH directory.bin/left its symlink behind, dangling on PATH; re-running the component now prunes those instead of accumulating them.gitstatusbranch of the-inoise filter had no anchor, so any line containing that substring was dropped — the same silent-deletion defect v2 was meant to end. Child commands also inherited subd's own stdin, so the first directory drained it and the rest read nothing, differing from parallel mode; and "no matching directories" returned 0, reporting success for a run that executed nothing (now 3, as grep does).Hardening the migrated scripts
The scripts came over from
~/.scriptsunchanged. Linking them onto PATH under a component that also runs on macOS made their existing gaps reachable:mklnbuilt relative links withrealpath --relative-to, a GNU coreutils flag macOS lacks, and reported success while producing empty link targets. An empty source directory also left a link named*.machine-infoassumedlscpu,freeandnvidia-smiall exist, failed its core arithmetic without lscpu, printed an emptyGPU: ()without nvidia-smi, and mangled every GPU model after the first on heterogeneous hosts.pip-install.shnever validated its argument, so a bare invocation expanded"$TARGET_DIR"/*to/*and ranpip install -eagainst every top-level directory. It also discarded pip's diagnostics and always exited 0.get_audio_duration.shemitted a CSV row for the unexpanded glob when a directory held no.wav.mp4-to-wav.shexited 0 after failing every conversion.Scope
bin/holds seven general-purpose scripts. The callabo and rt-* tooling stays out, along withinstall.sh(wired to internal NFS and/opt/toolspaths, and destructive under--overwrite),ssh-list(keyed to office/IDC host groups), andslack/ntfy(carry endpoint values).~/.local/binprecedes~/.scriptson PATH, so these links override the copy shared over NFS without deleting it; removing a link rolls back to it.🤖 Generated with Claude Code