Files
epusdt/epctl
T

1468 lines
45 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
readonly EPCTL_NAME="epctl"
readonly REPO_OWNER="GMWalletApp"
readonly REPO_NAME="epusdt"
readonly SERVICE_NAME="epusdt"
readonly SYSTEM_USER="epusdt"
readonly INSTALL_DIR="/opt/epusdt"
readonly BIN_PATH="${INSTALL_DIR}/epusdt"
readonly CONFIG_PATH="${INSTALL_DIR}/.env"
readonly CONFIG_EXAMPLE_PATH="${INSTALL_DIR}/.env.example"
readonly WWW_PATH="${INSTALL_DIR}/www"
readonly CACHE_ROOT="/tmp/epusdt"
readonly SYSTEMD_UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service"
readonly SELF_INSTALL_PATH="/usr/local/bin/${EPCTL_NAME}"
readonly DEFAULT_APP_URI="http://127.0.0.1:8000"
readonly DEFAULT_LISTEN="127.0.0.1:8000"
readonly SCRIPT_SOURCE_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
COLOR_RESET=""
COLOR_BOLD=""
COLOR_DIM=""
COLOR_CYAN=""
COLOR_GREEN=""
COLOR_YELLOW=""
COLOR_RED=""
COLOR_BLUE=""
ACTIVE_LANG=""
POSITIONAL_ARGS=()
detect_default_language() {
local raw normalized
raw="${EPCTL_LANG:-}"
if [[ -z "${raw}" ]]; then
printf 'zh\n'
return
fi
normalized="${raw,,}"
case "${normalized}" in
zh|zh-*|zh_*|cn|chs|cht|中文)
printf 'zh\n'
;;
en|en-*|en_*|english)
printf 'en\n'
;;
*)
printf '%s\n' "${raw}"
;;
esac
}
set_language() {
local requested normalized fallback_lang
requested="${1:-$(detect_default_language)}"
normalized="${requested,,}"
fallback_lang="${ACTIVE_LANG:-$(detect_default_language)}"
case "${normalized}" in
zh|zh-*|zh_*|cn|chs|cht|中文)
ACTIVE_LANG="zh"
;;
en|en-*|en_*|english)
ACTIVE_LANG="en"
;;
*)
ACTIVE_LANG="${fallback_lang}"
die "$(trf unsupported_language "${requested}")"
;;
esac
export EPCTL_LANG="${ACTIVE_LANG}"
}
trf() {
local key="$1"
shift || true
local template
case "${ACTIVE_LANG}:${key}" in
en:label_warn) template='warn' ;;
zh:label_warn) template='警告' ;;
en:label_ok) template='ok' ;;
zh:label_ok) template='完成' ;;
en:label_error) template='error' ;;
zh:label_error) template='错误' ;;
en:only_linux) template='this script only supports Linux' ;;
zh:only_linux) template='本脚本仅支持 Linux' ;;
en:missing_dependency) template='missing dependency: %s' ;;
zh:missing_dependency) template='缺少依赖:%s' ;;
en:root_required) template='this command must be run as root' ;;
zh:root_required) template='此命令必须以 root 身份执行' ;;
en:release_tag_required) template='release tag is required' ;;
zh:release_tag_required) template='必须提供 release tag' ;;
en:release_tag_invalid) template='--tag must match a release tag such as v1.0.6' ;;
zh:release_tag_invalid) template='--tag 必须是有效的 release tag,例如 v1.0.6' ;;
en:unsupported_arch) template='unsupported architecture: %s' ;;
zh:unsupported_arch) template='不支持的架构:%s' ;;
en:checksum_missing) template='checksum entry for %s not found in %s' ;;
zh:checksum_missing) template='在 %s 中找不到 %s 的校验项' ;;
en:latest_release_resolve_failed) template='failed to resolve the latest release tag from GitHub' ;;
zh:latest_release_resolve_failed) template='无法从 GitHub 解析最新 release tag' ;;
en:using_latest_without_confirmation) template='using latest release tag %s without confirmation because EPCTL_ASSUME_YES=1' ;;
zh:using_latest_without_confirmation) template='检测到 EPCTL_ASSUME_YES=1,直接使用最新 release tag %s,不再二次确认' ;;
en:latest_tag_no_tty) template='no --tag provided and no interactive terminal available; pass --tag explicitly or set EPCTL_ASSUME_YES=1' ;;
zh:latest_tag_no_tty) template='未提供 --tag,且当前不是交互终端;请显式传入 --tag,或设置 EPCTL_ASSUME_YES=1' ;;
en:latest_prompt) template='latest release tag is %s. Continue? [y/N] ' ;;
zh:latest_prompt) template='检测到最新 release tag 为 %s,是否继续?[y/N] ' ;;
en:cancelled) template='cancelled' ;;
zh:cancelled) template='已取消' ;;
en:cached_release_files) template='using cached release files in %s' ;;
zh:cached_release_files) template='使用缓存中的 release 文件:%s' ;;
en:downloading_archive) template='downloading %s' ;;
zh:downloading_archive) template='正在下载 %s' ;;
en:checksum_skipped) template='sha256sum not found; skipped checksum verification' ;;
zh:checksum_skipped) template='未检测到 sha256sum,已跳过校验' ;;
en:archive_missing_binary) template='release archive is missing the epusdt binary' ;;
zh:archive_missing_binary) template='release 压缩包中缺少 epusdt 二进制文件' ;;
en:archive_missing_env_example) template='release archive is missing .env.example' ;;
zh:archive_missing_env_example) template='release 压缩包中缺少 .env.example' ;;
en:keeping_existing_config) template='keeping existing config at %s' ;;
zh:keeping_existing_config) template='保留现有配置文件:%s' ;;
en:service_failed_active) template='service %s failed to become active' ;;
zh:service_failed_active) template='服务 %s 未能成功进入 active 状态' ;;
en:service_installed) template='installed %s to %s from %s' ;;
zh:service_installed) template='已将 %s 安装到 %s,来源版本:%s' ;;
en:self_install_current) template='%s already points to the current script' ;;
zh:self_install_current) template='%s 已经指向当前脚本,无需重复安装' ;;
en:self_install_done) template='installed %s into /usr/local/bin' ;;
zh:self_install_done) template='已将 %s 安装到 /usr/local/bin' ;;
en:config_not_found) template='config file not found: %s' ;;
zh:config_not_found) template='找不到配置文件:%s' ;;
en:http_listen_not_found) template='http_listen not found in %s' ;;
zh:http_listen_not_found) template='在 %s 中找不到 http_listen' ;;
en:http_listen_parse_failed) template='failed to parse the port from http_listen=%s' ;;
zh:http_listen_parse_failed) template='无法从 http_listen=%s 中解析端口' ;;
en:curl_partial_body) template='curl failed and returned a partial response body:' ;;
zh:curl_partial_body) template='curl 请求失败,并返回了部分响应体:' ;;
en:init_password_http_failed) template='init-password request failed with HTTP %s: %s' ;;
zh:init_password_http_failed) template='init-password 请求失败,HTTP %s%s' ;;
en:response_body_label) template='response body:' ;;
zh:response_body_label) template='响应体:' ;;
en:response_body_empty) template='response body is empty' ;;
zh:response_body_empty) template='响应体为空' ;;
en:lines_positive_integer) template='--lines must be a positive integer' ;;
zh:lines_positive_integer) template='--lines 必须是正整数' ;;
en:state_not_installed) template='not installed' ;;
zh:state_not_installed) template='未安装' ;;
en:state_no_systemctl) template='unit exists, but systemctl is unavailable' ;;
zh:state_no_systemctl) template='unit 文件已存在,但 systemctl 不可用' ;;
en:state_no_systemd) template='unit exists, but systemd is unavailable' ;;
zh:state_no_systemd) template='unit 文件已存在,但 systemd 不可用' ;;
en:state_active) template='active' ;;
zh:state_active) template='运行中' ;;
en:state_inactive) template='installed but inactive' ;;
zh:state_inactive) template='已安装,但未运行' ;;
en:menu_title) template='EPCTL Console' ;;
zh:menu_title) template='EPCTL 控制台' ;;
en:menu_subtitle) template='Linux binary manager for epusdt' ;;
zh:menu_subtitle) template='面向 epusdt 的 Linux 二进制管理工具' ;;
en:label_service) template='service' ;;
zh:label_service) template='服务状态' ;;
en:label_install_dir) template='install dir' ;;
zh:label_install_dir) template='安装目录' ;;
en:label_config) template='config' ;;
zh:label_config) template='配置文件' ;;
en:menu_section_setup) template='Setup and release management' ;;
zh:menu_section_setup) template='部署与发布' ;;
en:menu_section_runtime) template='Runtime inspection' ;;
zh:menu_section_runtime) template='运行查看' ;;
en:menu_section_tools) template='Language and help' ;;
zh:menu_section_tools) template='语言与帮助' ;;
en:menu_badge_direct) template='direct' ;;
zh:menu_badge_direct) template='直达' ;;
en:menu_badge_confirm) template='confirm' ;;
zh:menu_badge_confirm) template='确认' ;;
en:menu_exit) template='Quit' ;;
zh:menu_exit) template='退出' ;;
en:menu_hint_direct) template='Direct items run immediately after selection.' ;;
zh:menu_hint_direct) template='带 [直达] 的项目会在选择后直接执行。' ;;
en:menu_hint_confirm) template='Confirm items show an extra confirmation step first.' ;;
zh:menu_hint_confirm) template='带 [确认] 的项目会先显示确认步骤。' ;;
en:command_completed) template='command completed' ;;
zh:command_completed) template='命令执行完成' ;;
en:command_failed_exit) template='command failed with exit code %s' ;;
zh:command_failed_exit) template='命令执行失败,退出码:%s' ;;
en:invalid_selection) template='invalid selection: %s' ;;
zh:invalid_selection) template='无效选择:%s' ;;
en:arg_requires_value) template='%s requires a value' ;;
zh:arg_requires_value) template='%s 需要一个参数值' ;;
en:unknown_arg) template='unknown argument for %s: %s' ;;
zh:unknown_arg) template='%s 命令不支持该参数:%s' ;;
en:unknown_command) template='unknown command: %s' ;;
zh:unknown_command) template='未知命令:%s' ;;
en:input_release_tag) template='Release tag (blank = latest)' ;;
zh:input_release_tag) template='Release tag(留空则使用 latest' ;;
en:input_upgrade_tag) template='Upgrade tag (blank = latest)' ;;
zh:input_upgrade_tag) template='升级目标 tag(留空则使用 latest' ;;
en:input_app_uri) template='app_uri' ;;
zh:input_app_uri) template='app_uri' ;;
en:input_http_listen) template='http_listen' ;;
zh:input_http_listen) template='http_listen' ;;
en:input_log_lines) template='Log lines' ;;
zh:input_log_lines) template='日志行数' ;;
en:press_enter_continue) template='Press Enter to return to the menu...' ;;
zh:press_enter_continue) template='按回车返回菜单...' ;;
en:menu_select_action) template='Select an action [0-10]: ' ;;
zh:menu_select_action) template='请选择操作 [0-10]' ;;
en:menu_language_prompt) template='Choose a language / 请选择语言: 1) 中文 2) English' ;;
zh:menu_language_prompt) template='请选择语言 / Choose a language1)中文 2English' ;;
en:menu_language_input) template='Language selection(语言选择)' ;;
zh:menu_language_input) template='语言选择(Language selection' ;;
en:menu_confirm_prompt) template='Continue? / 是否继续? [y/N] ' ;;
zh:menu_confirm_prompt) template='是否继续?/ Continue? [y/N] ' ;;
en:menu_cancelled_back) template='Cancelled and returned to the menu / 已取消,返回菜单' ;;
zh:menu_cancelled_back) template='已取消,返回菜单 / Cancelled and returned to the menu' ;;
en:language_switched) template='Language switched to %s / 语言已切换为 %s' ;;
zh:language_switched) template='语言已切换为 %s / Language switched to %s' ;;
en:language_name_zh) template='中文' ;;
zh:language_name_zh) template='中文' ;;
en:language_name_en) template='English' ;;
zh:language_name_en) template='English' ;;
en:unsupported_language) template='unsupported language: %s' ;;
zh:unsupported_language) template='不支持的语言:%s' ;;
en:current_language) template='current language' ;;
zh:current_language) template='当前语言' ;;
en:base_image_label) template='base image' ;;
zh:base_image_label) template='基础镜像' ;;
en:action_download_title) template='Download release package' ;;
zh:action_download_title) template='下载 release 包' ;;
en:action_download_desc) template='This will download the selected GitHub release archive into %s, and verify SHA256 when sha256sum is available. It will not install or start the service.' ;;
zh:action_download_desc) template='此操作会把选定的 GitHub release 压缩包下载到 %s,并在本机有 sha256sum 时校验 SHA256。它不会安装,也不会启动服务。' ;;
en:action_install_title) template='Install or refresh the service' ;;
zh:action_install_title) template='安装或刷新服务' ;;
en:action_install_desc) template='This will install epusdt into %s, create or reuse %s, write %s, and enable %s through systemd. Existing .env will be kept.' ;;
zh:action_install_desc) template='此操作会把 epusdt 安装到 %s,创建或复用 %s,写入 %s,并通过 systemd 启用 %s。若已有 .env,则会保留。' ;;
en:action_upgrade_title) template='Upgrade the service' ;;
zh:action_upgrade_title) template='升级服务' ;;
en:action_upgrade_desc) template='This will download and install a newer release into %s, then restart %s. The existing .env configuration will be preserved.' ;;
zh:action_upgrade_desc) template='此操作会下载并安装较新的 release 到 %s,然后重启 %s。现有 .env 配置会被保留。' ;;
en:action_self_install_title) template='Install epctl into PATH' ;;
zh:action_self_install_title) template='安装 epctl 到 PATH' ;;
en:action_self_install_desc) template='This will install %s into /usr/local/bin so it can be called from anywhere. Use %s to force Chinese or %s to force English.' ;;
zh:action_self_install_desc) template='此操作会把 %s 安装到 /usr/local/bin,便于在任意目录直接调用。之后可用 %s 强制中文,或用 %s 强制英文。' ;;
en:action_show_config_title) template='Show active config' ;;
zh:action_show_config_title) template='查看当前配置' ;;
en:action_show_config_desc) template='This will print the full active configuration file %s to the terminal. Sensitive values will be visible.' ;;
zh:action_show_config_desc) template='此操作会把当前生效的配置文件 %s 完整输出到终端,其中敏感配置也会直接显示。' ;;
en:action_init_password_title) template='Request initial admin password' ;;
zh:action_init_password_title) template='获取初始化密码' ;;
en:action_init_password_desc) template='This will call the local HTTP route /admin/api/v1/auth/init-password based on http_listen in %s. It does not read the database directly.' ;;
zh:action_init_password_desc) template='此操作会根据 %s 中的 http_listen,请求本地 HTTP 路由 /admin/api/v1/auth/init-password。它不会直接读取数据库。' ;;
en:action_status_title) template='Show service status' ;;
zh:action_status_title) template='查看服务状态' ;;
en:action_status_desc) template='This will run systemctl status for %s and print the current runtime state, recent logs, and unit summary.' ;;
zh:action_status_desc) template='此操作会对 %s 执行 systemctl status,并输出当前运行状态、最近日志和 unit 摘要。' ;;
en:action_logs_title) template='Show recent logs' ;;
zh:action_logs_title) template='查看最近日志' ;;
en:action_logs_desc) template='This will read recent journal logs for %s. You can choose how many lines to print next.' ;;
zh:action_logs_desc) template='此操作会读取 %s 的最近 journal 日志。下一步你可以选择要输出多少行。' ;;
en:action_language_title) template='Switch language(切换语言)' ;;
zh:action_language_title) template='Switch language(切换语言)' ;;
en:action_language_desc) template='This only changes the current interactive interface language. It does not change the service config or installation files. / 此操作只会切换当前交互界面的语言,不会修改服务配置,也不会改动安装文件。' ;;
zh:action_language_desc) template='此操作只会切换当前交互界面的语言,不会修改服务配置,也不会改动安装文件。 / This only changes the current interactive interface language. It does not change the service config or installation files.' ;;
en:action_help_title) template='Show help' ;;
zh:action_help_title) template='查看帮助' ;;
en:action_help_desc) template='This will print the full command help, including all non-interactive commands and global options.' ;;
zh:action_help_desc) template='此操作会输出完整命令帮助,包括所有非交互命令和全局参数。' ;;
*)
template="missing translation: ${key}"
;;
esac
printf -- "${template}" "$@"
}
init_colors() {
if [[ -n "${NO_COLOR:-}" || "${EPCTL_NO_COLOR:-}" == "1" ]]; then
return
fi
if [[ -t 1 || -t 2 ]]; then
COLOR_RESET=$'\033[0m'
COLOR_BOLD=$'\033[1m'
COLOR_DIM=$'\033[2m'
COLOR_CYAN=$'\033[36m'
COLOR_GREEN=$'\033[32m'
COLOR_YELLOW=$'\033[33m'
COLOR_RED=$'\033[31m'
COLOR_BLUE=$'\033[34m'
fi
}
log() {
printf '%b[%s]%b %s\n' "${COLOR_CYAN}${COLOR_BOLD}" "${EPCTL_NAME}" "${COLOR_RESET}" "$*" >&2
}
warn() {
printf '%b[%s] %s:%b %s\n' "${COLOR_YELLOW}${COLOR_BOLD}" "${EPCTL_NAME}" "$(trf label_warn)" "${COLOR_RESET}" "$*" >&2
}
success() {
printf '%b[%s] %s:%b %s\n' "${COLOR_GREEN}${COLOR_BOLD}" "${EPCTL_NAME}" "$(trf label_ok)" "${COLOR_RESET}" "$*" >&2
}
die() {
printf '%b[%s] %s:%b %s\n' "${COLOR_RED}${COLOR_BOLD}" "${EPCTL_NAME}" "$(trf label_error)" "${COLOR_RESET}" "$*" >&2
exit 1
}
usage() {
if [[ "${ACTIVE_LANG}" == "zh" ]]; then
cat <<'EOF'
用法:
epctl [--lang zh|en] [--no-color]
epctl zh
epctl en
epctl [--lang zh|en] menu
epctl [--lang zh|en] download [--tag <tag>]
epctl [--lang zh|en] install [--tag <tag>] [--app-uri URL] [--listen ADDR:PORT]
epctl [--lang zh|en] upgrade [--tag <tag>]
epctl [--lang zh|en] self-install
epctl [--lang zh|en] show-config
epctl [--lang zh|en] init-password
epctl [--lang zh|en] status
epctl [--lang zh|en] logs [--lines N]
全局选项:
--lang zh|en 切换界面语言
--no-color 禁用彩色输出
快捷方式 `epctl zh` 直接进入中文;`epctl en` 直接进入英文
命令说明:
menu 进入交互式菜单;直接执行 `epctl` 也会进入
download 下载 GitHub Release 包到 /tmp/epusdt/<tag>/
install 安装或刷新 /opt/epusdt,并注册 epusdt.service
upgrade 升级到新的 release,保留现有 .env
self-install 安装 epctl 到 /usr/local/bin
show-config 输出当前生效的 /opt/epusdt/.env
init-password 通过本地 HTTP 路由请求初始化管理员密码
status 显示 epusdt 的 systemd 状态
logs 显示最近的服务日志
说明:
- 仅支持 Linux
- 仅支持二进制安装
- install / upgrade / self-install 会写入 /opt、/etc/systemd、/usr/local/bin
- 需要提权的命令会自动通过 sudo 重新执行
- tag 需要是真实的 GitHub release tag,例如 v1.0.6
- 未提供 --tag 时,会自动解析 latest,并要求二次确认
EOF
else
cat <<'EOF'
Usage:
epctl [--lang zh|en] [--no-color]
epctl zh
epctl en
epctl [--lang zh|en] menu
epctl [--lang zh|en] download [--tag <tag>]
epctl [--lang zh|en] install [--tag <tag>] [--app-uri URL] [--listen ADDR:PORT]
epctl [--lang zh|en] upgrade [--tag <tag>]
epctl [--lang zh|en] self-install
epctl [--lang zh|en] show-config
epctl [--lang zh|en] init-password
epctl [--lang zh|en] status
epctl [--lang zh|en] logs [--lines N]
Global options:
--lang zh|en Select the interface language
--no-color Disable colored output
Shortcut `epctl zh` enters Chinese directly; `epctl en` enters English directly
Command summary:
menu Open the interactive menu; `epctl` with no arguments does this too
download Download a GitHub release archive into /tmp/epusdt/<tag>/
install Install or refresh /opt/epusdt and register epusdt.service
upgrade Upgrade to a newer release while keeping the existing .env
self-install Install epctl into /usr/local/bin
show-config Print the active /opt/epusdt/.env file
init-password Request the initial admin password from the local HTTP route
status Show the systemd status of epusdt
logs Show recent service logs
Notes:
- Linux only
- Binary installation only
- install / upgrade / self-install write into /opt, /etc/systemd, and /usr/local/bin
- privileged commands automatically re-exec through sudo
- tags must match real GitHub release tags such as v1.0.6
- if --tag is omitted, epctl resolves the latest release and asks for confirmation
EOF
fi
}
parse_global_options() {
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--lang)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "--lang")"
set_language "$2"
shift 2
;;
--lang=*)
set_language "${1#*=}"
shift
;;
--no-color)
export EPCTL_NO_COLOR=1
shift
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
}
require_linux() {
[[ "$(uname -s)" == "Linux" ]] || die "$(trf only_linux)"
}
have_command() {
command -v "$1" >/dev/null 2>&1
}
require_command() {
have_command "$1" || die "$(trf missing_dependency "$1")"
}
is_interactive_terminal() {
[[ -t 0 && -t 1 ]]
}
require_root() {
[[ "${EUID}" -eq 0 ]] || die "$(trf root_required)"
}
ensure_root_via_sudo() {
if [[ "${EUID}" -eq 0 ]]; then
return
fi
require_command sudo
exec sudo -- "${SCRIPT_SOURCE_PATH}" --lang "${ACTIVE_LANG}" "$@"
}
require_release_tag() {
local tag="$1"
[[ -n "${tag}" ]] || die "$(trf release_tag_required)"
[[ "${tag}" == v* ]] || die "$(trf release_tag_invalid)"
}
map_arch() {
case "$(uname -m)" in
x86_64|amd64)
printf 'amd64\n'
;;
aarch64|arm64)
printf 'arm64\n'
;;
*)
die "$(trf unsupported_arch "$(uname -m)")"
;;
esac
}
asset_version_from_tag() {
local tag="$1"
printf '%s\n' "${tag#v}"
}
cache_dir_for_tag() {
local tag="$1"
printf '%s/%s\n' "${CACHE_ROOT}" "${tag}"
}
archive_name_for_tag() {
local tag="$1"
local arch="$2"
printf 'epusdt-%s-linux-%s.tar.gz\n' "$(asset_version_from_tag "${tag}")" "${arch}"
}
release_base_url_for_tag() {
local tag="$1"
printf 'https://github.com/%s/%s/releases/download/%s\n' "${REPO_OWNER}" "${REPO_NAME}" "${tag}"
}
download_file() {
local url="$1"
local destination="$2"
local partial="${destination}.part"
rm -f "${partial}"
curl --fail --location --silent --show-error --output "${partial}" "${url}"
mv "${partial}" "${destination}"
}
verify_checksum() {
local sums_path="$1"
local archive_path="$2"
local archive_name
local checksum_line
archive_name="$(basename "${archive_path}")"
checksum_line="$(grep -F " ${archive_name}" "${sums_path}" || true)"
[[ -n "${checksum_line}" ]] || die "$(trf checksum_missing "${archive_name}" "$(basename "${sums_path}")")"
(
cd "$(dirname "${archive_path}")"
printf '%s\n' "${checksum_line}" | sha256sum -c - >&2
)
}
ensure_download_dependencies() {
require_command curl
require_command grep
require_command sed
}
latest_release_tag() {
local api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest"
local response tag
ensure_download_dependencies
response="$(curl --fail --location --silent --show-error "${api_url}")"
tag="$(printf '%s\n' "${response}" | sed -n 's/^[[:space:]]*"tag_name":[[:space:]]*"\([^"]\+\)".*$/\1/p' | head -n 1)"
[[ -n "${tag}" ]] || die "$(trf latest_release_resolve_failed)"
require_release_tag "${tag}"
printf '%s\n' "${tag}"
}
is_affirmative_reply() {
local reply="${1:-}"
reply="${reply,,}"
case "${reply}" in
y|yes|ok|true|1|s|shi||确认|確認)
return 0
;;
*)
return 1
;;
esac
}
confirm_latest_release_tag() {
local tag="$1"
local reply
if [[ "${EPCTL_ASSUME_YES:-}" == "1" ]]; then
log "$(trf using_latest_without_confirmation "${tag}")"
printf '%s\n' "${tag}"
return
fi
if [[ ! -t 0 ]]; then
die "$(trf latest_tag_no_tty)"
fi
printf '%b[%s]%b %s' "${COLOR_CYAN}${COLOR_BOLD}" "${EPCTL_NAME}" "${COLOR_RESET}" "$(trf latest_prompt "${tag}")" >&2
read -r reply
if is_affirmative_reply "${reply}"; then
printf '%s\n' "${tag}"
return
fi
die "$(trf cancelled)"
}
resolve_effective_tag() {
local requested_tag="$1"
local latest_tag
if [[ -n "${requested_tag}" ]]; then
require_release_tag "${requested_tag}"
printf '%s\n' "${requested_tag}"
return
fi
latest_tag="$(latest_release_tag)"
confirm_latest_release_tag "${latest_tag}"
}
download_release() {
local tag="$1"
local arch archive_name release_base_url cache_dir archive_path sums_path
require_linux
ensure_download_dependencies
arch="$(map_arch)"
archive_name="$(archive_name_for_tag "${tag}" "${arch}")"
release_base_url="$(release_base_url_for_tag "${tag}")"
cache_dir="$(cache_dir_for_tag "${tag}")"
archive_path="${cache_dir}/${archive_name}"
sums_path="${cache_dir}/SHA256SUMS"
mkdir -p "${cache_dir}"
if [[ -f "${archive_path}" && -f "${sums_path}" ]]; then
log "$(trf cached_release_files "${cache_dir}")"
else
log "$(trf downloading_archive "${archive_name}")"
download_file "${release_base_url}/${archive_name}" "${archive_path}"
download_file "${release_base_url}/SHA256SUMS" "${sums_path}"
fi
if have_command sha256sum; then
verify_checksum "${sums_path}" "${archive_path}"
else
warn "$(trf checksum_skipped)"
fi
printf '%s\n' "${archive_path}"
}
escape_sed_replacement() {
printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'
}
set_env_key() {
local file="$1"
local key="$2"
local value="$3"
local escaped
escaped="$(escape_sed_replacement "${value}")"
if grep -q "^${key}=" "${file}"; then
sed -i "s|^${key}=.*$|${key}=${escaped}|" "${file}"
else
printf '%s=%s\n' "${key}" "${value}" >> "${file}"
fi
}
ensure_system_user() {
if ! getent group "${SYSTEM_USER}" >/dev/null 2>&1; then
groupadd --system "${SYSTEM_USER}"
fi
if ! id -u "${SYSTEM_USER}" >/dev/null 2>&1; then
useradd --system --gid "${SYSTEM_USER}" --home-dir "${INSTALL_DIR}" --shell /usr/sbin/nologin "${SYSTEM_USER}"
fi
}
extract_release_archive() {
local tag="$1"
local archive_path="$2"
local extract_dir
extract_dir="$(cache_dir_for_tag "${tag}")/extract"
rm -rf "${extract_dir}"
mkdir -p "${extract_dir}"
tar -xzf "${archive_path}" -C "${extract_dir}"
[[ -f "${extract_dir}/epusdt" ]] || die "$(trf archive_missing_binary)"
[[ -f "${extract_dir}/.env.example" ]] || die "$(trf archive_missing_env_example)"
printf '%s\n' "${extract_dir}"
}
create_default_config_if_missing() {
local app_uri="$1"
local listen="$2"
if [[ -f "${CONFIG_PATH}" ]]; then
log "$(trf keeping_existing_config "${CONFIG_PATH}")"
return
fi
cp "${CONFIG_EXAMPLE_PATH}" "${CONFIG_PATH}"
set_env_key "${CONFIG_PATH}" "install" "false"
set_env_key "${CONFIG_PATH}" "app_uri" "${app_uri}"
set_env_key "${CONFIG_PATH}" "http_listen" "${listen}"
chmod 0644 "${CONFIG_PATH}"
}
write_systemd_unit() {
cat > "${SYSTEMD_UNIT_PATH}" <<EOF
[Unit]
Description=epusdt service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${SYSTEM_USER}
Group=${SYSTEM_USER}
WorkingDirectory=${INSTALL_DIR}
ExecStart=${BIN_PATH} http start
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
}
render_service_debug_info() {
systemctl status "${SERVICE_NAME}" --no-pager || true
journalctl -u "${SERVICE_NAME}" --no-pager -n 100 || true
}
command_download() {
local tag=""
local effective_tag archive_path
while [[ $# -gt 0 ]]; do
case "$1" in
--tag)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "--tag")"
tag="$2"
shift 2
;;
--tag=*)
tag="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "download" "$1")"
;;
esac
done
effective_tag="$(resolve_effective_tag "${tag}")"
archive_path="$(download_release "${effective_tag}")"
printf '%s\n' "${archive_path}"
}
command_install() {
local tag=""
local app_uri="${DEFAULT_APP_URI}"
local listen="${DEFAULT_LISTEN}"
local original_args=("$@")
local effective_tag archive_path extract_dir
while [[ $# -gt 0 ]]; do
case "$1" in
--tag)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "--tag")"
tag="$2"
shift 2
;;
--tag=*)
tag="${1#*=}"
shift
;;
--app-uri)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "--app-uri")"
app_uri="$2"
shift 2
;;
--app-uri=*)
app_uri="${1#*=}"
shift
;;
--listen)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "--listen")"
listen="$2"
shift 2
;;
--listen=*)
listen="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "install" "$1")"
;;
esac
done
ensure_root_via_sudo install "${original_args[@]}"
require_root
require_linux
require_command tar
require_command install
require_command systemctl
require_command grep
require_command sed
require_command getent
require_command groupadd
require_command useradd
require_command chown
effective_tag="$(resolve_effective_tag "${tag}")"
archive_path="$(download_release "${effective_tag}")"
extract_dir="$(extract_release_archive "${effective_tag}" "${archive_path}")"
ensure_system_user
install -d -m 0755 "${INSTALL_DIR}"
install -m 0755 "${extract_dir}/epusdt" "${BIN_PATH}"
install -m 0644 "${extract_dir}/.env.example" "${CONFIG_EXAMPLE_PATH}"
create_default_config_if_missing "${app_uri}" "${listen}"
chown -R "${SYSTEM_USER}:${SYSTEM_USER}" "${INSTALL_DIR}"
write_systemd_unit
systemctl daemon-reload
systemctl enable --now "${SERVICE_NAME}"
if ! systemctl is-active --quiet "${SERVICE_NAME}"; then
render_service_debug_info
die "$(trf service_failed_active "${SERVICE_NAME}")"
fi
log "$(trf service_installed "${SERVICE_NAME}" "${INSTALL_DIR}" "${effective_tag}")"
}
command_self_install() {
local original_args=("$@")
require_linux
require_command install
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "self-install" "$1")"
;;
esac
done
ensure_root_via_sudo self-install "${original_args[@]}"
if [[ "${SCRIPT_SOURCE_PATH}" == "${SELF_INSTALL_PATH}" ]]; then
chmod 0755 "${SELF_INSTALL_PATH}"
log "$(trf self_install_current "${SELF_INSTALL_PATH}")"
else
install -m 0755 "${SCRIPT_SOURCE_PATH}" "${SELF_INSTALL_PATH}"
fi
log "$(trf self_install_done "${EPCTL_NAME}")"
}
command_upgrade() {
local original_args=("$@")
while [[ $# -gt 0 ]]; do
case "$1" in
--tag|--app-uri|--listen)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "$1")"
shift 2
;;
--tag=*|--app-uri=*|--listen=*)
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "upgrade" "$1")"
;;
esac
done
ensure_root_via_sudo upgrade "${original_args[@]}"
command_install "${original_args[@]}"
}
command_show_config() {
require_linux
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "show-config" "$1")"
;;
esac
done
[[ -f "${CONFIG_PATH}" ]] || die "$(trf config_not_found "${CONFIG_PATH}")"
cat "${CONFIG_PATH}"
}
resolve_http_listen() {
[[ -f "${CONFIG_PATH}" ]] || die "$(trf config_not_found "${CONFIG_PATH}")"
local listen
listen="$(grep -E '^http_listen=' "${CONFIG_PATH}" | tail -n 1 | cut -d'=' -f2- || true)"
[[ -n "${listen}" ]] || die "$(trf http_listen_not_found "${CONFIG_PATH}")"
printf '%s\n' "${listen}"
}
init_password_url() {
local listen="$1"
local host port
if [[ "${listen}" =~ ^\[([^]]+)\]:(.+)$ ]]; then
host="${BASH_REMATCH[1]}"
port="${BASH_REMATCH[2]}"
elif [[ "${listen}" == :* ]]; then
host="127.0.0.1"
port="${listen#:}"
else
host="${listen%:*}"
port="${listen##*:}"
fi
case "${host}" in
""|"0.0.0.0"|"::"|"[::]")
host="127.0.0.1"
;;
esac
[[ -n "${port}" ]] || die "$(trf http_listen_parse_failed "${listen}")"
printf 'http://%s:%s/admin/api/v1/auth/init-password\n' "${host}" "${port}"
}
command_init_password() {
local listen url body_file http_code curl_rc
require_linux
require_command curl
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "init-password" "$1")"
;;
esac
done
listen="$(resolve_http_listen)"
url="$(init_password_url "${listen}")"
body_file="$(mktemp)"
if http_code="$(curl --silent --show-error --location --output "${body_file}" --write-out '%{http_code}' "${url}")"; then
:
else
curl_rc="$?"
if [[ -s "${body_file}" ]]; then
warn "$(trf curl_partial_body)"
cat "${body_file}" >&2
printf '\n' >&2
fi
rm -f "${body_file}"
return "${curl_rc}"
fi
if [[ "${http_code}" =~ ^2[0-9][0-9]$ ]]; then
cat "${body_file}"
rm -f "${body_file}"
return 0
fi
warn "$(trf init_password_http_failed "${http_code}" "${url}")"
if [[ -s "${body_file}" ]]; then
printf '%b%s%b\n' "${COLOR_YELLOW}${COLOR_BOLD}" "$(trf response_body_label)" "${COLOR_RESET}" >&2
cat "${body_file}" >&2
printf '\n' >&2
else
warn "$(trf response_body_empty)"
fi
rm -f "${body_file}"
return 1
}
command_status() {
local original_args=("$@")
require_linux
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "status" "$1")"
;;
esac
done
ensure_root_via_sudo status "${original_args[@]}"
require_root
require_command systemctl
systemctl status "${SERVICE_NAME}" --no-pager
}
command_logs() {
local lines="100"
local original_args=("$@")
while [[ $# -gt 0 ]]; do
case "$1" in
--lines)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "--lines")"
lines="$2"
shift 2
;;
--lines=*)
lines="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "$(trf unknown_arg "logs" "$1")"
;;
esac
done
[[ "${lines}" =~ ^[1-9][0-9]*$ ]] || die "$(trf lines_positive_integer)"
require_linux
ensure_root_via_sudo logs "${original_args[@]}"
require_root
require_command journalctl
journalctl -u "${SERVICE_NAME}" --no-pager -q -n "${lines}"
}
prompt_input() {
local label="$1"
local default_value="${2:-}"
local input
if [[ -n "${default_value}" ]]; then
printf '%b%s%b [%s]: ' "${COLOR_BOLD}" "${label}" "${COLOR_RESET}" "${default_value}" >&2
else
printf '%b%s%b: ' "${COLOR_BOLD}" "${label}" "${COLOR_RESET}" >&2
fi
read -r input || return 1
if [[ -z "${input}" ]]; then
printf '%s\n' "${default_value}"
else
printf '%s\n' "${input}"
fi
}
pause_for_menu() {
printf '\n'
printf '%b%s%b' "${COLOR_DIM}" "$(trf press_enter_continue)" "${COLOR_RESET}"
read -r _ || true
}
print_rule() {
local char="${1:--}"
local width="${2:-64}"
local line
printf -v line '%*s' "${width}" ''
line="${line// /${char}}"
printf '%b%s%b\n' "${COLOR_DIM}" "${line}" "${COLOR_RESET}"
}
format_badge() {
local text="$1"
local color="${2:-${COLOR_BLUE}}"
printf '%b[%s]%b' "${color}${COLOR_BOLD}" "${text}" "${COLOR_RESET}"
}
format_service_state_badge() {
local state="$1"
local color="${COLOR_RED}"
case "${state}" in
"$(trf state_active)")
color="${COLOR_GREEN}"
;;
"$(trf state_inactive)")
color="${COLOR_YELLOW}"
;;
esac
format_badge "${state}" "${color}"
}
format_menu_item_badge() {
local kind="$1"
local label color
case "${kind}" in
direct)
label="$(trf menu_badge_direct)"
color="${COLOR_GREEN}"
;;
confirm)
label="$(trf menu_badge_confirm)"
color="${COLOR_YELLOW}"
;;
*)
return 0
;;
esac
format_badge "${label}" "${color}"
}
print_menu_meta_line() {
local label="$1"
local value="$2"
printf ' %b%s%b %s\n' "${COLOR_DIM}" "${label}:" "${COLOR_RESET}" "${value}"
}
print_menu_section_title() {
printf '\n%b%s%b\n' "${COLOR_BLUE}${COLOR_BOLD}" "$1" "${COLOR_RESET}"
}
print_menu_item() {
local index="$1"
local title="$2"
local mode="${3:-}"
printf ' %b%2s%b. %s' "${COLOR_CYAN}${COLOR_BOLD}" "${index}" "${COLOR_RESET}" "${title}"
if [[ -n "${mode}" ]]; then
printf ' %s' "$(format_menu_item_badge "${mode}")"
fi
printf '\n'
}
print_menu_action_preview() {
local title="$1"
local description="$2"
printf '\n' >&2
print_rule "-" 64 >&2
printf '%b> %s%b\n' "${COLOR_BLUE}${COLOR_BOLD}" "${title}" "${COLOR_RESET}" >&2
printf '%s\n' "${description}" >&2
print_rule "-" 64 >&2
}
confirm_menu_action() {
local title="$1"
local description="$2"
local reply
print_menu_action_preview "${title}" "${description}"
printf '%b%s%b' "${COLOR_BOLD}" "$(trf menu_confirm_prompt)" "${COLOR_RESET}" >&2
read -r reply || return 1
if is_affirmative_reply "${reply}"; then
return 0
fi
warn "$(trf menu_cancelled_back)"
pause_for_menu
return 1
}
service_state_summary() {
if [[ ! -f "${SYSTEMD_UNIT_PATH}" ]]; then
printf '%s\n' "$(trf state_not_installed)"
return
fi
if ! have_command systemctl; then
printf '%s\n' "$(trf state_no_systemctl)"
return
fi
if [[ ! -d /run/systemd/system ]]; then
printf '%s\n' "$(trf state_no_systemd)"
return
fi
if systemctl is-active --quiet "${SERVICE_NAME}" >/dev/null 2>&1; then
printf '%s\n' "$(trf state_active)"
return
fi
printf '%s\n' "$(trf state_inactive)"
}
print_menu_header() {
local current_lang_label service_state
if [[ "${ACTIVE_LANG}" == "zh" ]]; then
current_lang_label="$(trf language_name_zh)"
else
current_lang_label="$(trf language_name_en)"
fi
service_state="$(service_state_summary)"
printf '\n'
print_rule "=" 64
printf '%b%s%b\n' "${COLOR_CYAN}${COLOR_BOLD}" "$(trf menu_title)" "${COLOR_RESET}"
printf '%b%s%b\n' "${COLOR_DIM}" "$(trf menu_subtitle)" "${COLOR_RESET}"
print_rule "-" 64
print_menu_meta_line "$(trf label_service)" "$(format_service_state_badge "${service_state}")"
print_menu_meta_line "$(trf label_install_dir)" "${INSTALL_DIR}"
print_menu_meta_line "$(trf label_config)" "${CONFIG_PATH}"
print_menu_meta_line "$(trf current_language)" "$(format_badge "${current_lang_label}" "${COLOR_CYAN}")"
print_rule "-" 64
}
print_menu_options() {
print_menu_section_title "$(trf menu_section_setup)"
print_menu_item "1" "$(trf action_download_title)" "confirm"
print_menu_item "2" "$(trf action_install_title)" "confirm"
print_menu_item "3" "$(trf action_upgrade_title)" "confirm"
print_menu_item "4" "$(trf action_self_install_title)" "confirm"
print_menu_section_title "$(trf menu_section_runtime)"
print_menu_item "5" "$(trf action_show_config_title)" "direct"
print_menu_item "6" "$(trf action_init_password_title)" "confirm"
print_menu_item "7" "$(trf action_status_title)" "direct"
print_menu_item "8" "$(trf action_logs_title)" "direct"
print_menu_section_title "$(trf menu_section_tools)"
print_menu_item "9" "$(trf action_language_title)" "direct"
print_menu_item "10" "$(trf action_help_title)" "direct"
print_menu_item "0" "$(trf menu_exit)"
print_rule "-" 64
printf '%b%s%b\n' "${COLOR_DIM}" "$(trf menu_hint_direct)" "${COLOR_RESET}"
printf '%b%s%b\n' "${COLOR_DIM}" "$(trf menu_hint_confirm)" "${COLOR_RESET}"
}
run_menu_command() {
local rc
printf '\n'
if EPCTL_LANG="${ACTIVE_LANG}" "${SCRIPT_SOURCE_PATH}" --lang "${ACTIVE_LANG}" "$@"; then
success "$(trf command_completed)"
else
rc="$?"
warn "$(trf command_failed_exit "${rc}")"
fi
pause_for_menu
}
interactive_download() {
local tag
confirm_menu_action \
"$(trf action_download_title)" \
"$(trf action_download_desc "${CACHE_ROOT}")" || return 0
tag="$(prompt_input "$(trf input_release_tag)")" || return 0
if [[ -n "${tag}" ]]; then
run_menu_command download --tag "${tag}"
else
run_menu_command download
fi
}
interactive_install() {
local tag app_uri listen
confirm_menu_action \
"$(trf action_install_title)" \
"$(trf action_install_desc "${INSTALL_DIR}" "${SYSTEM_USER}" "${SYSTEMD_UNIT_PATH}" "${SERVICE_NAME}")" || return 0
tag="$(prompt_input "$(trf input_release_tag)")" || return 0
app_uri="$(prompt_input "$(trf input_app_uri)" "${DEFAULT_APP_URI}")" || return 0
listen="$(prompt_input "$(trf input_http_listen)" "${DEFAULT_LISTEN}")" || return 0
if [[ -n "${tag}" ]]; then
run_menu_command install --tag "${tag}" --app-uri "${app_uri}" --listen "${listen}"
else
run_menu_command install --app-uri "${app_uri}" --listen "${listen}"
fi
}
interactive_upgrade() {
local tag
confirm_menu_action \
"$(trf action_upgrade_title)" \
"$(trf action_upgrade_desc "${INSTALL_DIR}" "${SERVICE_NAME}")" || return 0
tag="$(prompt_input "$(trf input_upgrade_tag)")" || return 0
if [[ -n "${tag}" ]]; then
run_menu_command upgrade --tag "${tag}"
else
run_menu_command upgrade
fi
}
interactive_logs() {
local lines
print_menu_action_preview \
"$(trf action_logs_title)" \
"$(trf action_logs_desc "${SERVICE_NAME}")"
lines="$(prompt_input "$(trf input_log_lines)" "100")" || return 0
run_menu_command logs --lines "${lines}"
}
interactive_switch_language() {
local choice
print_menu_action_preview \
"$(trf action_language_title)" \
"$(trf action_language_desc)"
printf '\n%b%s%b\n' "${COLOR_BOLD}" "$(trf menu_language_prompt)" "${COLOR_RESET}"
choice="$(prompt_input "$(trf menu_language_input)")" || return 0
case "${choice}" in
1|zh|ZH|cn|CN|中文)
set_language "zh"
success "$(trf language_switched "$(trf language_name_zh)" "$(trf language_name_zh)")"
;;
2|en|EN|english|English|英文|英语)
set_language "en"
success "$(trf language_switched "$(trf language_name_en)" "$(trf language_name_en)")"
;;
*)
warn "$(trf invalid_selection "${choice}")"
;;
esac
pause_for_menu
}
interactive_menu() {
local choice
require_linux
while true; do
print_menu_header
print_menu_options
printf '%b%s%b' "${COLOR_BOLD}" "$(trf menu_select_action)" "${COLOR_RESET}"
read -r choice || return 0
case "${choice}" in
1)
interactive_download
;;
2)
interactive_install
;;
3)
interactive_upgrade
;;
4)
if confirm_menu_action \
"$(trf action_self_install_title)" \
"$(trf action_self_install_desc "${EPCTL_NAME}" "${EPCTL_NAME} --lang zh" "${EPCTL_NAME} --lang en")"; then
run_menu_command self-install
fi
;;
5)
print_menu_action_preview \
"$(trf action_show_config_title)" \
"$(trf action_show_config_desc "${CONFIG_PATH}")"
run_menu_command show-config
;;
6)
if confirm_menu_action \
"$(trf action_init_password_title)" \
"$(trf action_init_password_desc "${CONFIG_PATH}")"; then
run_menu_command init-password
fi
;;
7)
print_menu_action_preview \
"$(trf action_status_title)" \
"$(trf action_status_desc "${SERVICE_NAME}")"
run_menu_command status
;;
8)
interactive_logs
;;
9)
interactive_switch_language
;;
10)
print_menu_action_preview \
"$(trf action_help_title)" \
"$(trf action_help_desc)"
usage
pause_for_menu
;;
0|q|Q|quit|QUIT|exit|EXIT)
return 0
;;
*)
warn "$(trf invalid_selection "${choice}")"
pause_for_menu
;;
esac
done
}
main() {
local command="${1:-}"
shift || true
case "${command}" in
zh|zh-cn|zh_cn|cn|中文)
set_language "zh"
if [[ $# -gt 0 ]]; then
main "$@"
elif is_interactive_terminal; then
interactive_menu
else
usage
fi
;;
en|en-us|en_us|english)
set_language "en"
if [[ $# -gt 0 ]]; then
main "$@"
elif is_interactive_terminal; then
interactive_menu
else
usage
fi
;;
menu|interactive)
interactive_menu
;;
download)
command_download "$@"
;;
install)
command_install "$@"
;;
upgrade)
command_upgrade "$@"
;;
self-install)
command_self_install "$@"
;;
show-config)
command_show_config "$@"
;;
init-password)
command_init_password "$@"
;;
status)
command_status "$@"
;;
logs)
command_logs "$@"
;;
-h|--help|help)
usage
;;
"")
if is_interactive_terminal; then
interactive_menu
else
usage
exit 1
fi
;;
*)
die "$(trf unknown_command "${command}")"
;;
esac
}
set_language ""
parse_global_options "$@"
init_colors
main "${POSITIONAL_ARGS[@]}"