VPS one-click scripts
All scripts / speedtest

speedtest Speedtest

To speed-test a VPS with the official Ookla CLI, run the command below as root. It installs speedtest into /usr/local/bin from Ookla's official tarball (no package repository is added) and starts a test only after you confirm the license notice. The test contacts Ookla servers — that is the CLI's own behaviour.

Command
$ curl -fsSL xyz.sh/speedtest | bash
Last updated:
Declared OS support:Debian, Ubuntu, CentOS, Rocky, AlmaLinux
Root required
Upstream:https://www.speedtest.net/apps/cli
SHA256:99a187b01dc65e8d6e28a0ea6f2fb3b0e1ed4064c8950d61c351cf19f7038310 · Matches the script curl receives. You can also fetch /speedtest.sh.sha256
Read the tutorial:VPS Speedtest Tutorial (Official Ookla CLI)

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

Downloads Ookla's official Linux tarball from install.speedtest.net (currently 1.2.0; architecture is detected: x86_64, aarch64, armhf, armel, or i386), installs the official `speedtest` binary into /usr/local/bin, and does not add any package repository. By default it tests the server Ookla picks as nearest. With --overseas it also tests Hong Kong, Tokyo, Singapore and the US West: the official CLI is tried first with a regional server search, then speedtest-go (official release, SHA256 pinned, run from a temp dir) if that fails. This wrapper will not silently pass --accept-license or --accept-gdpr until you confirm (or use -y after the notice is printed). xyz.sh adds no extra telemetry. Use --install-only to install without running a test, or --uninstall to remove the binary. Mainland China nodes are not tested.

Supported OS

  • Debian
  • Ubuntu
  • CentOS
  • Rocky
  • AlmaLinux

Root required

Flags and examples

$ curl -fsSL xyz.sh/speedtest | bash
$ curl -fsSL xyz.sh/speedtest | bash -s -- -y
$ curl -fsSL xyz.sh/speedtest | bash -s -- --overseas -y
$ curl -fsSL xyz.sh/speedtest | bash -s -- --install-only
$ curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall

Upstream: https://www.speedtest.net/apps/cli

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)

OSResult
debian:12pass
debian:11pass
ubuntu:24.04pass
ubuntu:22.04pass
ubuntu:20.04pass
ubuntu:18.04pass
rockylinux:9pass
almalinux:9pass
centos:7pass

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 test is performed by Ookla's official CLI, which contacts Ookla servers and has its own terms and privacy policy: https://www.speedtest.net/terms and https://www.speedtest.net/about/privacy. xyz.sh will not silently add the accept flags before you confirm, and it adds no extra telemetry.
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 uninstall it?
Run curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall, or delete /usr/local/bin/speedtest. License acceptance may live in ~/.config/ookla.
Can I install without running a test?
Yes: curl -fsSL xyz.sh/speedtest | bash -s -- --install-only.

Full source

This is exactly what curl receives (common library + script body). Read it before running.

xyz.sh/speedtest.sh
#!/usr/bin/env bash
# Served by xyz.sh/speedtest
# Human-readable page: https://xyz.sh/speedtest
# 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.sh Ookla helper library (shared by speedtest, speedtest-cn, all).
# Official product  : https://www.speedtest.net/apps/cli
# Official tarballs : https://install.speedtest.net/app/cli/ookla-speedtest-<ver>-linux-<arch>.tgz
# Served between lib/common.sh and the script body. No telemetry.

# shellcheck disable=SC2034
XYZ_OOKLA_LOADED=1
# Current tarball version published on https://www.speedtest.net/apps/cli
# (the binary itself reports 1.2.0.84). Override with OOKLA_CLI_VERSION.
OOKLA_CLI_VERSION="${OOKLA_CLI_VERSION:-1.2.0}"
OOKLA_CLI_BASE="https://install.speedtest.net/app/cli"

# Known SHA256 of the official 1.2.0 Linux tarballs (fetched from
# install.speedtest.net, matching the links on speedtest.net/apps/cli).
xyz_ookla_tarball_sha256() {
  case "$1" in
    x86_64)  printf '%s' "5690596c54ff9bed63fa3732f818a05dbc2db19ad36ed68f21ca5f64d5cfeeb7" ;;
    aarch64) printf '%s' "3953d231da3783e2bf8904b6dd72767c5c6e533e163d3742fd0437affa431bd3" ;;
    armhf)   printf '%s' "e45fcdebbd8a185553535533dd032d6b10bc8c64eee4139b1147b9c09835d08d" ;;
    armel)   printf '%s' "629a455a2879224bd0dbd4b36d8c721dda540717937e4660b4d2c966029466bf" ;;
    i386)    printf '%s' "9ff7e18dbae7ee0e03c66108445a2fb6ceea6c86f66482e1392f55881b772fe8" ;;
    *)       return 1 ;;
  esac
}

xyz_ookla_arch() {
  local machine
  machine="$(uname -m)"
  case "${machine}" in
    x86_64|amd64) printf 'x86_64' ;;
    aarch64|arm64) printf 'aarch64' ;;
    armv7l|armv7hl|armhf) printf 'armhf' ;;
    armv6l|armv6hl|armv5tel|armel) printf 'armel' ;;
    arm)
      # Official "arm32" / armel build.
      printf 'armel' ;;
    i386|i486|i586|i686|x86) printf 'i386' ;;
    *)
      xyz_error "$(xyz_t \
        "Unsupported architecture: ${machine}. Official Linux tarballs exist for x86_64, aarch64, armhf, armel, and i386." \
        "不支持的架构:${machine}。官方 Linux 包提供 x86_64、aarch64、armhf、armel 和 i386。")"
      exit 1
      ;;
  esac
}

xyz_ookla_url() {
  local arch="$1"
  printf '%s/ookla-speedtest-%s-linux-%s.tgz' \
    "${OOKLA_CLI_BASE}" "${OOKLA_CLI_VERSION}" "${arch}"
}

xyz_ookla_version_line() {
  local bin="$1"
  local line=""
  line="$("${bin}" --version 2>&1 || true)"
  printf '%s\n' "${line%%$'\n'*}"
}

xyz_have_official_speedtest() {
  local bin
  bin="$(command -v speedtest 2>/dev/null || true)"
  if [[ -z "${bin}" ]]; then
    return 1
  fi
  # Official Ookla binary reports "Speedtest by Ookla" and accepts
  # --accept-license. Distro Python packages often ship as speedtest-cli.
  if "${bin}" --version 2>/dev/null | grep -q 'Speedtest by Ookla'; then
    return 0
  fi
  return 1
}

# xyz_ookla_fetch_binary DIR
# Downloads the official tarball into DIR, verifies the pinned SHA256 and
# extracts it. Sets XYZ_OOKLA_EXTRACTED to the unpacked binary path.
xyz_ookla_fetch_binary() {
  local dir="$1"
  local arch url tarball expected actual
  arch="$(xyz_ookla_arch)"
  url="$(xyz_ookla_url "${arch}")"
  xyz_info "$(xyz_t "Official tarball:" "官方压缩包:") ${url}"
  tarball="${dir}/ookla-speedtest-${OOKLA_CLI_VERSION}-linux-${arch}.tgz"
  xyz_fetch "${url}" "${tarball}"
  expected=""
  if [[ "${OOKLA_CLI_VERSION}" == "1.2.0" ]]; then
    expected="$(xyz_ookla_tarball_sha256 "${arch}")"
  fi
  if [[ -n "${expected}" ]] && xyz_need_cmd sha256sum; then
    actual="$(sha256sum "${tarball}" | awk '{print $1}')"
    if [[ "${actual}" != "${expected}" ]]; then
      xyz_error "$(xyz_t "SHA256 mismatch for the official tarball." "官方压缩包的 SHA256 不匹配。")"
      xyz_error "  expected: ${expected}"
      xyz_error "  actual:   ${actual}"
      exit 1
    fi
    xyz_info "$(xyz_t "SHA256 verified." "SHA256 已校验。")"
  fi
  mkdir -p "${dir}/extract"
  tar -xzf "${tarball}" -C "${dir}/extract"
  chmod +x "${dir}/extract/speedtest" 2>/dev/null || true
  if [[ ! -x "${dir}/extract/speedtest" ]]; then
    xyz_error "$(xyz_t "The tarball did not contain an executable 'speedtest' binary." "压缩包中没有可执行的 speedtest 文件。")"
    exit 1
  fi
  XYZ_OOKLA_EXTRACTED="${dir}/extract/speedtest"
}

# xyz_ookla_ensure: make an official binary available as XYZ_OOKLA_BIN.
# Uses an installed official CLI if present. Otherwise downloads the official
# tarball; as root (after confirmation) installs it to /usr/local/bin,
# otherwise runs it from the temp dir for this run only.
xyz_ookla_ensure() {
  XYZ_OOKLA_BIN=""
  if xyz_have_official_speedtest; then
    XYZ_OOKLA_BIN="$(command -v speedtest)"
    xyz_info "$(xyz_t "Official Speedtest CLI found:" "已找到官方 Speedtest CLI:") $(xyz_ookla_version_line "${XYZ_OOKLA_BIN}")"
    return 0
  fi
  if ! xyz_need_cmd tar; then
    xyz_require_root
    xyz_ensure_cmds tar
  fi
  xyz_init_tmpdir
  xyz_ookla_fetch_binary "$(xyz_tmpdir)"
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    XYZ_OOKLA_BIN="${XYZ_OOKLA_EXTRACTED}"
    return 0
  fi
  if xyz_is_root && xyz_confirm "$(xyz_t \
      "Install the official Speedtest CLI to /usr/local/bin/speedtest?" \
      "将官方 Speedtest CLI 安装到 /usr/local/bin/speedtest?")"; then
    install -m 0755 "${XYZ_OOKLA_EXTRACTED}" /usr/local/bin/speedtest
    XYZ_OOKLA_BIN="/usr/local/bin/speedtest"
    xyz_info "$(xyz_t "Installed:" "已安装:") ${XYZ_OOKLA_BIN}"
  else
    XYZ_OOKLA_BIN="${XYZ_OOKLA_EXTRACTED}"
    xyz_info "$(xyz_t "Using a temporary copy for this run only." "本次仅使用临时副本。")"
  fi
}

# Print Ookla's terms once and ask before passing the accept flags.
xyz_ookla_accept() {
  xyz_info "$(xyz_t \
    "Ookla's official CLI will contact Ookla servers. Ookla has its own license and privacy terms:" \
    "接下来会运行 Ookla 官方 CLI,它会连接 Ookla 服务器。Ookla 有自己的许可与隐私条款:")"
  printf '  https://www.speedtest.net/about/privacy\n'
  printf '  https://www.speedtest.net/terms\n'
  xyz_confirm "$(xyz_t \
    "Accept Ookla's terms for this run (--accept-license --accept-gdpr)?" \
    "本次运行接受 Ookla 条款(--accept-license --accept-gdpr)?")"
}

# xyz.sh terminal report helpers: frames, colored badges, bar charts.
# Shared by speedtest-cn, route, unlock and all. Pure bash, no telemetry.
#
# Colors: on only when stdout is a terminal (or XYZ_COLOR=always). Off with
# --no-color, NO_COLOR=1 or XYZ_COLOR=never.
# Glyphs: Unicode box drawing when the locale is UTF-8, otherwise ASCII
# (force with XYZ_ASCII=1 or XYZ_UTF8=1). Frames are 78 columns wide.

# shellcheck disable=SC2034
XYZ_UI_LOADED=1
XYZ_UI_WIDTH=78

xyz_ui_init() {
  local want="${XYZ_COLOR:-auto}"
  [[ "${want}" == "auto" && -n "${NO_COLOR:-}" ]] && want="never"
  [[ "${XYZ_NO_COLOR:-0}" == "1" ]] && want="never"
  XYZ_UI_COLOR=0
  case "${want}" in
    always) XYZ_UI_COLOR=1 ;;
    never) XYZ_UI_COLOR=0 ;;
    *) [[ -t 1 ]] && XYZ_UI_COLOR=1 ;;
  esac
  if [[ "${XYZ_UI_COLOR}" == "1" ]]; then
    U_X=$'\033[0m'; U_B=$'\033[1m'; U_D=$'\033[2m'
    U_RED=$'\033[31m'; U_G=$'\033[32m'; U_Y=$'\033[33m'
    U_BL=$'\033[34m'; U_M=$'\033[35m'; U_C=$'\033[36m'
  else
    U_X=""; U_B=""; U_D=""; U_RED=""; U_G=""; U_Y=""; U_BL=""; U_M=""; U_C=""
  fi
  XYZ_UI_UTF8=0
  if [[ "${XYZ_ASCII:-0}" != "1" ]]; then
    case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
      *[Uu][Tt][Ff]-8*|*[Uu][Tt][Ff]8*) XYZ_UI_UTF8=1 ;;
      *) XYZ_UI_UTF8=0 ;;
    esac
    [[ "${XYZ_UTF8:-0}" == "1" ]] && XYZ_UI_UTF8=1
  fi
  if [[ "${XYZ_UI_UTF8}" == "1" ]]; then
    G_H="─"; G_V="│"; G_TL="╭"; G_TR="╮"; G_BL="╰"; G_BR="╯"; G_LT="├"; G_RT="┤"
    G_FULL="█"; G_EMPTY="░"; G_OK="✔"; G_BAD="✘"; G_UNK="?"; G_DOT="●"; G_STAR="★"
    G_DOWN="↓"; G_UP="↑"; G_WARN="⚠"; G_MID="·"; G_TIMES="×"
  else
    G_H="-"; G_V="|"; G_TL="+"; G_TR="+"; G_BL="+"; G_BR="+"; G_LT="+"; G_RT="+"
    G_FULL="#"; G_EMPTY="."; G_OK="+"; G_BAD="x"; G_UNK="?"; G_DOT="*"; G_STAR="*"
    G_DOWN="v"; G_UP="^"; G_WARN="!"; G_MID="-"; G_TIMES="x"
  fi
}

# Display width of a string: ANSI codes removed, CJK counted as 2 columns.
xyz_ui_w() {
  local s="$1" pre rest
  while [[ "${s}" == *$'\033['* ]]; do
    pre="${s%%$'\033['*}"
    rest="${s#*$'\033['}"
    rest="${rest#*m}"
    s="${pre}${rest}"
  done
  local LC_ALL=C
  local a="${s//[^ -~]/}"
  local one="${s//[^$'\xc2'-$'\xdf'$'\xe2']/}"
  local two="${s//[^$'\xe3'-$'\xef']/}"
  printf '%d' $(( ${#a} + ${#one} + 2 * ${#two} ))
}

xyz_ui_rep() {
  # xyz_ui_rep CHAR N
  local out="" n="$2"
  while (( n > 0 )); do out="${out}$1"; n=$((n - 1)); done
  printf '%s' "${out}"
}

xyz_ui_pad() {
  # xyz_ui_pad TEXT WIDTH  (left aligned, never truncates)
  local w
  w="$(xyz_ui_w "$1")"
  printf '%s%s' "$1" "$(xyz_ui_rep ' ' $(( $2 - w )))"
}

xyz_ui_lpad() {
  # xyz_ui_lpad TEXT WIDTH  (right aligned)
  local w
  w="$(xyz_ui_w "$1")"
  printf '%s%s' "$(xyz_ui_rep ' ' $(( $2 - w )))" "$1"
}

xyz_ui_top() {
  local title=" $1 " w
  w="$(xyz_ui_w "${title}")"
  printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_TL}${G_H}" "${U_X}${U_B}" "${title}" "${U_X}${U_C}" \
    "$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_TR}" "${U_X}"
}

xyz_ui_sep() {
  local label="${1:-}" w=0
  if [[ -n "${label}" ]]; then
    label=" ${label} "
    w="$(xyz_ui_w "${label}")"
    printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_LT}${G_H}" "${U_X}${U_B}" "${label}" "${U_X}${U_C}" \
      "$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_RT}" "${U_X}"
  else
    printf '%s%s%s\n' "${U_C}" "${G_LT}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_RT}" "${U_X}"
  fi
}

xyz_ui_row() {
  local text="$1" inner=$(( XYZ_UI_WIDTH - 4 )) plain
  if (( $(xyz_ui_w "${text}") > inner )); then
    # too wide: drop colors and cut to fit, ending with "~"
    plain="$(printf '%s' "${text}" | sed 's/\x1b\[[0-9;]*m//g')"
    plain="${plain:0:inner}"
    while (( $(xyz_ui_w "${plain}") > inner - 1 )); do plain="${plain%?}"; done
    text="${plain}~"
  fi
  printf '%s%s%s %s %s%s%s\n' "${U_C}" "${G_V}" "${U_X}" "$(xyz_ui_pad "${text}" "${inner}")" "${U_C}" "${G_V}" "${U_X}"
}

xyz_ui_bottom() {
  printf '%s%s%s\n' "${U_C}" "${G_BL}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_BR}" "${U_X}"
}

xyz_ui_bar() {
  # xyz_ui_bar VALUE MAX WIDTH [COLOR]  -> filled/empty bar (at least 1 cell if VALUE > 0)
  local n color="${4:-${U_G}}"
  n="$(awk -v v="${1:-0}" -v m="${2:-0}" -v w="$3" 'BEGIN {
    if (m <= 0 || v <= 0) { print 0; exit }
    n = int(v / m * w + 0.5); if (n < 1) n = 1; if (n > w) n = w; print n }')"
  printf '%s%s%s%s%s' "${color}" "$(xyz_ui_rep "${G_FULL}" "${n}")" "${U_D}" "$(xyz_ui_rep "${G_EMPTY}" $(( $3 - n )))" "${U_X}"
}

xyz_ui_usage() {
  # xyz_ui_usage PERCENT WIDTH -> colored usage bar + percent (red > 90, yellow > 75)
  local p="${1:-0}" color="${U_G}"
  p="${p%%.*}"; [[ "${p}" =~ ^[0-9]+$ ]] || p=0
  (( p > 75 )) && color="${U_Y}"
  (( p > 90 )) && color="${U_RED}"
  printf '%s %s%3d%%%s' "$(xyz_ui_bar "${p}" 100 "$2" "${color}")" "${color}" "${p}" "${U_X}"
}

xyz_ui_badge() {
  # xyz_ui_badge yes|no|partial|unknown|ok|skip [TEXT]
  local txt="${2:-}"
  case "$1" in
    yes|ok) printf '%s%s %s%s' "${U_G}" "${G_OK}" "${txt:-$(xyz_t "yes" "解锁")}" "${U_X}" ;;
    no|fail) printf '%s%s %s%s' "${U_RED}" "${G_BAD}" "${txt:-$(xyz_t "no" "不可用")}" "${U_X}" ;;
    partial) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "partial" "部分")}" "${U_X}" ;;
    skip) printf '%s%s %s%s' "${U_D}" "-" "${txt:-$(xyz_t "skipped" "跳过")}" "${U_X}" ;;
    *) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "unknown" "未知")}" "${U_X}" ;;
  esac
}

xyz_ui_line_class() {
  # premium | good | ordinary | other
  case "$1" in
    CN2-GIA|9929/CUII|9929|CMIN2) printf 'premium' ;;
    CN2-GT) printf 'good' ;;
    163|4837|CMI|CMNET) printf 'ordinary' ;;
    *) printf 'other' ;;
  esac
}

xyz_ui_linetags() {
  # xyz_ui_linetags "CN2-GIA 9929/CUII" -> colored tags
  local t out="" c
  for t in $1; do
    case "$(xyz_ui_line_class "${t}")" in
      premium) c="${U_G}${U_B}" ;;
      good) c="${U_C}" ;;
      ordinary) c="${U_Y}" ;;
      *) c="${U_D}" ;;
    esac
    out="${out}${out:+ }${c}${t}${U_X}"
  done
  printf '%s' "${out:-${U_D}?${U_X}}"
}

xyz_ui_fill() {
  # Replace empty TSV fields with "-" (bash read collapses consecutive tabs).
  awk -F'\t' -v OFS='\t' '{ for (i = 1; i <= NF; i++) if ($i == "") $i = "-"; print }' "$1"
}

xyz_ui_carrier() {
  case "$1" in
    nearest) xyz_t "Local" "本地" ;;
    ct) xyz_t "Telecom" "电信" ;;
    cu) xyz_t "Unicom" "联通" ;;
    cm) xyz_t "Mobile" "移动" ;;
    custom) xyz_t "Custom" "自定义" ;;
    hk) xyz_t "HK" "香港" ;;
    tokyo) xyz_t "Tokyo" "东京" ;;
    singapore) xyz_t "SGP" "新加坡" ;;
    us-west) xyz_t "US West" "美西" ;;
    *) printf '%s' "$1" ;;
  esac
}

xyz_ui_carrier_color() {
  case "$1" in
    ct|tokyo) printf '%s' "${U_BL}" ;;
    cu) printf '%s' "${U_RED}" ;;
    cm|singapore) printf '%s' "${U_G}" ;;
    hk) printf '%s' "${U_Y}" ;;
    *) printf '%s' "${U_M}" ;;
  esac
}

xyz_ui_fmt_num() {
  # 2 decimals below 1000, 1 decimal above
  awk -v v="${1:-0}" 'BEGIN { if (v >= 1000) printf "%.1f", v; else printf "%.2f", v }'
}

# xyz_ui_speed_rows FILE
# FILE: carrier id label down up ping loss status [engine] [note] (TSV, Mbps / ms)
xyz_ui_speed_rows() {
  local f="$1" max carrier id label down up lat loss status engine note head
  [[ -s "${f}" ]] || return 0
  max="$(awk -F'\t' '$8=="ok" { if ($4+0 > m) m = $4+0; if ($5+0 > m) m = $5+0 } END { print m+0 }' "${f}")"
  while IFS=$'\t' read -r carrier id label down up lat loss status engine note; do
    [[ "${engine}" == "-" ]] && engine=""
    head="$(xyz_ui_carrier_color "${carrier}")$(xyz_ui_pad "$(xyz_ui_carrier "${carrier}")" 8)${U_X}${U_B}${label}${U_X}${U_D} #${id}${engine:+ ${G_MID} ${engine}}${U_X}"
    xyz_ui_row "${head}"
    if [[ "${status}" == "ok" ]]; then
      local pc="${U_G}"
      lat="${lat:-?}"
      awk -v p="${lat}" 'BEGIN { exit !(p+0 > 150) }' 2>/dev/null && pc="${U_Y}"
      awk -v p="${lat}" 'BEGIN { exit !(p+0 > 250) }' 2>/dev/null && pc="${U_RED}"
      xyz_ui_row "  ${G_DOWN} $(xyz_ui_bar "${down}" "${max}" 14 "${U_G}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${down}")" 7) Mbps  ${G_UP} $(xyz_ui_bar "${up}" "${max}" 14 "${U_C}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${up}")" 7) Mbps  ${pc}$(xyz_ui_lpad "${lat}" 6)ms${U_X}"
    else
      [[ "${note}" == "-" ]] && note=""
      xyz_ui_row "  $(xyz_ui_badge skip "$(xyz_t "skipped" "跳过")")${U_D}${note:+ ${G_MID} ${note:0:62}}${U_X}"
    fi
  done < <(xyz_ui_fill "${f}")
}

xyz_ui_speed_table() {
  # xyz_ui_speed_table FILE TITLE
  xyz_ui_top "$2"
  xyz_ui_speed_rows "$1"
  xyz_ui_bottom
}

# Route grid. FILE: target label kind hops [key]   (key like bj-ct)
xyz_ui_route_key() {
  case "$1" in
    219.141.140.10) printf 'bj-ct' ;; 202.106.195.68) printf 'bj-cu' ;; 221.179.155.161) printf 'bj-cm' ;;
    202.96.209.133) printf 'sh-ct' ;; 210.22.97.1) printf 'sh-cu' ;; 211.136.112.200) printf 'sh-cm' ;;
    58.60.188.222) printf 'gz-ct' ;; 210.21.196.6) printf 'gz-cu' ;; 120.196.165.24) printf 'gz-cm' ;;
    *) printf '' ;;
  esac
}

xyz_ui_route_rows() {
  local f="$1" target label kind hops key city c cell line extra=0
  local -A K=()
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r target label kind hops key; do
    [[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
    if [[ -n "${key}" ]]; then
      K["${key}"]="${kind}"
    else
      extra=1
    fi
  done < <(xyz_ui_fill "${f}")
  if (( ${#K[@]} > 0 )); then
    xyz_ui_row "${U_D}$(xyz_ui_pad "" 10)$(xyz_ui_pad "$(xyz_ui_carrier ct)" 21)$(xyz_ui_pad "$(xyz_ui_carrier cu)" 21)$(xyz_ui_carrier cm)${U_X}"
    for city in bj sh gz; do
      case "${city}" in
        bj) line="$(xyz_ui_pad "$(xyz_t "Beijing" "北京")" 10)" ;;
        sh) line="$(xyz_ui_pad "$(xyz_t "Shanghai" "上海")" 10)" ;;
        *) line="$(xyz_ui_pad "$(xyz_t "Guangzhou" "广州")" 10)" ;;
      esac
      for c in ct cu cm; do
        cell="${K[${city}-${c}]-}"
        [[ "${cell}" == *" ("* || "${cell}" == unknown* || "${cell}" == 未识别* ]] && cell="?"
        if [[ -z "${cell}" ]]; then cell="${U_D}-${U_X}"; else cell="$(xyz_ui_linetags "${cell}")"; fi
        line="${line}$(xyz_ui_pad "${cell}" 21)"
      done
      xyz_ui_row "${line}"
    done
  fi
  if [[ "${extra}" == "1" ]]; then
    while IFS=$'\t' read -r target label kind hops key; do
      [[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
      [[ -n "${key}" ]] && continue
      xyz_ui_row "$(xyz_ui_pad "${label:-${target}}" 22) $(xyz_ui_linetags "${kind}")"
    done < <(xyz_ui_fill "${f}")
  fi
  xyz_ui_row "${U_D}$(xyz_t "Legend:" "图例:")${U_X} ${U_G}${U_B}$(xyz_t "premium" "优质")${U_X} CN2-GIA/9929/CMIN2  ${U_C}CN2-GT${U_X}  ${U_Y}$(xyz_t "ordinary" "普通")${U_X} 163/4837/CMI"
}

xyz_ui_route_score() {
  # prints 0-100 (average best class per target) or nothing
  local f="$1" kind t best n=0 sum=0 cls
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r _ _ kind _ _; do
    best=0
    for t in ${kind}; do
      cls="$(xyz_ui_line_class "${t}")"
      case "${cls}" in premium) best=100 ;; good) (( best < 75 )) && best=75 ;; ordinary) (( best < 40 )) && best=40 ;; *) : ;; esac
    done
    n=$((n + 1)); sum=$((sum + best))
  done < "${f}"
  (( n > 0 )) && printf '%d' $(( sum / n ))
}

# Unlock grid. FILE: service status region note
xyz_ui_unlock_rows() {
  local f="$1" svc st reg note cells=() i cell
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r svc st reg note; do
    cell="$(xyz_ui_badge "${st}" "$(xyz_ui_pad "${svc}" 16)")"
    [[ "${reg}" != "-" ]] && cell="${cell} ${U_D}${reg:0:12}${U_X}"
    cells+=("${cell}")
  done < <(xyz_ui_fill "${f}")
  for (( i = 0; i < ${#cells[@]}; i += 2 )); do
    xyz_ui_row "$(xyz_ui_pad "${cells[$i]}" 37)${cells[$((i + 1))]-}"
  done
}

xyz_ui_unlock_count() {
  # prints "yes total"
  awk -F'\t' '{ n++; if ($2 == "yes") y++ } END { printf "%d %d\n", y, n }' "$1"
}

xyz_ui_init

XYZ_NAME="speedtest"

# xyz.sh/speedtest — install and run the official Ookla Speedtest CLI.
#
# Official product  : https://www.speedtest.net/apps/cli
# Official tarballs : https://install.speedtest.net/app/cli/ookla-speedtest-<ver>-linux-<arch>.tgz
#
# This wrapper downloads Ookla's official Linux tarball (no package repository
# is added), installs the `speedtest` binary, then optionally runs a test.
# Ookla's CLI contacts Ookla servers (that is the test) and has its own
# license / GDPR prompts. This wrapper will not silently pass
# --accept-license or --accept-gdpr unless you confirm (or use -y after the
# notice is printed).
#
# Usage:
#   curl -fsSL xyz.sh/speedtest | bash
#   curl -fsSL xyz.sh/speedtest | bash -s -- -y
#   curl -fsSL xyz.sh/speedtest | bash -s -- --install-only
#   curl -fsSL xyz.sh/speedtest | bash -s -- --dry-run
#   curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall
#
# Supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux (any Linux arch listed
# on Ookla's download page: x86_64, aarch64, armhf, armel, i386).
# Requires root to install into /usr/local/bin; --keep can copy the binary
# into the current directory without a system install.
# Running an already-installed official binary does not require root.
# No telemetry is added by this wrapper.

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

if [[ -z "${XYZ_OOKLA_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  # shellcheck source=lib/ookla.sh
  source "${_xyz_dir}/lib/ookla.sh"
fi
if [[ -z "${XYZ_UI_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  if [[ -f "${_xyz_dir}/lib/report.sh" ]]; then
    # shellcheck source=lib/report.sh
    source "${_xyz_dir}/lib/report.sh"
  fi
fi

XYZ_NAME="speedtest"
XYZ_TITLE="$(xyz_t "Ookla Speedtest CLI" "Ookla Speedtest 命令行")"
XYZ_UPSTREAM="https://www.speedtest.net/apps/cli"
# OOKLA_CLI_VERSION / OOKLA_CLI_BASE and the pinned SHA256 table live in lib/ookla.sh.
INSTALL_PREFIX="/usr/local/bin"
DO_INSTALL_ONLY=0
DO_UNINSTALL=0
DO_KEEP=0
DO_NO_INSTALL=0
DO_REINSTALL=0
DO_OVERSEAS=0
DO_LIST=0

xyz_init "$@"

xyz_speedtest_uninstall_help() {
  cat <<HINT
$(xyz_t "Uninstall:" "卸载:")
  curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall
  $(xyz_t "or delete the binary yourself:" "或自行删除二进制文件:")
  rm -f /usr/local/bin/speedtest
  $(xyz_t "License acceptance may live in ~/.config/ookla; remove that directory if you want a clean slate." "许可接受记录可能在 ~/.config/ookla,如需彻底清理可删除该目录。")
  $(xyz_t "If an older package-manager install still exists, also run apt-get/dnf/yum remove speedtest." "若以前用包管理器安装过 speedtest,请另外执行 apt-get/dnf/yum remove speedtest。")
HINT
}

i=0
while [[ $i -lt ${#XYZ_ARGS[@]} ]]; do
  arg="${XYZ_ARGS[$i]}"
  case "${arg}" in
    --install-only) DO_INSTALL_ONLY=1 ;;
    --uninstall|--remove) DO_UNINSTALL=1 ;;
    --keep) DO_KEEP=1 ;;
    --no-install) DO_NO_INSTALL=1 ;;
    --reinstall) DO_REINSTALL=1 ;;
    --overseas|--regions) DO_OVERSEAS=1 ;;
    --list) DO_LIST=1 ;;
    --prefix)
      i=$((i + 1))
      INSTALL_PREFIX="${XYZ_ARGS[$i]:-}"
      if [[ -z "${INSTALL_PREFIX}" ]]; then
        xyz_error "$(xyz_t "--prefix requires a directory." "--prefix 需要目录。")"
        exit 1
      fi
      ;;
    --prefix=*) INSTALL_PREFIX="${arg#--prefix=}" ;;
    --help|-h) XYZ_HELP=1 ;;
    *)
      xyz_error "$(xyz_t "Unknown argument:" "未知参数:") ${arg}"
      exit 1
      ;;
  esac
  i=$((i + 1))
done

DEST="${INSTALL_PREFIX%/}/speedtest"

if [[ "${XYZ_HELP:-}" == "1" ]]; then
  cat <<HINT
$(xyz_t "Usage:" "用法:")
  curl -fsSL xyz.sh/speedtest | bash
  curl -fsSL xyz.sh/speedtest | bash -s -- -y
  curl -fsSL xyz.sh/speedtest | bash -s -- -y
  curl -fsSL xyz.sh/speedtest | bash -s -- --overseas -y
  curl -fsSL xyz.sh/speedtest | bash -s -- --list
  curl -fsSL xyz.sh/speedtest | bash -s -- --install-only
  curl -fsSL xyz.sh/speedtest | bash -s -- --dry-run
  curl -fsSL xyz.sh/speedtest | bash -s -- --keep --no-install
  curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall

Official: https://www.speedtest.net/apps/cli
$(xyz_t "Downloads the official Linux tarball (no apt/dnf repository is added) into a temporary directory, then installs speedtest to ${DEST}." "从官方 Linux tarball 下载(不会添加 apt/dnf 软件源)到临时目录,再安装到 ${DEST}。")
$(xyz_t "Default: one test against the server Ookla picks as nearest." "默认对 Ookla 自动选择的就近节点测一次。")
$(xyz_t "--overseas also tests Hong Kong, Tokyo, Singapore and the US West. The official CLI is tried first (servers looked up by region); if that fails, speedtest-go (official release, SHA256 pinned, temp dir only) is used. Mainland China nodes are not tested." "--overseas 会再测香港、东京、新加坡、美西:先用官方 CLI 按地区检索服务器,失败则用 speedtest-go(官方发布版,SHA256 固定,只在临时目录运行)。不测试中国大陆节点。")
$(xyz_t "--keep copies the binary into the current directory as ./speedtest. --no-install skips ${DEST}." "--keep 会把二进制复制到当前目录的 ./speedtest。--no-install 不写入 ${DEST}。")

$(xyz_speedtest_uninstall_help)
HINT
  exit 0
fi

# ---- overseas multi-node test (Hong Kong / Tokyo / Singapore / US West) --------
# id|region|name-zh|name-en|host:port
OV_NODES=(
  "1536|hk|香港 HKBN|Hong Kong (HKBN)|speedtest.hkbn.net:8080"
  "40508|hk|香港|Hong Kong|"
  "28910|tokyo|东京|Tokyo|"
  "2433|tokyo|东京|Tokyo|"
  "4235|singapore|新加坡 StarHub|Singapore (StarHub)|"
  "7311|singapore|新加坡|Singapore|"
  "14236|us-west|洛杉矶|Los Angeles|"
  "17384|us-west|圣何塞|San Jose|"
)
OV_REGIONS=(hk tokyo singapore us-west)
GO_VERSION="1.8.3"
GO_BASE="https://github.com/showwin/speedtest-go/releases/download/v${GO_VERSION}"

ov_field() { printf '%s' "$1" | cut -d'|' -f"$2"; }
ov_name() { xyz_t "$(ov_field "$1" 4)" "$(ov_field "$1" 3)"; }
ov_region_label() { xyz_ui_carrier "$1" 2>/dev/null || printf '%s' "$1"; }

print_ov_nodes() {
  local n
  printf '  %-8s %-10s %s\n' "ID" "$(xyz_t "Region" "地区")" "$(xyz_t "Node" "节点")"
  for n in "${OV_NODES[@]}"; do
    printf '  %-8s %-10s %s\n' "$(ov_field "${n}" 1)" "$(ov_region_label "$(ov_field "${n}" 2)")" "$(ov_name "${n}")"
  done
}

if [[ "${DO_LIST}" == "1" ]]; then
  xyz_step "$(xyz_t "Overseas nodes (built-in candidates; a live search is used at run time)" "境外节点(内置候选;运行时还会按地区检索)")"
  print_ov_nodes
  exit 0
fi

# ---- helpers used by --overseas ------------------------------------------------
go_asset_arch() {
  case "$(uname -m)" in
    x86_64|amd64) printf 'x86_64' ;;
    aarch64|arm64) printf 'arm64' ;;
    armv7l|armv7hl|armhf) printf 'armv7' ;;
    armv6l) printf 'armv6' ;;
    i386|i486|i586|i686) printf 'i386' ;;
    *) return 1 ;;
  esac
}
go_sha256() {
  case "$1" in
    x86_64) printf 'c55caae22927cd719a2f8c0bef96ecf74b5fe8ca919ff9200d76328ae5de9477' ;;
    arm64) printf '48f51504548d76d5dc2ce7f72e713bd3b4e35554028b7131467060688754c657' ;;
    armv7) printf '29d6f7b1038d765970c4f0a24e2e26a80b2041f58a7ce3c2e1b4318317c0fbe0' ;;
    armv6) printf '395fd65509af7297b62d8942a323483b48c176cd9404889cc499870fe291d596' ;;
    i386) printf '7b9641833a94c5937d9872f589e017c0fa5e3ef9c4a36a4d88f3b5c7355dc053' ;;
    *) return 1 ;;
  esac
}
GO_BIN=""
GO_TRIED=0
fetch_go() {
  [[ -n "${GO_BIN}" ]] && return 0
  [[ "${GO_TRIED}" == "1" ]] && return 1
  GO_TRIED=1
  local arch asset want got dir
  xyz_init_tmpdir
  dir="$(xyz_tmpdir)/speedtest-go"
  arch="$(go_asset_arch)" || { xyz_warn "$(xyz_t "speedtest-go has no build for" "speedtest-go 没有对应架构:") $(uname -m)"; return 1; }
  asset="speedtest-go_${GO_VERSION}_Linux_${arch}.tar.gz"
  want="$(go_sha256 "${arch}")"
  xyz_info "$(xyz_t "speedtest-go (official release, run from a temp dir, nothing installed):" "speedtest-go(官方发布版,仅在临时目录运行,不安装):") ${GO_BASE}/${asset}"
  if [[ "${1:-}" != "noconfirm" ]] && ! xyz_confirm "$(xyz_t "Download speedtest-go as a fallback if the official CLI cannot select a server?" "若官方 CLI 无法选中节点,是否下载 speedtest-go 作为备选?")"; then
    return 1
  fi
  mkdir -p "${dir}"
  if ! curl -fsSL --connect-timeout 15 --max-time 120 -o "${dir}/${asset}" "${GO_BASE}/${asset}"; then
    xyz_warn "$(xyz_t "Could not download speedtest-go." "无法下载 speedtest-go。")"
    return 1
  fi
  got="$(sha256sum "${dir}/${asset}" | awk '{print $1}')"
  if [[ "${got}" != "${want}" ]]; then
    xyz_warn "$(xyz_t "speedtest-go SHA256 mismatch, not using it:" "speedtest-go SHA256 不匹配,不使用:") ${got}"
    return 1
  fi
  xyz_info "$(xyz_t "SHA256 verified:" "SHA256 已校验:") ${got}"
  tar -xzf "${dir}/${asset}" -C "${dir}" speedtest-go 2>/dev/null || tar -xzf "${dir}/${asset}" -C "${dir}"
  if [[ ! -x "${dir}/speedtest-go" ]]; then
    xyz_warn "$(xyz_t "The speedtest-go archive had no binary." "speedtest-go 压缩包里没有可执行文件。")"
    return 1
  fi
  GO_BIN="${dir}/speedtest-go"
}

mbps_bytes() { awk -v b="${1:-0}" 'BEGIN { printf "%.2f", b * 8 / 1000000 }'; }
explain_error() {
  local f="$1" msgs
  msgs="$(grep -o '"message":"[^"]*"' "${f}" 2>/dev/null | cut -d'"' -f4 | paste -sd ';' - || true)"
  if [[ -n "${msgs}" ]]; then printf '%s' "${msgs}"
  else tr '\n\r' '  ' < "${f}" | sed 's/  */ /g; s/^ //; s/ $//'; fi
}

ov_record() {
  printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$@" >> "${OV_RESULTS}"
}
ov_skip() {
  xyz_warn "$(xyz_t "Node failed, skipping:" "节点不可用,已跳过:") $3 (#$2, $4): $5"
  ov_record "$1" "$2" "$3" "-" "-" "-" "-" "skip" "$4" "$(printf '%s' "$5" | tr '\t' ' ' | cut -c1-160)"
}
ov_probe() {
  local code
  code="$(curl -s -o /dev/null -m 5 --connect-timeout 4 -w '%{http_code}' "$1" 2>/dev/null || true)"
  [[ "${code}" =~ ^[234][0-9][0-9]$ ]]
}

run_ookla_ov() {
  local id="$1" carrier="$2" label="$3" out err runner sel
  out="${OV_TMP}/st-${carrier}-${id}.json"
  err="${OV_TMP}/st-${carrier}-${id}.err"
  runner=(); sel=(-s "${id}")
  xyz_need_cmd timeout && runner=(timeout 120)
  [[ "${id}" == "auto" ]] && sel=()
  printf '  %s %s (#%s, Ookla CLI)...\n' "$(xyz_t "Testing" "正在测试")" "${label}" "${id}"
  OOKLA_ERR=""
  if "${runner[@]+"${runner[@]}"}" "${RUN_BIN}" --accept-license --accept-gdpr \
      "${sel[@]+"${sel[@]}"}" -f json -p no >"${out}" 2>"${err}" \
      && grep -q '"type":"result"' "${out}"; then
    local lat down up loss sname sid
    lat="$(grep -o '"latency":[0-9.]*' "${out}" | head -1 | cut -d: -f2)"
    down="$(grep -o '"download":{"bandwidth":[0-9]*' "${out}" | grep -o '[0-9]*$')"
    up="$(grep -o '"upload":{"bandwidth":[0-9]*' "${out}" | grep -o '[0-9]*$')"
    loss="$(grep -o '"packetLoss":[0-9.]*' "${out}" | head -1 | cut -d: -f2)"
    if [[ "${id}" == "auto" ]]; then
      sname="$(grep -o '"server":{[^}]*' "${out}" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)"
      sid="$(grep -o '"server":{"id":[0-9]*' "${out}" | grep -o '[0-9]*$')"
      label="${label}: ${sname:-?}"
      id="${sid:-auto}"
    fi
    ov_record "${carrier}" "${id}" "${label}" "$(mbps_bytes "${down}")" "$(mbps_bytes "${up}")" "${lat:-?}" "${loss:-n/a}" ok ookla ""
    OV_USED_IDS="${OV_USED_IDS} ${id} "
    return 0
  fi
  cat "${out}" >> "${err}" 2>/dev/null || true
  OOKLA_ERR="$(explain_error "${err}")"
  [[ -z "${OOKLA_ERR}" ]] && OOKLA_ERR="$(xyz_t "no result (exit code or timeout)" "没有结果(退出或超时)")"
  return 1
}

run_go_ov() {
  local hp="$1" id="$2" carrier="$3" label="$4" out runner
  out="${OV_TMP}/go-${id}.json"
  runner=()
  GO_ERR=""
  if ! fetch_go; then GO_ERR="$(xyz_t "speedtest-go unavailable" "speedtest-go 不可用")"; return 1; fi
  xyz_need_cmd timeout && runner=(timeout 90)
  printf '  %s %s (#%s, speedtest-go %s)...\n' "$(xyz_t "Testing" "正在测试")" "${label}" "${id}" "${hp}"
  if "${runner[@]+"${runner[@]}"}" "${GO_BIN}" --custom-url="http://${hp}" --json >"${out}" 2>&1 \
      && grep -q '"dl_speed"' "${out}"; then
    local lat dl ul
    lat="$(grep -o '"latency":[0-9]*' "${out}" | head -1 | cut -d: -f2)"
    dl="$(grep -o '"dl_speed":[0-9.e+]*' "${out}" | head -1 | cut -d: -f2)"
    ul="$(grep -o '"ul_speed":[0-9.e+]*' "${out}" | head -1 | cut -d: -f2)"
    if awk -v d="${dl:-0}" -v u="${ul:-0}" 'BEGIN { exit !(d + u > 0) }'; then
      ov_record "${carrier}" "${id}" "${label}" "$(mbps_bytes "${dl}")" "$(mbps_bytes "${ul}")" \
        "$(awk -v n="${lat:-0}" 'BEGIN { printf "%.1f", n / 1000000 }')" "n/a" ok speedtest-go ""
      OV_USED_IDS="${OV_USED_IDS} ${id} "
      return 0
    fi
  fi
  GO_ERR="$(explain_error "${out}")"
  [[ -z "${GO_ERR}" ]] && GO_ERR="$(xyz_t "no result" "没有结果")"
  return 1
}

search_term() {
  case "$1" in
    hk) printf 'Hong Kong' ;;
    tokyo) printf 'Tokyo' ;;
    singapore) printf 'Singapore' ;;
    us-west) printf 'Los Angeles' ;;
    *) printf '%s' "$1" ;;
  esac
}

fetch_live_region() {
  local region="$1" term q url tmp py
  term="$(search_term "${region}")"
  q="$(printf '%s' "${term}" | sed 's/ /%20/g')"
  url="https://www.speedtest.net/api/js/servers?engine=js&https_functional=true&limit=8&search=${q}"
  tmp="${OV_TMP}/live-${region}.json"
  xyz_info "$(xyz_t "Looking up" "正在检索") ${term}: ${url}" >&2
  if ! curl -fsSL --connect-timeout 10 --max-time 20 -A "curl xyz.sh/speedtest" -o "${tmp}" "${url}"; then
    return 1
  fi
  py="$(command -v python3 || command -v python || true)"
  if [[ -z "${py}" ]]; then return 1; fi
  "${py}" - "${tmp}" "${region}" <<'PY'
import json, sys
path, region = sys.argv[1], sys.argv[2]
try:
    data = json.load(open(path, encoding="utf-8"))
except Exception:
    sys.exit(1)
if isinstance(data, dict):
    data = data.get("servers") or data.get("data") or []
for s in data or []:
    if not isinstance(s, dict):
        continue
    sid = s.get("id") or s.get("server_id")
    if sid is None:
        continue
    host = (s.get("host") or "").replace(".prod.hosts.ooklaserver.net", "")
    cc = (s.get("cc") or s.get("country_code") or s.get("country") or "")
    name = s.get("name") or s.get("sponsor") or "server"
    sponsor = s.get("sponsor") or name
    if region == "hk" and cc and cc not in ("HK", "Hong Kong"):
        continue
    if region == "tokyo" and cc and cc not in ("JP", "Japan"):
        continue
    if region == "singapore" and cc and cc not in ("SG", "Singapore"):
        continue
    if region == "us-west":
        blob = " ".join([str(cc), str(name), str(s.get("location") or ""), str(sponsor)])
        if "CN" == str(cc) or "China" in blob:
            continue
        if not any(k in blob for k in ("Los Angeles", "San Jose", "San Francisco", "Seattle", "San Diego", "Phoenix", "Las Vegas", "Fremont", "Santa Clara", "California", "Washington", "Oregon", "Arizona", "Nevada", "CA", "WA", "OR")):
            if str(cc) not in ("US", "United States", ""):
                continue
    label = "%s (%s)" % (name, sponsor) if sponsor and sponsor != name else str(name)
    sys.stdout.write("%s|%s|%s|%s|%s\n" % (sid, region, label, label, host))
PY
}

candidates_for() {
  local region="$1" n
  for n in "${OV_NODES[@]}"; do
    [[ "$(ov_field "${n}" 2)" == "${region}" ]] || continue
    printf '%s\n' "${n}"
  done
  fetch_live_region "${region}" 2>/dev/null || true
}

test_region() {
  local region="$1" ok=0 tries=0 line id zh en host label why
  xyz_step "$(xyz_t "Region" "地区"): $(ov_region_label "${region}")"
  while IFS= read -r line; do
    [[ -z "${line}" ]] && continue
    id="$(ov_field "${line}" 1)"
    [[ "${id}" =~ ^[0-9]+$ ]] || continue
    [[ "${OV_USED_IDS}" == *" ${id} "* ]] && continue
    zh="$(ov_field "${line}" 3)"
    en="$(ov_field "${line}" 4)"
    host="$(ov_field "${line}" 5)"
    label="$(xyz_t "${en}" "${zh}")"
    tries=$((tries + 1))
    if (( tries > 3 )); then break; fi
    if run_ookla_ov "${id}" "${region}" "${label}"; then ok=1; break
    else
      why="Ookla CLI: ${OOKLA_ERR}"
      if [[ -n "${host}" ]]; then
        if ! ov_probe "http://${host}/hello"; then
          ov_skip "${region}" "${id}" "${label}" "probe" "${why}; $(xyz_t "server unreachable:" "服务器无法连接:") http://${host}"
          continue
        fi
        if run_go_ov "${host}" "${id}" "${region}" "${label}"; then ok=1; break
        else
          ov_skip "${region}" "${id}" "${label}" "ookla/go" "${why}; speedtest-go: ${GO_ERR}"
        fi
      else
        ov_skip "${region}" "${id}" "${label}" "ookla" "${why}"
      fi
    fi
  done < <(candidates_for "${region}")
  if [[ "${ok}" == "0" && "${tries}" == "0" ]]; then
    ov_record "${region}" "-" "$(ov_region_label "${region}")" "-" "-" "-" "-" "skip" "-" "$(xyz_t "no candidate server" "没有候选节点")"
  fi
}

xyz_run_overseas() {
  xyz_init_tmpdir
  OV_TMP="$(xyz_tmpdir)"
  OV_RESULTS="${OV_TMP}/ov-results.tsv"
  : > "${OV_RESULTS}"
  OV_USED_IDS=" "
  xyz_step "$(xyz_t "Overseas nodes (built-in)" "境外节点(内置)")"
  print_ov_nodes
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    fetch_go noconfirm || true
    xyz_info "$(xyz_t \
      "Dry-run: official CLI (and speedtest-go) downloaded and verified; node list printed; no test was run." \
      "演练模式:已下载并校验官方 CLI(及 speedtest-go),已列出节点,未进行测速。")"
    return 0
  fi
  xyz_step "$(xyz_t "Running overseas speed tests" "正在进行境外测速")"
  if ! run_ookla_ov auto nearest "$(xyz_t "Nearest" "就近节点")"; then
    ov_skip nearest auto "$(xyz_t "Nearest" "就近节点")" ookla "${OOKLA_ERR}"
  fi
  local r
  for r in "${OV_REGIONS[@]}"; do
    test_region "${r}"
  done
  echo
  local view="${OV_TMP}/ov-view.tsv"
  local tried_word
  tried_word="$(xyz_t "tried" "个已尝试")"
  awk -F'\t' -v OFS='\t' -v w="${tried_word}" '
    { if (!($1 in seen)) { seen[$1] = 1; order[++n] = $1 }
      if ($8 == "ok") { good[$1] = 1; rows[++m] = $0; rc[m] = $1 }
      else { cnt[$1]++; if (!($1 in first)) first[$1] = $0; last[$1] = $10 } }
    END {
      for (i = 1; i <= n; i++) {
        c = order[i]
        if (good[c]) { for (j = 1; j <= m; j++) if (rc[j] == c) print rows[j] }
        else { split(first[c], f, "\t"); print c, f[2], f[3], "-", "-", "-", "-", "skip", cnt[c] " " w, last[c] }
      }
    }' "${OV_RESULTS}" > "${view}"
  if declare -F xyz_ui_speed_table >/dev/null 2>&1; then
    xyz_ui_speed_table "${view}" "$(xyz_t "Overseas speed test" "境外测速") (Mbps / ms)"
  else
    cat "${view}"
  fi
  echo
  if [[ -n "${XYZ_RESULT_FILE:-}" ]]; then
    sed 's/^/speedtest\t/' "${view}" >> "${XYZ_RESULT_FILE}" 2>/dev/null || true
  fi
  local good
  good="$(awk -F'\t' '$8 == "ok"' "${OV_RESULTS}" | wc -l | tr -d ' ')"
  if [[ "${good}" == "0" ]]; then
    xyz_error "$(xyz_t \
      "No overseas node finished a test. The network may block speed-test traffic." \
      "没有任何境外节点完成测速。当前网络可能屏蔽了测速流量。")"
    return 2
  fi
  xyz_info "$(xyz_t "Overseas speed test finished." "境外测速完成。")"
  return 0
}

if [[ "${DO_UNINSTALL}" == "1" ]]; then
  xyz_require_root
  if [[ ! -e "${DEST}" ]]; then
    xyz_error "$(xyz_t "No speedtest binary found at:" "找不到 speedtest:") ${DEST}"
    xyz_speedtest_uninstall_help
    exit 1
  fi
  xyz_info "$(xyz_t "Will remove:" "将删除:") ${DEST}"
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    xyz_info "$(xyz_t "Dry-run: not deleting the binary." "演练模式:不会删除该文件。")"
    xyz_speedtest_uninstall_help
    exit 0
  fi
  if ! xyz_confirm "$(xyz_t "Remove ${DEST}?" "删除 ${DEST}?")"; then
    exit 1
  fi
  rm -f "${DEST}"
  xyz_info "$(xyz_t "Removed." "已删除。")"
  xyz_speedtest_uninstall_help
  exit 0
fi

RUN_BIN=""
if [[ "${DO_REINSTALL}" != "1" ]] && xyz_have_official_speedtest; then
  RUN_BIN="$(command -v speedtest)"
  xyz_info "$(xyz_t "Official Speedtest CLI already installed:" "已安装官方 Speedtest CLI:") $(xyz_ookla_version_line "${RUN_BIN}")"
fi

if [[ -z "${RUN_BIN}" ]]; then
  ARCH="$(xyz_ookla_arch)"
  URL="$(xyz_ookla_url "${ARCH}")"
  xyz_info "$(xyz_t "Detected architecture:" "检测到的架构:") $(uname -m) -> ${ARCH}"
  xyz_info "$(xyz_t "Official tarball:" "官方压缩包:") ${URL}"
  xyz_info "$(xyz_t \
    "This wrapper downloads Ookla's official CLI tarball and does not add any package repository." \
    "本包装脚本只下载 Ookla 官方 CLI 压缩包,不会添加任何软件源。")"

  if [[ "${XYZ_DRY_RUN:-0}" != "1" && "${DO_NO_INSTALL}" != "1" ]]; then
    xyz_require_root
  elif [[ "${DO_KEEP}" != "1" && "${XYZ_DRY_RUN:-0}" != "1" && "${DO_INSTALL_ONLY}" == "1" ]]; then
    xyz_error "$(xyz_t \
      "--install-only needs a destination. Omit --no-install (installs to ${DEST}) or add --keep." \
      "--install-only 需要安装位置。请去掉 --no-install(安装到 ${DEST}),或加上 --keep。")"
    exit 1
  fi
  xyz_require_supported_os

  if ! xyz_need_cmd tar; then
    xyz_require_root
    xyz_ensure_cmds tar
  fi

  xyz_init_tmpdir
  tarball="$(xyz_tmpdir)/ookla-speedtest-${OOKLA_CLI_VERSION}-linux-${ARCH}.tgz"
  xyz_fetch "${URL}" "${tarball}"

  expected=""
  if [[ "${OOKLA_CLI_VERSION}" == "1.2.0" ]]; then
    expected="$(xyz_ookla_tarball_sha256 "${ARCH}")"
  fi
  if [[ -n "${expected}" ]] && xyz_need_cmd sha256sum; then
    actual="$(sha256sum "${tarball}" | awk '{print $1}')"
    if [[ "${actual}" != "${expected}" ]]; then
      xyz_error "$(xyz_t "SHA256 mismatch for the official tarball." "官方压缩包的 SHA256 不匹配。")"
      xyz_error "  expected: ${expected}"
      xyz_error "  actual:   ${actual}"
      exit 1
    fi
    xyz_info "$(xyz_t "SHA256 verified." "SHA256 已校验。")"
  fi

  extract="$(xyz_tmpdir)/extract"
  mkdir -p "${extract}"
  tar -xzf "${tarball}" -C "${extract}"
  if [[ ! -x "${extract}/speedtest" && -f "${extract}/speedtest" ]]; then
    chmod +x "${extract}/speedtest"
  fi
  if [[ ! -x "${extract}/speedtest" ]]; then
    xyz_error "$(xyz_t "The tarball did not contain an executable 'speedtest' binary." "压缩包中没有可执行的 speedtest 文件。")"
    tar -tzf "${tarball}" || true
    exit 1
  fi
  xyz_info "$(xyz_t "Tarball contents:" "压缩包内容:") $(tar -tzf "${tarball}" | tr '\n' ' ')"
  xyz_info "$(xyz_t "Unpacked binary:" "解压后的二进制:") $(xyz_ookla_version_line "${extract}/speedtest")"

  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    RUN_BIN="${extract}/speedtest"
    if [[ "${DO_OVERSEAS}" != "1" ]]; then
      xyz_info "$(xyz_t \
        "Dry-run: tarball downloaded and verified; not installing to ${DEST} and not running a test." \
        "演练模式:已下载并校验压缩包,不会安装到 ${DEST},也不会运行测速。")"
      xyz_speedtest_uninstall_help
      exit 0
    fi
  fi

  if [[ "${XYZ_DRY_RUN:-0}" != "1" && "${DO_NO_INSTALL}" != "1" ]]; then
    if ! xyz_confirm "$(xyz_t \
      "Install the official Speedtest CLI to ${DEST}?" \
      "将官方 Speedtest CLI 安装到 ${DEST}?")"; then
      xyz_error "$(xyz_t "Aborted by user." "已取消。")"
      exit 1
    fi
    mkdir -p "${INSTALL_PREFIX}"
    if xyz_need_cmd install; then
      install -m 0755 "${extract}/speedtest" "${DEST}"
    else
      cp "${extract}/speedtest" "${DEST}"
      chmod 0755 "${DEST}"
    fi
    xyz_info "$(xyz_t "Installed:" "已安装:") ${DEST}"
    RUN_BIN="${DEST}"
    export PATH="${INSTALL_PREFIX}:${PATH}"
  fi

  if [[ "${XYZ_DRY_RUN:-0}" != "1" && "${DO_KEEP}" == "1" ]]; then
    keep_path="${PWD}/speedtest"
    cp "${extract}/speedtest" "${keep_path}"
    chmod 0755 "${keep_path}"
    xyz_info "$(xyz_t "Kept a copy at:" "已另存副本:") ${keep_path}"
    if [[ -z "${RUN_BIN}" ]]; then
      RUN_BIN="${keep_path}"
    fi
  fi

  if [[ -z "${RUN_BIN}" ]]; then
    # Run once from the temp dir (deleted when the script exits).
    RUN_BIN="${extract}/speedtest"
    xyz_info "$(xyz_t \
      "Running from the temporary copy for this invocation only. Re-run without --no-install, or pass --keep, to keep the binary." \
      "本次仅从临时副本运行。若要保留二进制,请去掉 --no-install,或加上 --keep。")"
  fi
fi

if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
  if [[ "${DO_OVERSEAS}" == "1" ]]; then
    xyz_run_overseas
    exit $?
  fi
  xyz_info "$(xyz_t "Dry-run: not running a test." "演练模式:不运行测速。")"
  exit 0
fi

if [[ -z "${RUN_BIN}" || ! -x "${RUN_BIN}" ]]; then
  xyz_error "$(xyz_t "The speedtest command is still not available." "仍然找不到 speedtest 命令。")"
  exit 1
fi

xyz_info "$(xyz_t "Installed/found:" "已找到:") $(xyz_ookla_version_line "${RUN_BIN}")"
xyz_speedtest_uninstall_help

if [[ "${DO_INSTALL_ONLY}" == "1" ]]; then
  xyz_info "$(xyz_t "Install-only requested; not running a test." "仅安装,不运行测速。")"
  exit 0
fi

xyz_info "$(xyz_t \
  "The next step runs Ookla's official CLI. It will contact Ookla servers to measure latency, download, and upload. Ookla has its own license and privacy terms:" \
  "接下来将运行 Ookla 官方 CLI。它会连接 Ookla 服务器测量延迟、下载和上传。Ookla 有自己的许可与隐私条款:")"
printf '  https://www.speedtest.net/about/privacy\n'
printf '  https://www.speedtest.net/terms\n'

# Only pass accept flags after the user has seen the notice and confirmed.
# -y / YES=1 still prints this notice first, then auto-confirms.
ACCEPT_FLAGS=()
if xyz_confirm "$(xyz_t \
  "Run 'speedtest --accept-license --accept-gdpr' now?" \
  "现在运行 speedtest --accept-license --accept-gdpr?")"; then
  ACCEPT_FLAGS+=(--accept-license --accept-gdpr)
else
  xyz_error "$(xyz_t "Aborted. You can run 'speedtest' yourself later." "已取消。你可以稍后自行运行 speedtest。")"
  exit 1
fi

if [[ "${DO_OVERSEAS}" == "1" ]]; then
  if [[ "${XYZ_DRY_RUN:-0}" == "1" && -z "${RUN_BIN:-}" ]]; then
    RUN_BIN="speedtest"
  fi
  xyz_run_overseas
  exit $?
fi

xyz_step "$(xyz_t "Running Speedtest" "正在测速")"
"${RUN_BIN}" "${ACCEPT_FLAGS[@]}"

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.

Suggested for:United States

Placeholder · US West

Vendor TBD

Region, specs, and price will be filled in after onboarding. This is not a checkout link.

Placeholder · Value

Vendor TBD

Region, specs, and price will be filled in after onboarding. This is not a checkout link.

Placeholder · High RAM / storage

Vendor TBD

Region, specs, and price will be filled in after onboarding. This is not a checkout link.