port the timezone section

The original took whatever was typed and handed it straight to timedatectl. An
unknown zone — a typo, a guess at the spelling — fails there, and under `set -e`
that takes the whole run down four steps in. Names are now checked against
/usr/share/zoneinfo before use, and a bad one just re-asks.

It also never showed what the machine was already set to, and defaulted to option
1 (UTC) on Enter, so pressing return on a correctly-configured box silently moved
it. Now the current zone is printed, Enter keeps it, and a zone equal to the
current one reports nothing to do rather than setting it again.

timezone_current reads three sources — timedatectl, /etc/timezone, then the
/etc/localtime symlink — because they differ in availability rather than in
answer: timedatectl needs systemd, /etc/timezone is Debian's, and the symlink is
the one that is always there. timezone_set writes through timedatectl where
there is a systemd to talk to and the files directly otherwise, which is what it
would have written anyway; that is also the WSL path, where timedatectl exists
but does nothing.

Europe/Berlin added to the shortlist; TIMEZONE in the environment answers the
prompt ahead of time and is validated the same way, failing early with the bad
value named.

Verified detection (UTC here), validation of four names, and the env-var
rejection path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 17:43:20 +00:00
co-authored by Claude Opus 5
parent 163f8d5899
commit 6480788979
2 changed files with 113 additions and 1 deletions
+46
View File
@@ -88,3 +88,49 @@ locale_set() {
;; ;;
esac esac
} }
# -----------------------------------------------------------------------------
# Timezone
# -----------------------------------------------------------------------------
# The shortlist offered at the prompt. Any zone name can be typed instead, so
# this is a convenience rather than a limit.
TZ_OPTIONS=(UTC Europe/Lisbon Europe/London Europe/Berlin Europe/Stockholm US/Eastern US/Pacific Asia/Tokyo)
# What the machine is set to now.
#
# Three sources because they disagree about availability rather than about the
# answer: timedatectl is absent without systemd (containers, WSL), /etc/timezone
# is Debian-specific, and the /etc/localtime symlink is the one thing that is
# always true when any of them are.
timezone_current() {
if command -v timedatectl &>/dev/null && timedatectl show -p Timezone --value 2>/dev/null | grep -q .; then
timedatectl show -p Timezone --value 2>/dev/null
elif [[ -r /etc/timezone ]]; then
tr -d '[:space:]' </etc/timezone
elif [[ -L /etc/localtime ]]; then
readlink -f /etc/localtime | sed 's|.*/zoneinfo/||'
fi
}
# Checked against the zoneinfo database before it is used. `timedatectl
# set-timezone` on a name that does not exist fails, and under `set -e` that
# takes the whole run down over a typo.
timezone_is_valid() { [[ -f "/usr/share/zoneinfo/$1" ]]; }
timezone_set() {
local tz="$1"
case "$PM" in
brew) systemsetup -settimezone "$tz" >/dev/null ;;
*)
# timedatectl where there is a systemd to talk to; the files directly
# otherwise, which is the same thing it would have written.
if command -v timedatectl &>/dev/null && [[ "$IS_WSL" != true ]]; then
timedatectl set-timezone "$tz"
else
ln -sf "/usr/share/zoneinfo/${tz}" /etc/localtime
echo "$tz" >/etc/timezone
fi
;;
esac
}
+67 -1
View File
@@ -187,13 +187,79 @@ if ! skip; then
step_ok step_ok
fi fi
# =============================================================================
# 6. Timezone
# =============================================================================
#
# TIMEZONE in the environment answers the prompt ahead of time.
step "Timezone"
if ! skip; then
CURRENT_TZ="$(timezone_current)"
echo ""
info "Timezone — what logs, timers and every printed date are relative to"
echo " current: ${CURRENT_TZ:-unknown}"
if [[ -z "${TIMEZONE:-}" ]]; then
echo ""
for i in "${!TZ_OPTIONS[@]}"; do
printf ' [%d] %s\n' "$((i + 1))" "${TZ_OPTIONS[$i]}"
done
echo ""
while [[ -z "${TIMEZONE:-}" ]]; do
if ! read -rp " Pick a number, or type a zone name — Enter keeps ${CURRENT_TZ:-the current one}: " TZ_CHOICE; then
echo ""
fail "No answer. Set TIMEZONE=<zone> to answer this ahead of time."
fi
if [[ -z "$TZ_CHOICE" ]]; then
TIMEZONE="$CURRENT_TZ"
elif [[ "$TZ_CHOICE" =~ ^[0-9]+$ ]]; then
if ((TZ_CHOICE >= 1 && TZ_CHOICE <= ${#TZ_OPTIONS[@]})); then
TIMEZONE="${TZ_OPTIONS[$((TZ_CHOICE - 1))]}"
else
warn "There is no option ${TZ_CHOICE}."
fi
else
# Validated here rather than left to timedatectl, which fails on an
# unknown name and would take the whole run down over a typo.
if timezone_is_valid "$TZ_CHOICE"; then
TIMEZONE="$TZ_CHOICE"
else
warn "Not a zone this machine knows: '${TZ_CHOICE}' — try e.g. Europe/Berlin"
fi
fi
done
elif ! timezone_is_valid "$TIMEZONE"; then
fail "TIMEZONE='${TIMEZONE}' is not a zone this machine knows."
fi
if [[ "$TIMEZONE" == "$CURRENT_TZ" ]]; then
echo " keeping ${CURRENT_TZ}, nothing to do"
SUMMARY+=("Timezone: already ${CURRENT_TZ}")
else
echo " to set: ${TIMEZONE}"
if confirm "Proceed?"; then
timezone_set "$TIMEZONE"
ok "Timezone set to ${TIMEZONE}"
SUMMARY+=("Timezone: ${TIMEZONE}")
else
warn "skipped by request"
SUMMARY+=("Timezone: SKIPPED by request — left at ${CURRENT_TZ:-unknown}")
fi
fi
step_ok
fi
# ============================================================================= # =============================================================================
# NOT PORTED YET # NOT PORTED YET
# ============================================================================= # =============================================================================
# #
# Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order: # Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order:
# #
# timezone · swap · auto-suspend · boot-hang fix · user creation · # swap · auto-suspend · boot-hang fix · user creation ·
# ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades · # ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades ·
# git config · docker · zsh + prompt · tailscale · neovim · js runtimes · # git config · docker · zsh + prompt · tailscale · neovim · js runtimes ·
# dev tools · ufw · zshrc · disk ballast # dev tools · ufw · zshrc · disk ballast