Skip to content

Commit 9fc2b69

Browse files
feat: PATH shadow detection, locale fix, better install messaging
- update.sh: add _check_path_shadows — always runs at end (read-only); detects older/duplicate binaries at higher-priority PATH dirs that shadow /usr/local/bin managed tools (nvim, xcape) - modules/neovim.sh: add _nvim_warn_shadows helper; called at install time and on the skip-already-current path via direct file probes (bypasses install-time bash PATH ambiguity) - lib/utils.sh: add _ver_older_than using sort -V for correct semver comparison (e.g. 0.9.0 < 0.10.4) - modules/base.sh: add locales package + locale-gen en_US.UTF-8 to fix Perl locale warning on Ctrl+R (fzf history widget) on fresh Ubuntu - install.sh + modules/zsh.sh: post-install message reflects actual shell change outcome via _SHELL_IS_ZSH flag; clarifies exec zsh vs new terminal behaviour - bump version to 1.2.0
1 parent 6f86931 commit 9fc2b69

9 files changed

Lines changed: 177 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.2.0] - 2026-03-20
11+
12+
### Added
13+
- `lib/utils.sh`: `_ver_older_than` — version comparison helper using `sort -V`
14+
(GNU coreutils); returns true when `$1 < $2`, false when either arg is empty
15+
- `modules/neovim.sh`: `_nvim_warn_shadows` — detects pre-existing `nvim` copies
16+
at `~/.local/bin/nvim` or `~/bin/nvim` that shadow the managed install at
17+
`/usr/local/bin/nvim`; runs at both install time and on the skip-already-current
18+
path so a shadow is never silently missed
19+
- `update.sh`: `_check_path_shadows` — PATH shadow check that always runs at the
20+
end of `update.sh` (read-only, no side effects); walks PATH dirs before
21+
`/usr/local/bin`, reports older/duplicate/newer shadows for each managed tool
22+
(`nvim`, `xcape`); skipped with an informational message if `/usr/local/bin` is
23+
not in PATH at all
24+
25+
### Fixed
26+
- `modules/base.sh`: add `locales` package and `locale-gen en_US.UTF-8` to
27+
`install_base` — on fresh minimal Ubuntu images the locale data is absent,
28+
causing Perl to warn on every `Ctrl+R` invocation (fzf's history widget invokes
29+
`perl` for multi-line deduplication); idempotent (`locale -a` check guards re-run)
30+
- `install.sh` + `modules/zsh.sh`: post-install message now correctly reflects
31+
whether the default shell change succeeded or failed; uses `_SHELL_IS_ZSH` flag
32+
set by `_set_default_shell`; clarifies that `exec zsh` activates the shell
33+
immediately without a re-login
34+
1035
## [1.1.3] - 2026-03-18
1136

1237
### Fixed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.3
1+
1.2.0

git/.gitconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@
5151

5252
[include]
5353
path = ~/.gitconfig.local
54+
[credential "https://gitlab.gnome.org"]
55+
helper =
56+
helper = !/usr/bin/glab auth git-credential

install.sh

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,14 @@ echo ""
111111
echo " Next steps:"
112112
case "$PROFILE" in
113113
minimal|workstation)
114-
echo " • Switch to zsh now: exec zsh"
115-
echo " • (Or start a new terminal session)"
116-
echo " • Run p10k configure to customise your prompt"
114+
if ${_SHELL_IS_ZSH:-false}; then
115+
echo " • Default shell set to zsh — new terminals will open in zsh automatically"
116+
else
117+
echo " • Default shell change failed — new terminals will still use bash"
118+
echo " • To fix permanently: chsh -s $(command -v zsh) then re-login"
119+
fi
120+
echo " • Activate zsh right now (no re-login needed): exec zsh"
121+
echo " • Once in zsh: p10k configure to choose your prompt style"
117122
echo ""
118123
echo " Font (run on your LOCAL machine, not here if this is a remote/server):"
119124
echo " • ./scripts/install-fonts.sh — installs MesloLGS NF (required for icons)"

lib/utils.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,19 @@ _download_tar_bin() {
251251
rm -rf "$tmp"
252252
}
253253

254+
# Return 0 (true) if version string $1 is strictly older than $2.
255+
# Uses sort -V (GNU coreutils version sort, available on Ubuntu 20.04+).
256+
# Returns 1 (false) if either argument is empty — treats unknown as non-older.
257+
# Usage: _ver_older_than "0.9.0" "0.10.4"
258+
_ver_older_than() {
259+
local a="${1:-}" b="${2:-}"
260+
[ -z "$a" ] && return 1
261+
[ -z "$b" ] && return 1
262+
local lower
263+
lower=$(printf '%s\n%s\n' "$a" "$b" | sort -V | head -1)
264+
[ "$lower" = "$a" ] && [ "$a" != "$b" ]
265+
}
266+
254267
# Verify the installed binary at DEST is what the shell will actually resolve.
255268
# Usage: _verify_dest BINARY_NAME DEST
256269
_verify_dest() {

modules/base.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ install_base() {
1010

1111
if $CAN_SUDO; then
1212
local -a _pkgs=(
13+
locales
1314
git curl wget
1415
zsh tmux neovim
1516
ranger jq
@@ -22,6 +23,14 @@ install_base() {
2223
$SUDO apt-get -yq update
2324
apt_install "${_pkgs[@]}"
2425

26+
# Ensure en_US.UTF-8 locale is generated — without this, Perl (and tools
27+
# that shell out to it) will warn whenever LANG=en_US.UTF-8 is set but the
28+
# locale data isn't present (e.g. on fresh minimal Ubuntu installs).
29+
if ! locale -a 2>/dev/null | grep -q 'en_US.utf8'; then
30+
log_info "Generating locale: en_US.UTF-8"
31+
$SUDO locale-gen en_US.UTF-8
32+
fi
33+
2534
# fd is installed as 'fdfind' on Debian/Ubuntu — add a shim if fd is missing
2635
if ! has fd && has fdfind; then
2736
ln -sf "$(command -v fdfind)" ~/.local/bin/fd

modules/neovim.sh

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33
# Falls back to apt if GitHub is unreachable or arch is unsupported.
44
# Idempotent: skips install when the installed version already matches latest.
55

6+
# Probe ~/.local/bin/nvim and ~/bin/nvim for older copies that would shadow
7+
# CANONICAL (/usr/local/bin/nvim). Warns with a fix command for each found.
8+
# Uses direct path probes — not `command -v` — so install-time bash PATH
9+
# differences don't mask the shadow.
10+
_nvim_warn_shadows() {
11+
local canonical="$1"
12+
local _cv _sv _shadow
13+
_cv=$(_cmd_version "$canonical" --version) || _cv=""
14+
[ -z "$_cv" ] && return 0 # canonical unreadable — nothing useful to compare
15+
for _shadow in "$HOME/.local/bin/nvim" "$HOME/bin/nvim"; do
16+
[ -e "$_shadow" ] || continue
17+
_sv=$(_cmd_version "$_shadow" --version) || _sv=""
18+
if _ver_older_than "$_sv" "$_cv"; then
19+
log_warn "neovim: $_shadow ($_sv) will shadow $canonical ($_cv)"
20+
log_warn " Fix: rm $_shadow"
21+
fi
22+
done
23+
}
24+
625
# Link nvim config from dotfiles repo
726
link_nvim_config() {
827
log_step "nvim config"
@@ -79,6 +98,10 @@ install_neovim() {
7998
current=$(nvim --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
8099
if [ "$current" = "$latest" ]; then
81100
log_ok "neovim $latest_tag already installed — skipping"
101+
# Shadow check still needed: `nvim` above may have resolved to a
102+
# user-local copy (e.g. ~/.local/bin/nvim) that shadows an existing
103+
# /usr/local/bin/nvim at the same version.
104+
if $CAN_SUDO; then _nvim_warn_shadows /usr/local/bin/nvim; fi
82105
return
83106
fi
84107
log_info "neovim: upgrading $current$latest (installing to $prefix)"
@@ -120,7 +143,10 @@ install_neovim() {
120143
cp -r "$extracted"/. "$prefix/"
121144
fi
122145

123-
log_ok "neovim installed → $prefix ($(nvim --version 2>/dev/null | head -1))"
146+
# Use the full path so the version shown is always the binary we just
147+
# installed, not whatever `nvim` resolves to in the install-time bash PATH.
148+
log_ok "neovim installed → $prefix ($($prefix/bin/nvim --version 2>/dev/null | head -1))"
149+
if $CAN_SUDO; then _nvim_warn_shadows /usr/local/bin/nvim; fi
124150
}
125151

126152
_neovim_apt() {

modules/zsh.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ _link_zshrc() {
7878
log_ok "~/.zshrc → dotfiles/zsh/.zshrc"
7979
}
8080

81+
# Set by _set_default_shell; read by install.sh's "next steps" section.
82+
_SHELL_IS_ZSH=false
83+
8184
_set_default_shell() {
8285
log_step "Default shell"
8386
local zsh_path
@@ -88,6 +91,7 @@ _set_default_shell() {
8891
fi
8992
if [ "${SHELL:-}" = "$zsh_path" ]; then
9093
log_ok "zsh is already the default shell"
94+
_SHELL_IS_ZSH=true
9195
return
9296
fi
9397
if $CAN_SUDO; then
@@ -98,10 +102,13 @@ _set_default_shell() {
98102
[ -n "${SUDO:-}" ] && sudo -v 2>/dev/null || true
99103
$SUDO usermod -s "$zsh_path" "$(id -un)"
100104
log_ok "Default shell set to zsh (restart your session)"
105+
_SHELL_IS_ZSH=true
101106
elif chsh -s "$zsh_path" 2>/dev/null; then
102107
log_ok "Default shell set to zsh (restart your session)"
108+
_SHELL_IS_ZSH=true
103109
else
104110
log_warn "Could not change shell — run: chsh -s $zsh_path"
111+
_SHELL_IS_ZSH=false
105112
fi
106113
}
107114

update.sh

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111
# apt omz tmux-plugins zsh-plugins fzf rg fd shellcheck
1212
# zoxide delta eza uv ruff neovim cheat pre-commit xcape
1313
#
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+
#
1418
# 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)
1822
# NOSUDO=1 ./update.sh # skip apt upgrade, skip all sudo operations
1923

2024
set -euo pipefail
@@ -179,6 +183,80 @@ _do_update_neovim() {
179183
_verify_dest nvim "$nvim_dest"
180184
}
181185

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+
182260
# ── System packages ────────────────────────────────────────────────────────────
183261
log_info "Checking sudo access…"
184262
detect_sudo
@@ -453,6 +531,9 @@ if _should_run pre-commit; then
453531
fi
454532
fi
455533

534+
# ── PATH shadow check (always runs — read-only) ───────────────────────────────
535+
_check_path_shadows
536+
456537
echo ""
457538
if $CHECK_ONLY; then
458539
log_ok "Check complete"

0 commit comments

Comments
 (0)