From bff5ceddf07012708b92d676154a9151e2bd0213 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Fri, 7 Aug 2026 14:23:30 +0800 Subject: [PATCH] feat(install): optional add-ons via --with and interactive prompt Wire install_.sh into the main installer through a shared catalog so fnm/pyenv/nvm (and future tools) share one CLI and prompt path. --- README.md | 17 +++++- install.sh | 142 ++++++++++++++++++++++++++++++++++++++++++-- tests/installers.sh | 9 +++ 3 files changed, 160 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8829898..67292d1 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,21 @@ fzf-tab, k), plus eza, zoxide, fzf, and Nerd Fonts. curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/install.sh | bash ``` -Options: `--cp-hist` / `-c` imports bash history; `--non-interactive` / `-n` -skips the default-shell change. +Options: -From a git checkout: +- `--cp-hist` / `-c` — import bash history +- `--non-interactive` / `-n` — skip default-shell change and optional prompts +- `--with ` — install optional add-ons (comma-separated, repeatable). + Each name runs `install_.sh` (currently: `fnm`, `pyenv`, `nvm`). + Interactive installs (without `-n` and without `--with`) prompt for the same list. + +```bash +./install.sh --with fnm +./install.sh --with fnm,pyenv -n +curl -fsSL …/install.sh | bash -s -- --with fnm +``` + +From a git checkout (shell stubs only): ```bash ./deploy.sh diff --git a/install.sh b/install.sh index 12d6285..2903ca0 100755 --- a/install.sh +++ b/install.sh @@ -5,27 +5,120 @@ set -euo pipefail # Save the original working directory ORIGINAL_DIR="$(pwd)" +# Optional add-ons: each name maps to install_.sh in this repo. +# Append here when adding a new optional installer. +OPTIONAL_ADDONS=(fnm pyenv nvm) + +# When run from a git checkout, prefer local install_*.sh; curl|bash uses EASYZSH_RAW_BASE. +SCRIPT_DIR="" +if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "bash" && -f "${BASH_SOURCE[0]}" ]]; then + SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +fi + # Flags to determine if the arguments were passed cp_hist_flag=false noninteractive_flag=false +with_flag_set=false +selected_addons=() -# Loop through all arguments -for arg in "$@" -do - case $arg in +usage() { + cat <<'EOF' +Usage: install.sh [options] + +Options: + -c, --cp-hist Copy bash history into zsh history + -n, --non-interactive Skip default-shell change and optional prompts + --with Install optional add-ons (comma-separated). + Repeatable. Names match install_.sh + (currently: fnm, pyenv, nvm). + -h, --help Show this help + +Examples: + ./install.sh + ./install.sh --with fnm + ./install.sh --with fnm,pyenv -n + curl -fsSL …/install.sh | bash -s -- --with fnm +EOF +} + +is_known_addon() { + local name="$1" + local known + for known in "${OPTIONAL_ADDONS[@]}"; do + [[ "$known" == "$name" ]] && return 0 + done + return 1 +} + +append_addons_csv() { + local csv="$1" + local part + local IFS=',' + # shellcheck disable=SC2086 + for part in $csv; do + part="${part#"${part%%[![:space:]]*}"}" + part="${part%"${part##*[![:space:]]}"}" + [[ -z "$part" ]] && continue + if ! is_known_addon "$part"; then + echo "Unknown optional add-on: $part (known: ${OPTIONAL_ADDONS[*]})" >&2 + exit 2 + fi + selected_addons+=("$part") + done +} + +while [[ $# -gt 0 ]]; do + case $1 in --cp-hist|-c) cp_hist_flag=true + shift ;; --non-interactive|-n) noninteractive_flag=true + shift + ;; + --with) + with_flag_set=true + shift + if [[ $# -eq 0 || "$1" == -* ]]; then + echo "--with requires a comma-separated list of add-on names" >&2 + exit 2 + fi + append_addons_csv "$1" + shift + ;; + --with=*) + with_flag_set=true + append_addons_csv "${1#--with=}" + shift + ;; + -h|--help) + usage + exit 0 ;; *) - echo "Unknown option: $arg" >&2 + echo "Unknown option: $1" >&2 + usage >&2 exit 2 ;; esac done +dedupe_selected_addons() { + local _deduped=() + local _name _d _seen + for _name in "${selected_addons[@]+"${selected_addons[@]}"}"; do + _seen=false + for _d in "${_deduped[@]+"${_deduped[@]}"}"; do + [[ "$_d" == "$_name" ]] && _seen=true && break + done + [[ "$_seen" == false ]] && _deduped+=("$_name") + done + selected_addons=("${_deduped[@]+"${_deduped[@]}"}") +} + +dedupe_selected_addons + if [ -f ~/.gitconfig ]; then cp ~/.gitconfig ~/.gitconfig.backup echo "Backup of .gitconfig created." @@ -424,6 +517,45 @@ else echo -e "\nNot copying bash_history to zsh_history, as --cp-hist or -c is not supplied\n" fi +# Optional add-ons (install_.sh). After shell stubs so patches land correctly. +run_optional_addon() { + local name="$1" + local script="install_${name}.sh" + echo -e "\n=== Optional add-on: ${name} ===\n" + if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/$script" ]]; then + bash "$SCRIPT_DIR/$script" + else + curl -fsSL "${EASYZSH_RAW_BASE}/${script}" | bash + fi +} + +prompt_optional_addons() { + # Only when interactive and user did not pass --with. + echo -e "\nOptional add-ons (each is install_.sh):" + local name + for name in "${OPTIONAL_ADDONS[@]}"; do + echo " - $name" + done + echo -n "Install which? Comma-separated names, or Enter to skip: " + local reply + read -r reply || reply="" + [[ -z "${reply//[[:space:]]/}" ]] && return 0 + append_addons_csv "$reply" +} + +if [[ "$with_flag_set" == false && "$noninteractive_flag" == false ]]; then + prompt_optional_addons + dedupe_selected_addons +fi + +if ((${#selected_addons[@]})); then + for name in "${selected_addons[@]}"; do + run_optional_addon "$name" + done +elif [[ "$with_flag_set" == true ]]; then + echo -e "\n--with given but no add-ons selected; skipping optionals\n" +fi + if [ "$noninteractive_flag" = true ]; then echo -e "Installation complete, exit terminal and enter a new zsh session\n" echo -e "Make sure to change zsh to default shell by running: chsh -s $(command -v zsh)" diff --git a/tests/installers.sh b/tests/installers.sh index c17fec0..21557fc 100755 --- a/tests/installers.sh +++ b/tests/installers.sh @@ -99,6 +99,15 @@ grep -Fq '"$ZSH_CONFIGS_DIR"/*(DN)' "$ROOT/.zshrc" || fail "existing extensionle grep -Fq '_resolve_trash_cmd' "$ROOT/.zshrc" || fail "trash-backed rm helper is missing" grep -Fq 'libglib2.0-bin' "$ROOT/install.sh" || fail "Ubuntu Trash support is not installed" grep -Fq 'deploy.sh' "$ROOT/install.sh" || fail "install.sh does not invoke deploy.sh" +grep -Fq 'OPTIONAL_ADDONS=' "$ROOT/install.sh" || fail "install.sh missing OPTIONAL_ADDONS catalog" +grep -Fq -- '--with' "$ROOT/install.sh" || fail "install.sh missing --with option" +# --with parses known add-ons and rejects unknown ones without network. +if HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --with nosuch >/dev/null 2>&1; then + fail "install.sh accepted an unknown --with add-on" +fi +if ! HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --help >/dev/null; then + fail "install.sh --help failed" +fi grep -Fq 'brew shellenv' "$ROOT/.zshenv" || fail "Homebrew is not initialized in .zshenv" grep -Fq '/usr/local/bin/brew' "$ROOT/.zshenv" || fail "Intel Homebrew path is missing from .zshenv" grep -Fq '$HOME/.local/bin' "$ROOT/.zshenv" || fail "~/.local/bin is not added in .zshenv"