timezone Timezone and locale
To set a VPS timezone, run the command below as root and append a zone name such as Asia/Shanghai or Asia/Taipei. You may also pass a locale, for example zh_CN.UTF-8.
$ curl -fsSL xyz.sh/timezone | bash
ffa601da63629bef2631040d50ef20faa391ca1ed8f24f96296723c64e04c178
· Matches the script curl receives. You can also fetch /timezone.sh.sha256
Every script passes bash -n and ShellCheck and was tested with curl | bash as root in the official Docker images of Debian 12/11, Ubuntu 24.04/22.04/20.04/18.04, Rocky 9, AlmaLinux 9 and CentOS 7 (see “Container test results” on each page). Containers lack systemd, kernel modules and real public routes, so those parts still need a real VPS.
What it does
Sets the timezone with timedatectl (or /etc/localtime as a fallback) and can optionally set the system locale (locale-gen / update-locale on Debian, localectl on RHEL-like systems, installing glibc-langpack when needed). Without arguments it prompts on a TTY; without a TTY a timezone is required. Use --list to list timezones. Aliases: xyz.sh/tz, xyz.sh/locale.
Container test results
Results of curl | bash as root without a terminal in each distribution's official Docker image. partial means the container could not verify everything (for example no systemd, no kernel module, or the test protocol blocked by the test environment's proxy) and a real VPS is needed to confirm. (2026-11-11)
| OS | Result |
|---|---|
| debian:12 | pass |
| debian:11 | pass |
| ubuntu:24.04 | pass |
| ubuntu:22.04 | pass |
| ubuntu:20.04 | pass |
| ubuntu:18.04 | pass |
| rockylinux:9 | pass |
| almalinux:9 | pass |
| centos:7 | pass |
FAQ
- Is this script safe?
- The xyz.sh wrapper adds no telemetry and does not perform hidden actions. You can read the full source on the page or with curl -fsSL xyz.sh/<name>.sh before running it. Piping a remote script to bash is still a risk — review the source first. The script only changes the timezone and a locale you explicitly pass; it does not change other system settings.
- Which operating systems are supported?
- Declared support: Debian, Ubuntu, CentOS, Rocky Linux, and AlmaLinux. The script detects the distribution and asks before continuing on an unrecognized OS. Syntax was checked with bash -n and ShellCheck; installer scripts have not been end-to-end verified on every listed distribution.
- Do I need root?
- Yes. Run it as root or via sudo bash. The script exits with a message if it is not root.
- How do I revert it?
- Run it again with the timezone you want, for example curl -fsSL xyz.sh/timezone | bash -s -- UTC. There is no separate uninstall flag.
- How do I list available timezones?
- curl -fsSL xyz.sh/timezone | bash -s -- --list. --list is read-only and does not require root.
Full source
This is exactly what curl receives (common library + script body). Read it before running.
#!/usr/bin/env bash
# Served by xyz.sh/timezone
# Human-readable page: https://xyz.sh/timezone
# This file is the common library followed by the script body.
# No telemetry is added by xyz.sh.
# =============================================================================
# xyz.sh common library
# Shared helpers for every script served by xyz.sh.
#
# Promises:
# - No telemetry, analytics, or background "phone-home" calls
# - No hidden installs or config changes beyond what the script announces
# - Official upstream URLs are printed before any remote code is executed
# - Destructive or system-changing steps ask for confirmation (or YES=1 / -y)
# =============================================================================
if [[ -n "${XYZ_COMMON_LOADED:-}" ]]; then
# When sourced a second time, return. When executed as a file, return fails.
# shellcheck disable=SC2317
return 0 2>/dev/null || true
fi
# shellcheck disable=SC2034
XYZ_COMMON_LOADED=1
set -euo pipefail
XYZ_VERSION="1.3.0"
XYZ_BASE="${XYZ_BASE:-https://xyz.sh}"
XYZ_NAME="${XYZ_NAME:-unknown}"
# --- language ---------------------------------------------------------------
# Override with XYZ_LANG=zh or XYZ_LANG=en.
if [[ -z "${XYZ_LANG:-}" ]]; then
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|ZH*|cmn*|CMN*) XYZ_LANG="zh" ;;
*) XYZ_LANG="en" ;;
esac
fi
xyz_t() {
# xyz_t "English text" "中文文本"
if [[ "${XYZ_LANG}" == "zh" ]]; then
printf '%s' "${2:-$1}"
else
printf '%s' "$1"
fi
}
# --- colors (TTY only) ------------------------------------------------------
# XYZ_COLOR=always|never|auto (default auto: TTY and no NO_COLOR); --no-color.
_xyz_color=0
case "${XYZ_COLOR:-auto}" in
always) _xyz_color=1 ;;
never) _xyz_color=0 ;;
*) [[ -t 1 && -z "${NO_COLOR:-}" ]] && _xyz_color=1 ;;
esac
[[ "${XYZ_NO_COLOR:-0}" == "1" ]] && _xyz_color=0
if [[ "${_xyz_color}" == "1" ]]; then
XYZ_C_RESET=$'\033[0m'
XYZ_C_BOLD=$'\033[1m'
XYZ_C_DIM=$'\033[2m'
XYZ_C_RED=$'\033[31m'
XYZ_C_GREEN=$'\033[32m'
XYZ_C_YELLOW=$'\033[33m'
XYZ_C_CYAN=$'\033[36m'
else
XYZ_C_RESET=""
XYZ_C_BOLD=""
XYZ_C_DIM=""
XYZ_C_RED=""
XYZ_C_GREEN=""
XYZ_C_YELLOW=""
XYZ_C_CYAN=""
fi
xyz_info() { printf '%s[INFO]%s %s\n' "${XYZ_C_GREEN}" "${XYZ_C_RESET}" "$*"; }
xyz_warn() { printf '%s[WARN]%s %s\n' "${XYZ_C_YELLOW}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_error() { printf '%s[ERROR]%s %s\n' "${XYZ_C_RED}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_step() { printf '%s==>%s %s\n' "${XYZ_C_CYAN}" "${XYZ_C_RESET}" "$*"; }
# --- args / init ------------------------------------------------------------
XYZ_ARGS=()
XYZ_YES=0
XYZ_DRY_RUN=0
xyz_parse_global_args() {
local filtered=()
local arg
for arg in "$@"; do
case "${arg}" in
-y|--yes)
XYZ_YES=1
;;
--lang=zh|zh)
XYZ_LANG="zh"
;;
--lang=en|en)
XYZ_LANG="en"
;;
--dry-run)
XYZ_DRY_RUN=1
;;
--no-color)
XYZ_NO_COLOR=1
XYZ_C_RESET=""; XYZ_C_BOLD=""; XYZ_C_DIM=""; XYZ_C_RED=""
XYZ_C_GREEN=""; XYZ_C_YELLOW=""; XYZ_C_CYAN=""
;;
--help|-h)
XYZ_HELP=1
filtered+=("${arg}")
;;
*)
filtered+=("${arg}")
;;
esac
done
XYZ_ARGS=("${filtered[@]+"${filtered[@]}"}")
if [[ "${YES:-}" =~ ^(1|yes|true|YES)$ || "${XYZ_ASSUME_YES:-}" == "1" ]]; then
XYZ_YES=1
fi
# Exported so individual scripts can read flags set here.
export XYZ_ARGS XYZ_YES XYZ_LANG XYZ_HELP="${XYZ_HELP:-}" XYZ_DRY_RUN="${XYZ_DRY_RUN:-0}"
export XYZ_NO_COLOR="${XYZ_NO_COLOR:-0}"
if declare -F xyz_ui_init >/dev/null; then xyz_ui_init; fi
}
xyz_banner() {
local title="${XYZ_TITLE:-$XYZ_NAME}"
printf '\n%s%sxyz.sh/%s%s v%s\n' "${XYZ_C_BOLD}" "${XYZ_C_CYAN}" "${XYZ_NAME}" "${XYZ_C_RESET}" "${XYZ_VERSION}"
printf '%s\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
printf ' %s: %s/%s\n' "$(xyz_t "Page" "页面")" "${XYZ_BASE}" "${XYZ_NAME}"
printf ' %s: %s/%s.sh\n' "$(xyz_t "Raw" "源码")" "${XYZ_BASE}" "${XYZ_NAME}"
if [[ -n "${XYZ_UPSTREAM:-}" ]]; then
printf ' %s: %s\n' "$(xyz_t "Upstream" "上游来源")" "${XYZ_UPSTREAM}"
fi
printf ' %s\n' "$(xyz_t "No telemetry, no hidden actions." "无遥测、无隐藏操作。")"
printf '%s\n\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
if [[ -n "${title}" && "${title}" != "${XYZ_NAME}" ]]; then
xyz_step "${title}"
fi
}
xyz_init() {
xyz_parse_global_args "$@"
xyz_detect_os
xyz_banner
}
# --- root / OS --------------------------------------------------------------
xyz_is_root() {
[[ "${EUID:-$(id -u)}" -eq 0 ]]
}
xyz_require_root() {
if ! xyz_is_root; then
xyz_error "$(xyz_t \
"This script must be run as root. Try: sudo bash" \
"请使用 root 运行。可尝试:sudo bash")"
exit 1
fi
}
xyz_prefer_root() {
if ! xyz_is_root; then
xyz_warn "$(xyz_t \
"Not running as root; some information or tests may be limited." \
"当前不是 root,部分信息或测试可能不完整。")"
fi
}
XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY="" # debian | rhel | unknown
XYZ_OS_PRETTY=""
xyz_detect_os() {
XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY="unknown"
XYZ_OS_PRETTY="unknown"
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
XYZ_OS_ID="${ID:-}"
XYZ_OS_LIKE="${ID_LIKE:-}"
XYZ_OS_VERSION="${VERSION_ID:-}"
XYZ_OS_PRETTY="${PRETTY_NAME:-$XYZ_OS_ID}"
export XYZ_OS_ID XYZ_OS_LIKE XYZ_OS_VERSION XYZ_OS_FAMILY XYZ_OS_PRETTY
fi
case "${XYZ_OS_ID}" in
debian|ubuntu)
XYZ_OS_FAMILY="debian"
;;
centos|rhel|rocky|almalinux|alma|fedora)
XYZ_OS_FAMILY="rhel"
;;
*)
case " ${XYZ_OS_ID} ${XYZ_OS_LIKE} " in
*debian*|*ubuntu*) XYZ_OS_FAMILY="debian" ;;
*rhel*|*centos*|*fedora*|*rocky*|*alma*) XYZ_OS_FAMILY="rhel" ;;
*) : ;;
esac
;;
esac
}
xyz_os_supported() {
case "${XYZ_OS_ID}" in
debian|ubuntu|centos|rocky|almalinux) return 0 ;;
*) : ;;
esac
case " ${XYZ_OS_LIKE} " in
*debian*|*ubuntu*|*rhel*|*centos*|*rocky*|*alma*) return 0 ;;
*) : ;;
esac
return 1
}
xyz_require_supported_os() {
xyz_detect_os
if ! xyz_os_supported; then
xyz_warn "$(xyz_t \
"Unrecognized distribution: ${XYZ_OS_ID:-unknown} (${XYZ_OS_PRETTY}). Officially supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux." \
"未能识别的发行版:${XYZ_OS_ID:-unknown}(${XYZ_OS_PRETTY})。官方支持:Debian、Ubuntu、CentOS、Rocky、AlmaLinux。")"
if ! xyz_confirm "$(xyz_t "Continue anyway?" "仍然继续?")"; then
exit 1
fi
else
xyz_info "$(xyz_t "Detected OS:" "检测到系统:") ${XYZ_OS_PRETTY} (${XYZ_OS_FAMILY})"
fi
}
# --- packages / commands ----------------------------------------------------
xyz_need_cmd() {
command -v "$1" >/dev/null 2>&1
}
xyz_pkg_install() {
xyz_detect_os
if (( $# == 0 )); then
return 0
fi
case "${XYZ_OS_FAMILY}" in
debian)
export DEBIAN_FRONTEND=noninteractive
apt-get update -y || xyz_warn "$(xyz_t "apt-get update reported errors (a repository may be unreachable); trying to install anyway." "apt-get update 报错(可能有软件源无法访问),仍尝试安装。")"
apt-get install -y --no-install-recommends "$@"
;;
rhel)
if xyz_need_cmd dnf; then
dnf install -y "$@"
elif xyz_need_cmd yum; then
yum install -y "$@"
else
xyz_error "$(xyz_t "Neither dnf nor yum is available." "找不到 dnf 或 yum。")"
exit 1
fi
;;
*)
xyz_error "$(xyz_t "Cannot install packages on this OS automatically." "无法在此系统上自动安装软件包。")"
exit 1
;;
esac
}
xyz_ensure_cmds() {
local pkg
local missing=()
for pkg in "$@"; do
if ! xyz_need_cmd "${pkg}"; then
missing+=("${pkg}")
fi
done
if ((${#missing[@]} > 0)); then
xyz_info "$(xyz_t "Installing missing commands:" "正在安装缺失命令:") ${missing[*]}"
xyz_pkg_install "${missing[@]}"
fi
}
# --- confirmation (safe with curl | bash) -----------------------------------
# A readable /dev/tty node is not enough: in containers or curl|bash without a
# controlling terminal, open() fails with ENXIO ("No such device or address").
# [[ -r /dev/tty ]] is true in that case and must not be used as the check.
xyz_open_tty() {
# Opens /dev/tty into XYZ_TTY_FD if it is a real terminal. Caller must close.
local fd
XYZ_TTY_FD=""
# Brace group so a failed open cannot leak "No such device or address".
if { exec {fd}<>/dev/tty; } 2>/dev/null && [[ -t "${fd}" ]]; then
XYZ_TTY_FD="${fd}"
return 0
fi
if [[ -n "${fd:-}" ]]; then
exec {fd}>&- 2>/dev/null || true
fi
return 1
}
xyz_close_tty() {
if [[ -n "${XYZ_TTY_FD:-}" ]]; then
exec {XYZ_TTY_FD}>&- 2>/dev/null || true
XYZ_TTY_FD=""
fi
}
xyz_confirm() {
local prompt="$1"
if [[ "${XYZ_YES}" == "1" ]]; then
xyz_info "$(xyz_t "Auto-confirmed (-y / YES=1):" "已自动确认(-y / YES=1):") ${prompt}"
return 0
fi
local reply=""
if xyz_open_tty; then
printf '%s [y/N] ' "${prompt}" >&"${XYZ_TTY_FD}"
# shellcheck disable=SC2162
read -r reply <&"${XYZ_TTY_FD}" || true
xyz_close_tty
else
xyz_warn "$(xyz_t \
"No TTY available for confirmation. Re-run with -y or YES=1 if you accept." \
"没有可用的终端来确认。若你已了解并同意,请加 -y 或 YES=1 重试。")"
return 1
fi
case "${reply}" in
y|Y|yes|YES|Yes) return 0 ;;
*) return 1 ;;
esac
}
# --- download helpers -------------------------------------------------------
# Create the temp dir in the main shell. xyz_tmpdir is usually called inside
# $(...), a subshell, so creating it (and its EXIT trap) there would delete the
# directory as soon as the subshell returned.
xyz_init_tmpdir() {
if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
XYZ_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/xyzsh.XXXXXX")"
# shellcheck disable=SC2064
trap "rm -rf '${XYZ_TMPDIR}'" EXIT
fi
}
xyz_tmpdir() {
if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
xyz_error "internal: temp dir not initialised"
exit 1
fi
printf '%s' "${XYZ_TMPDIR}"
}
xyz_fetch() {
# xyz_fetch URL DEST
local url="$1"
local dest="$2"
xyz_info "$(xyz_t "Downloading" "正在下载"): ${url}"
xyz_info "$(xyz_t "Saving to" "保存到"): ${dest}"
if xyz_need_cmd curl; then
curl -fsSL --connect-timeout 20 --max-time 180 --retry 2 --retry-delay 1 \
-o "${dest}" "${url}"
elif xyz_need_cmd wget; then
wget -q --timeout=60 -O "${dest}" "${url}"
else
xyz_error "$(xyz_t "Need curl or wget to download files." "需要 curl 或 wget 才能下载文件。")"
exit 1
fi
if [[ ! -s "${dest}" ]]; then
xyz_error "$(xyz_t "Download failed or file is empty:" "下载失败或文件为空:") ${url}"
exit 1
fi
if xyz_need_cmd sha256sum; then
xyz_info "SHA256: $(sha256sum "${dest}" | awk '{print $1}')"
elif xyz_need_cmd shasum; then
xyz_info "SHA256: $(shasum -a 256 "${dest}" | awk '{print $1}')"
fi
}
xyz_run_upstream() {
# xyz_run_upstream URL [args...]
# Downloads an official installer, shows the URL and a short preview, then
# runs it only after confirmation (unless -y / YES=1).
local url="$1"
shift
local dest
xyz_init_tmpdir
dest="$(xyz_tmpdir)/upstream.sh"
xyz_fetch "${url}" "${dest}"
chmod +x "${dest}"
xyz_info "$(xyz_t "Preview (first 20 lines):" "预览(前 20 行):")"
sed -n '1,20p' "${dest}" || true
printf '%s\n' "${XYZ_C_DIM}...${XYZ_C_RESET}"
# Default --dry-run: verify the download and preview, do not execute.
# Scripts that forward --dry-run to upstream (e.g. docker) set
# XYZ_FORWARD_DRY_RUN=1 so the official script still runs with that flag.
if [[ "${XYZ_DRY_RUN:-0}" == "1" && "${XYZ_FORWARD_DRY_RUN:-0}" != "1" ]]; then
xyz_info "$(xyz_t \
"Dry-run: official script downloaded and previewed; not executing it." \
"演练模式:已下载并预览官方脚本,不会执行。")"
return 0
fi
if ! xyz_confirm "$(xyz_t \
"Run this official upstream script?" \
"运行这份官方上游脚本?")"; then
xyz_error "$(xyz_t "Aborted by user." "已取消。")"
exit 1
fi
xyz_step "$(xyz_t "Executing upstream script" "正在执行上游脚本")"
# Never let the upstream script inherit our stdin: under `curl | bash` that
# is the rest of this very script. Give it the terminal if there is one,
# otherwise /dev/null. Interactive installers set XYZ_UPSTREAM_NEEDS_TTY=1.
if xyz_open_tty; then
local tty_fd="${XYZ_TTY_FD}"
XYZ_TTY_FD=""
bash "${dest}" "$@" <&"${tty_fd}"
local rc=$?
exec {tty_fd}>&- 2>/dev/null || true
return "${rc}"
fi
if [[ "${XYZ_UPSTREAM_NEEDS_TTY:-0}" == "1" ]]; then
xyz_error "$(xyz_t \
"This official installer is interactive and needs a terminal. Log in with SSH and run: bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})" \
"这个官方安装脚本需要交互,必须在终端里运行。请用 SSH 登录后执行:bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})")"
exit 1
fi
bash "${dest}" "$@" </dev/null
}
# --- small shared helpers ----------------------------------------------------
xyz_strip_ansi() {
sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g' -e 's/\r//g'
}
# Effective SSH port: sshd -T (what sshd really uses) > config files > env > 22.
xyz_effective_ssh_port() {
local port=""
if xyz_need_cmd sshd; then
port="$(sshd -T 2>/dev/null | awk '$1=="port" {print $2; exit}' || true)"
fi
if [[ -z "${port}" && -d /etc/ssh/sshd_config.d ]]; then
port="$(cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null | awk 'tolower($1)=="port" {print $2; exit}' || true)"
fi
if [[ -z "${port}" && -f /etc/ssh/sshd_config ]]; then
port="$(awk 'tolower($1)=="port" {print $2; exit}' /etc/ssh/sshd_config || true)"
fi
if [[ -z "${port}" && -n "${SSH_CONNECTION:-}" ]]; then
port="$(printf '%s' "${SSH_CONNECTION}" | awk '{print $4}')"
fi
printf '%s' "${port:-22}"
}
xyz_has_systemd() {
[[ -d /run/systemd/system ]] && xyz_need_cmd systemctl
}
# Back up a file once per run as FILE.xyz-bak-YYYYmmddHHMMSS and print the path.
xyz_backup_file() {
local f="$1"
local bak
bak="${f}.xyz-bak-$(date +%Y%m%d%H%M%S)"
if [[ -e "${f}" ]]; then
cp -a "${f}" "${bak}"
printf '%s' "${bak}"
fi
}
# --- misc -------------------------------------------------------------------
xyz_usage_exit() {
if [[ "${1:-}" != "" ]]; then
printf '%s\n' "$1"
fi
exit 0
}
XYZ_NAME="timezone"
# xyz.sh/timezone — set timezone and (optionally) system locale.
#
# Usage:
# curl -fsSL xyz.sh/timezone | bash
# curl -fsSL xyz.sh/timezone | bash -s -- Asia/Shanghai
# curl -fsSL xyz.sh/timezone | bash -s -- Asia/Taipei zh_TW.UTF-8
# curl -fsSL xyz.sh/timezone | bash -s -- --timezone UTC --locale en_US.UTF-8
# curl -fsSL xyz.sh/timezone | bash -s -- --list
#
# Alias: tz, locale (same script).
# Requires root to change settings; --list / current view works without root.
# Uses timedatectl / systemd when available, with /etc/localtime fallback.
# No telemetry.
set -euo pipefail
if [[ -z "${XYZ_COMMON_LOADED:-}" ]]; then
_xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "${_xyz_dir}/lib/common.sh" ]]; then
# shellcheck source=lib/common.sh
source "${_xyz_dir}/lib/common.sh"
else
echo "xyz.sh: missing lib/common.sh" >&2
exit 1
fi
fi
XYZ_NAME="${XYZ_NAME:-timezone}"
XYZ_TITLE="$(xyz_t "Timezone and locale" "时区与 locale")"
xyz_init "$@"
DO_LIST=0
TZ_VALUE=""
LOCALE_VALUE=""
if [[ "${XYZ_HELP:-}" == "1" ]]; then
cat <<EOF
$(xyz_t "Usage:" "用法:")
curl -fsSL xyz.sh/timezone | bash
curl -fsSL xyz.sh/timezone | bash -s -- Asia/Shanghai
curl -fsSL xyz.sh/timezone | bash -s -- America/New_York en_US.UTF-8
curl -fsSL xyz.sh/timezone | bash -s -- --timezone UTC --locale C.UTF-8
curl -fsSL xyz.sh/timezone | bash -s -- --list
curl -fsSL xyz.sh/timezone | bash -s -- --dry-run Asia/Shanghai
$(xyz_t "Common zones:" "常用时区:") Asia/Shanghai Asia/Taipei Asia/Tokyo Asia/Singapore
Europe/London Europe/Berlin America/New_York America/Los_Angeles UTC
EOF
exit 0
fi
i=0
POSITIONAL=()
while [[ $i -lt ${#XYZ_ARGS[@]} ]]; do
arg="${XYZ_ARGS[$i]}"
case "${arg}" in
--list) DO_LIST=1 ;;
--timezone)
i=$((i + 1))
TZ_VALUE="${XYZ_ARGS[$i]:-}"
;;
--locale)
i=$((i + 1))
LOCALE_VALUE="${XYZ_ARGS[$i]:-}"
;;
--timezone=*) TZ_VALUE="${arg#--timezone=}" ;;
--locale=*) LOCALE_VALUE="${arg#--locale=}" ;;
--*)
xyz_error "$(xyz_t "Unknown argument:" "未知参数:") ${arg}"
exit 1
;;
*)
POSITIONAL+=("${arg}")
;;
esac
i=$((i + 1))
done
if [[ -z "${TZ_VALUE}" && ${#POSITIONAL[@]} -ge 1 ]]; then
TZ_VALUE="${POSITIONAL[0]}"
fi
if [[ -z "${LOCALE_VALUE}" && ${#POSITIONAL[@]} -ge 2 ]]; then
LOCALE_VALUE="${POSITIONAL[1]}"
fi
# Official Ubuntu container images (and some minimal VPS images) ship without
# tzdata, so /usr/share/zoneinfo is missing. A real Ubuntu VPS usually has it.
xyz_ensure_tzdata() {
if [[ -f "/usr/share/zoneinfo/UTC" || -d /usr/share/zoneinfo/Asia || -d /usr/share/zoneinfo/Etc ]]; then
return 0
fi
xyz_detect_os
case "${XYZ_OS_FAMILY}" in
debian)
xyz_info "$(xyz_t "tzdata is not installed; installing it." "未安装 tzdata,正在安装。")"
xyz_pkg_install tzdata
;;
rhel)
xyz_info "$(xyz_t "tzdata is not installed; installing it." "未安装 tzdata,正在安装。")"
xyz_pkg_install tzdata
;;
*)
return 1
;;
esac
}
xyz_current() {
xyz_step "$(xyz_t "Current settings" "当前设置")"
if xyz_need_cmd timedatectl; then
timedatectl || true
else
printf ' date : %s\n' "$(date)"
printf ' timezone : %s\n' "$(date +%Z) ($(date +%z))"
if [[ -L /etc/localtime ]]; then
printf ' localtime: %s\n' "$(readlink /etc/localtime)"
fi
fi
echo
printf ' LANG : %s\n' "${LANG:-unset}"
printf ' LC_ALL : %s\n' "${LC_ALL:-unset}"
if [[ -f /etc/default/locale ]]; then
printf ' /etc/default/locale:\n'
sed 's/^/ /' /etc/default/locale
fi
if [[ -f /etc/locale.conf ]]; then
printf ' /etc/locale.conf:\n'
sed 's/^/ /' /etc/locale.conf
fi
if xyz_need_cmd localectl; then
localectl status 2>/dev/null || true
fi
}
if [[ "${DO_LIST}" == "1" ]]; then
if ! { [[ -d /usr/share/zoneinfo ]] || xyz_need_cmd timedatectl; }; then
if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
xyz_info "$(xyz_t "Dry-run: tzdata is missing; would install it to list timezones." "演练模式:缺少 tzdata,将先安装再列出时区。")"
exit 0
fi
xyz_require_root
xyz_ensure_tzdata || true
fi
if xyz_need_cmd timedatectl && timedatectl list-timezones 2>/dev/null; then
exit 0
elif [[ -d /usr/share/zoneinfo ]]; then
find /usr/share/zoneinfo -type f -o -type l | sed 's|^/usr/share/zoneinfo/||' | grep -v '^[.]' | sort
else
xyz_error "$(xyz_t "Cannot list timezones on this system (tzdata is not installed)." "无法列出本机时区(未安装 tzdata)。")"
exit 1
fi
exit 0
fi
xyz_current
if [[ -z "${TZ_VALUE}" && -z "${LOCALE_VALUE}" ]]; then
if xyz_open_tty; then
printf '%s ' "$(xyz_t "Timezone (empty to skip, e.g. Asia/Shanghai):" "时区(留空跳过,例如 Asia/Shanghai):")" >&"${XYZ_TTY_FD}"
read -r TZ_VALUE <&"${XYZ_TTY_FD}" || true
printf '%s ' "$(xyz_t "Locale (empty to skip, e.g. zh_CN.UTF-8):" "Locale(留空跳过,例如 zh_CN.UTF-8):")" >&"${XYZ_TTY_FD}"
read -r LOCALE_VALUE <&"${XYZ_TTY_FD}" || true
xyz_close_tty
else
xyz_error "$(xyz_t \
"No timezone or locale given and no TTY. Pass one, e.g.: bash -s -- Asia/Shanghai" \
"未指定时区或 locale,且没有可用终端。请传入参数,例如:bash -s -- Asia/Shanghai")"
exit 1
fi
fi
if [[ -z "${TZ_VALUE}" && -z "${LOCALE_VALUE}" ]]; then
xyz_info "$(xyz_t "Nothing to change." "没有需要修改的项。")"
exit 0
fi
xyz_require_root
xyz_require_supported_os
xyz_zone_exists() {
local zone="$1"
if [[ -e "/usr/share/zoneinfo/${zone}" || -L "/usr/share/zoneinfo/${zone}" ]]; then
return 0
fi
if xyz_need_cmd timedatectl && timedatectl list-timezones 2>/dev/null | grep -Fxq "${zone}"; then
return 0
fi
return 1
}
if [[ -n "${TZ_VALUE}" ]]; then
if ! xyz_zone_exists "${TZ_VALUE}"; then
if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
xyz_info "$(xyz_t \
"Dry-run: timezone ${TZ_VALUE} is not in /usr/share/zoneinfo yet (tzdata may be missing). Would install tzdata if needed and set this zone." \
"演练模式:/usr/share/zoneinfo 中还没有 ${TZ_VALUE}(可能未安装 tzdata)。将在需要时安装 tzdata 并设置该时区。")"
# Continue to the planned-changes + dry-run exit below; do not fail here.
else
xyz_ensure_tzdata || true
if ! xyz_zone_exists "${TZ_VALUE}"; then
xyz_error "$(xyz_t "Unknown timezone:" "未知时区:") ${TZ_VALUE}"
xyz_info "$(xyz_t "List zones with:" "可用以下命令列出时区:") curl -fsSL xyz.sh/timezone | bash -s -- --list"
exit 1
fi
fi
fi
fi
xyz_info "$(xyz_t "Planned changes:" "计划变更:")"
[[ -n "${TZ_VALUE}" ]] && printf ' timezone : %s\n' "${TZ_VALUE}"
[[ -n "${LOCALE_VALUE}" ]] && printf ' locale : %s\n' "${LOCALE_VALUE}"
if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
xyz_info "$(xyz_t "Dry-run: timezone/locale will not be changed." "演练模式:不会修改时区或 locale。")"
exit 0
fi
if ! xyz_confirm "$(xyz_t "Apply these settings?" "应用这些设置?")"; then
exit 1
fi
if [[ -n "${TZ_VALUE}" ]]; then
xyz_step "$(xyz_t "Setting timezone" "正在设置时区")"
if xyz_need_cmd timedatectl && timedatectl set-timezone "${TZ_VALUE}"; then
:
else
if xyz_need_cmd timedatectl; then
xyz_warn "$(xyz_t \
"timedatectl failed (often because systemd is not PID 1). Falling back to /etc/localtime." \
"timedatectl 失败(通常是因为 systemd 不是 PID 1)。回退到 /etc/localtime。")"
fi
ln -sf "/usr/share/zoneinfo/${TZ_VALUE}" /etc/localtime
printf '%s\n' "${TZ_VALUE}" > /etc/timezone 2>/dev/null || true
fi
fi
if [[ -n "${LOCALE_VALUE}" ]]; then
xyz_step "$(xyz_t "Setting locale" "正在设置 locale")"
case "${XYZ_OS_FAMILY}" in
debian)
if ! xyz_need_cmd locale-gen || ! xyz_need_cmd update-locale; then
xyz_info "$(xyz_t "The locales package is not installed; installing it." "未安装 locales,正在安装。")"
xyz_pkg_install locales
fi
if xyz_need_cmd locale-gen; then
# Ensure the locale is enabled in /etc/locale.gen when that file exists.
if [[ -f /etc/locale.gen ]]; then
if grep -q "^# *${LOCALE_VALUE}" /etc/locale.gen; then
sed -i "s/^# *${LOCALE_VALUE}/${LOCALE_VALUE}/" /etc/locale.gen
elif ! grep -q "^${LOCALE_VALUE}" /etc/locale.gen; then
printf '%s UTF-8\n' "${LOCALE_VALUE}" >> /etc/locale.gen
fi
fi
locale-gen "${LOCALE_VALUE}" || locale-gen
fi
if xyz_need_cmd update-locale; then
update-locale LANG="${LOCALE_VALUE}"
else
printf 'LANG=%s\n' "${LOCALE_VALUE}" > /etc/default/locale
fi
if xyz_need_cmd localectl; then
localectl set-locale LANG="${LOCALE_VALUE}" || true
fi
;;
rhel)
# glibc-langpack may be needed on minimal images.
langpack="$(printf '%s' "${LOCALE_VALUE}" | cut -d. -f1 | cut -d_ -f1)"
if xyz_need_cmd dnf; then
dnf install -y "glibc-langpack-${langpack}" || true
fi
if xyz_need_cmd localectl && localectl set-locale LANG="${LOCALE_VALUE}"; then
:
else
if xyz_need_cmd localectl; then
xyz_warn "$(xyz_t \
"localectl failed (often because systemd is not PID 1). Writing /etc/locale.conf instead." \
"localectl 失败(通常是因为 systemd 不是 PID 1)。改为写入 /etc/locale.conf。")"
fi
printf 'LANG=%s\n' "${LOCALE_VALUE}" > /etc/locale.conf
fi
;;
*)
if xyz_need_cmd localectl; then
localectl set-locale LANG="${LOCALE_VALUE}"
else
xyz_warn "$(xyz_t "Could not set locale automatically on this OS." "无法在此系统上自动设置 locale。")"
fi
;;
esac
fi
xyz_info "$(xyz_t "Done." "完成。")"
xyz_current
Recommended VPS
These slots are placeholders. No vendor or affiliate link has been added, and nothing here is a purchase button. Real plans and compliant referral links will be filled in later.
Vendor TBD
Region, specs, and price will be filled in after onboarding. This is not a checkout link.
Vendor TBD
Region, specs, and price will be filled in after onboarding. This is not a checkout link.