|
11 | 11 | # apt omz tmux-plugins zsh-plugins fzf rg fd shellcheck |
12 | 12 | # zoxide delta eza uv ruff neovim cheat pre-commit xcape |
13 | 13 | # |
| 14 | +# A PATH shadow check always runs at the end (read-only). It detects older |
| 15 | +# binaries at higher-priority PATH locations that would hide managed versions |
| 16 | +# installed to /usr/local/bin (e.g. nvim, xcape). |
| 17 | +# |
14 | 18 | # Examples: |
15 | | -# ./update.sh --check # check all versions |
16 | | -# ./update.sh --check neovim fzf # check only neovim and fzf |
17 | | -# ./update.sh neovim # update only neovim |
| 19 | +# ./update.sh --check # check all versions + shadow report |
| 20 | +# ./update.sh --check neovim fzf # check only neovim and fzf (+ shadow report) |
| 21 | +# ./update.sh neovim # update only neovim (+ shadow report) |
18 | 22 | # NOSUDO=1 ./update.sh # skip apt upgrade, skip all sudo operations |
19 | 23 |
|
20 | 24 | set -euo pipefail |
@@ -179,6 +183,80 @@ _do_update_neovim() { |
179 | 183 | _verify_dest nvim "$nvim_dest" |
180 | 184 | } |
181 | 185 |
|
| 186 | +# ── PATH shadow check ───────────────────────────────────────────────────────── |
| 187 | +# Detect binaries at higher-priority PATH locations that shadow dotfiles-managed |
| 188 | +# versions at /usr/local/bin. Runs always (read-only, no side effects). |
| 189 | +# |
| 190 | +# Only /usr/local/bin tools need this check — tools at ~/.local/bin are already |
| 191 | +# at the highest dotfiles-managed PATH priority and cannot be shadowed by the |
| 192 | +# installer itself. |
| 193 | +_check_path_shadows() { |
| 194 | + log_step "PATH shadow check" |
| 195 | + |
| 196 | + # Collect PATH dirs that appear before /usr/local/bin. |
| 197 | + # These are the only locations that can shadow /usr/local/bin binaries. |
| 198 | + local -a _before=() _all_dirs=() |
| 199 | + local _dir _found_usr_local=false |
| 200 | + IFS=: read -ra _all_dirs <<< "${PATH:-}" |
| 201 | + for _dir in "${_all_dirs[@]}"; do |
| 202 | + if [ "$_dir" = "/usr/local/bin" ]; then |
| 203 | + _found_usr_local=true |
| 204 | + break |
| 205 | + fi |
| 206 | + _before+=("$_dir") |
| 207 | + done |
| 208 | + |
| 209 | + # If /usr/local/bin is not in PATH at all, the check is meaningless — |
| 210 | + # managed binaries there are unreachable regardless of shadows. |
| 211 | + if ! $_found_usr_local; then |
| 212 | + log_info "PATH shadow check: /usr/local/bin not in PATH — skipping (run from zsh after exec zsh)" |
| 213 | + return 0 |
| 214 | + fi |
| 215 | + |
| 216 | + if [ ${#_before[@]} -eq 0 ]; then |
| 217 | + log_ok "/usr/local/bin is first in PATH — no shadow risk" |
| 218 | + return 0 |
| 219 | + fi |
| 220 | + |
| 221 | + local _any_issue=false _any_checked=false _tool _canonical _shadow _cv _sv |
| 222 | + for _tool in nvim xcape; do |
| 223 | + _canonical="/usr/local/bin/$_tool" |
| 224 | + [ -x "$_canonical" ] || continue # not installed at /usr/local/bin |
| 225 | + _any_checked=true |
| 226 | + |
| 227 | + _shadow="" |
| 228 | + for _dir in "${_before[@]}"; do |
| 229 | + [ -x "$_dir/$_tool" ] && _shadow="$_dir/$_tool" && break |
| 230 | + done |
| 231 | + |
| 232 | + [ -z "$_shadow" ] && continue # no shadow for this tool — silent in clean case |
| 233 | + |
| 234 | + _cv=$(_cmd_version "$_canonical" --version) || _cv="" |
| 235 | + _sv=$(_cmd_version "$_shadow" --version) || _sv="" |
| 236 | + |
| 237 | + if [ -z "$_cv" ] || [ -z "$_sv" ]; then |
| 238 | + log_warn "$_tool: $_shadow shadows /usr/local/bin/$_tool (cannot read versions — inspect manually)" |
| 239 | + _any_issue=true |
| 240 | + elif _ver_older_than "$_sv" "$_cv"; then |
| 241 | + log_warn "$_tool: $_shadow ($_sv) shadows /usr/local/bin/$_tool ($_cv)" |
| 242 | + log_warn " Fix: rm $_shadow" |
| 243 | + _any_issue=true |
| 244 | + elif [ "$_sv" = "$_cv" ]; then |
| 245 | + log_warn "$_tool: duplicate at $_shadow — same version as /usr/local/bin/$_tool" |
| 246 | + log_warn " Consider: rm $_shadow" |
| 247 | + _any_issue=true |
| 248 | + else |
| 249 | + log_info "$_tool: $_shadow ($_sv) supersedes /usr/local/bin/$_tool ($_cv) — custom newer version" |
| 250 | + fi |
| 251 | + done |
| 252 | + |
| 253 | + if ! $_any_checked; then |
| 254 | + log_info "No tools installed at /usr/local/bin — shadow check not applicable" |
| 255 | + elif ! $_any_issue; then |
| 256 | + log_ok "No PATH shadows detected for /usr/local/bin tools" |
| 257 | + fi |
| 258 | +} |
| 259 | + |
182 | 260 | # ── System packages ──────────────────────────────────────────────────────────── |
183 | 261 | log_info "Checking sudo access…" |
184 | 262 | detect_sudo |
@@ -453,6 +531,9 @@ if _should_run pre-commit; then |
453 | 531 | fi |
454 | 532 | fi |
455 | 533 |
|
| 534 | +# ── PATH shadow check (always runs — read-only) ─────────────────────────────── |
| 535 | +_check_path_shadows |
| 536 | + |
456 | 537 | echo "" |
457 | 538 | if $CHECK_ONLY; then |
458 | 539 | log_ok "Check complete" |
|
0 commit comments