fix the silent death after installing zsh

Reported from a fresh Hetzner VPS: the run stopped dead right after apt finished
installing zsh, printing nothing at all — just install.sh's "machine setup did not
finish".

install_oh_my_zsh carried a comment saying it "Returns 0 whatever happens". It did
not. Under `set -e` a failing command inside a function aborts the SHELL at that
line when the function is called plainly; `return 0` underneath is never reached.
The command is also `>/dev/null 2>&1`, so the cause was invisible — which is why
the transcript just ends.

`|| true` is what actually makes it non-fatal. The file already uses that idiom
correctly in four other places, so this was a slip rather than a misunderstanding.

set_login_shell had the identical bug on `chsh`, which the same run would have hit
on the very next question. Fixed differently and deliberately: `|| true` there
would let the caller announce a login shell that was never set, so it returns
chsh's real status and the CALLER guards the call — which is also what keeps set -e
out of it. A refusal now reports, names the manual chsh command, and carries on,
because a machine with zsh installed and bash at login still works.

Does not explain WHY oh-my-zsh failed on that host — the output was discarded. It
will now say "oh-my-zsh did not install" and continue, which is enough to see it.

Verified: bash -n on both files, and a reduced case proving broken() exits 1 while
fixed() survives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 06:23:45 +00:00
co-authored by Claude Opus 5
parent 6c13e0d8f6
commit 4c33ef7206
2 changed files with 33 additions and 14 deletions
+20 -11
View File
@@ -92,27 +92,36 @@ oh_my_zsh_installed() { [[ -d "${USER_HOME}/.oh-my-zsh" ]]; }
install_oh_my_zsh() { install_oh_my_zsh() {
# The installer refuses to run unattended over an existing install, so this is # The installer refuses to run unattended over an existing install, so this is
# only ever called when there is none. # only ever called when there is none.
#
# ── `|| true` is what makes this non-fatal, NOT the `return 0` below ──
#
# It used to be `return 0` alone, with a comment claiming the function returned
# zero whatever happened. It did not. Under `set -e` a failing command inside a
# function aborts the SHELL at that line when the function is called plainly —
# `return 0` is never reached. So a machine where this curl or the installer
# failed died here, silently, because the output is redirected: the run just
# stopped after apt finished installing zsh, with nothing said. Observed on a
# fresh Hetzner VPS, 2026-08-14.
sudo -H -u "$USERNAME" sh -c \ sudo -H -u "$USERNAME" sh -c \
"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended >/dev/null 2>&1 "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended >/dev/null 2>&1 ||
# Returns 0 whatever happens. This is an optional improvement, and a true
# function that ends on a failing command is fatal under `set -e` when it # Belt and braces: `|| true` above already makes the last command succeed, and
# is called as a plain command — which would abort the remaining sections # this states the contract for anyone adding a line beneath it.
# over something the run could simply report. The caller checks the outcome.
return 0 return 0
} }
# `chsh` is what actually changes the login shell. Asked separately from # `chsh` is what actually changes the login shell. Asked separately from
# installing zsh, because having a shell available and being handed it at every # installing zsh, because having a shell available and being handed it at every
# login are different decisions. # login are different decisions.
# Reports whether chsh worked, rather than swallowing it. The same `set -e` trap as
# install_oh_my_zsh applies — a bare `chsh` that fails kills the run at this line —
# but here the answer matters: the caller announces the new login shell, and `|| true`
# would have it announce one that was never set. So the status comes back and the
# CALLER guards the call, which is also what keeps set -e out of it.
set_login_shell() { set_login_shell() {
local shell="$1" local shell="$1"
grep -qxF "$shell" /etc/shells || echo "$shell" >>/etc/shells grep -qxF "$shell" /etc/shells || echo "$shell" >>/etc/shells
chsh -s "$shell" "$USERNAME" chsh -s "$shell" "$USERNAME" >/dev/null 2>&1
# Returns 0 whatever happens. This is an optional improvement, and a
# function that ends on a failing command is fatal under `set -e` when it
# is called as a plain command — which would abort the remaining sections
# over something the run could simply report. The caller checks the outcome.
return 0
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+11 -1
View File
@@ -2217,10 +2217,20 @@ if ! skip; then
echo "" echo ""
echo " ${USERNAME}'s login shell is ${SHELL_NOW}. Changing it to zsh takes" echo " ${USERNAME}'s login shell is ${SHELL_NOW}. Changing it to zsh takes"
echo " effect at the next login, and does not affect this session." echo " effect at the next login, and does not affect this session."
# Guarded, not bare: `chsh` can refuse — a PAM policy, a shell missing from
# /etc/shells, an account whose password field blocks it — and a bare call would
# end the run there under `set -e`. Reported instead, because a machine with the
# right shell installed and the wrong one at login still works.
if confirm "Make zsh the login shell?"; then if confirm "Make zsh the login shell?"; then
set_login_shell "$(command -v zsh)" if set_login_shell "$(command -v zsh)"; then
ok "login shell is now $(user_login_shell)" ok "login shell is now $(user_login_shell)"
SUMMARY+=("Shell: login shell set to zsh") SUMMARY+=("Shell: login shell set to zsh")
else
warn "chsh refused — login shell is still $(user_login_shell)"
echo " Change it later with: chsh -s $(command -v zsh) ${USERNAME}"
ERRORS+=("Shell: chsh refused, login shell left as $(user_login_shell)")
SUMMARY+=("Shell: login shell NOT changed")
fi
else else
warn "left as ${SHELL_NOW}" warn "left as ${SHELL_NOW}"
SUMMARY+=("Shell: login shell left as ${SHELL_NOW}") SUMMARY+=("Shell: login shell left as ${SHELL_NOW}")