diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..46c109a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + - name: Run shellcheck + # SC2034: globals shared across sourced libs + # SC2086: intentional word-splitting for configurable command paths/flags + # SC1007: empty color assignments when colors disabled + # SC2206: MFlags word-splitting is intentional + run: | + shellcheck --shell=bash \ + -e SC2034,SC2086,SC1007,SC2206 \ + miaur install.sh lib/*.sh tests/run.sh + + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run unit tests + run: bash tests/run.sh diff --git a/README.md b/README.md index a0e94c1..946038d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,26 @@ Install for the current user under `~/.local`: The installer adds the required packages automatically: `base-devel`, `git`, `curl`, and `jq`. +## Upgrade + +Already installed (preferred): + +```bash +miaur self-update --noconfirm +``` + +One-line reinstall from GitHub (same as install; overwrites the current prefix): + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/WHC2006/miaur/main/install.sh) +``` + +User install: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/WHC2006/miaur/main/install.sh) --user +``` + ## Usage ```bash @@ -36,6 +56,8 @@ miaur -Ss # Search packages miaur -Qua # List available AUR upgrades miaur -Sua # Upgrade AUR packages only miaur -G # Download a PKGBUILD +miaur --noconfirm -Syu # Upgrade without prompts +miaur self-update # Update miaur from Git ``` Run `miaur -h` for more options. @@ -44,6 +66,8 @@ Run `miaur -h` for more options. The optional configuration file is `~/.config/miaur/miaur.conf`. See `config/miaur.conf.example`. +Self-update reads `RepoUrl` / `Branch` (or `MIAUR_REPO` / `MIAUR_BRANCH`) when set. + ## License MIT diff --git a/completions/miaur.bash b/completions/miaur.bash index 5f1710c..c400740 100644 --- a/completions/miaur.bash +++ b/completions/miaur.bash @@ -5,8 +5,8 @@ _miaur() { cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD - 1]} - local ops='-S -Syu -Sua -Ss -Si -Sc -Q -Qua -Qm -R -Rns -G -Gp -B -Bi -h -V' - local longs='--aur --repo --skipreview --cleanafter --removemake --noremovermake --bottomup --topdown --clonedir --mflags --sudoloop --nosudoloop --help --version' + local ops='-S -Syu -Sua -Ss -Si -Sc -Q -Qua -Qm -R -Rns -G -Gp -B -Bi -h -V self-update' + local longs='--aur --repo --skipreview --cleanafter --removemake --noremovemake --noremovermake --bottomup --topdown --clonedir --mflags --sudoloop --nosudoloop --noconfirm --self-update --repo-url --branch --help --version' case "$prev" in -S | -Si | -Ss | -G | -Gp | -R | -Rns | -Qi) @@ -17,12 +17,16 @@ _miaur() { COMPREPLY=($(compgen -d -- "$cur")) return ;; + self-update | --self-update) + COMPREPLY=($(compgen -W '--noconfirm --repo-url --branch' -- "$cur")) + return + ;; esac if [[ $cur == -* ]]; then COMPREPLY=($(compgen -W "$ops $longs" -- "$cur")) else - COMPREPLY=($(compgen -W "$(pacman -Ssq "^${cur}" 2>/dev/null | head -40)" -- "$cur")) + COMPREPLY=($(compgen -W "self-update $(pacman -Ssq "^${cur}" 2>/dev/null | head -40)" -- "$cur")) fi } complete -F _miaur miaur diff --git a/completions/miaur.zsh b/completions/miaur.zsh index 6f14694..10da25c 100644 --- a/completions/miaur.zsh +++ b/completions/miaur.zsh @@ -14,9 +14,13 @@ _miaur() { '-Bi:build and install' '-Sc:clean caches' '-Rns:remove a package' + 'self-update:update miaur from Git' '--aur:use the AUR only' '--repo:use repositories only' '--skipreview:skip PKGBUILD review' + '--noremovemake:keep make dependencies' + '--noconfirm:bypass confirmation prompts' + '--self-update:update miaur from Git' '--bottomup:show AUR results first' '--help:show help' ) diff --git a/config/miaur.conf.example b/config/miaur.conf.example index fb6cb9f..2288dee 100644 --- a/config/miaur.conf.example +++ b/config/miaur.conf.example @@ -10,9 +10,12 @@ CleanAfter = false RemoveMake = true BottomUp = false SudoLoop = true +Noconfirm = false # MFlags = -si # SortBy = votes # SearchBy = name-desc # Editor = nvim # Bat = bat +# RepoUrl = https://github.com/WHC2006/miaur.git +# Branch = main diff --git a/install.sh b/install.sh index f80d8d5..376b092 100644 --- a/install.sh +++ b/install.sh @@ -22,9 +22,11 @@ for arg in "$@"; do cat <<'EOF' Usage: install.sh [--user] [--uninstall] - (default) Install system-wide to /usr/local (needs sudo) - --user Install to ~/.local (no sudo for the link) + (default) Install or upgrade system-wide to /usr/local (needs sudo) + --user Install or upgrade to ~/.local (no sudo for the link) --uninstall Remove installed miaur + +Re-running this script upgrades an existing install in place. EOF exit 0 ;; @@ -108,7 +110,11 @@ do_install() { [[ -f $SRC/miaur && -d $SRC/lib ]] || die "incomplete source tree: $SRC" - echo "==> Installing to ${SHARE_DIR}" + if [[ -e $BIN_LINK || -d $SHARE_DIR ]]; then + echo "==> Updating existing install at ${SHARE_DIR}" + else + echo "==> Installing to ${SHARE_DIR}" + fi $SUDO mkdir -p "$SHARE_DIR" "${PREFIX}/bin" $SUDO rm -rf "$SHARE_DIR" $SUDO mkdir -p "$SHARE_DIR" diff --git a/lib/aur_rpc.sh b/lib/aur_rpc.sh index 712fe88..3d92601 100644 --- a/lib/aur_rpc.sh +++ b/lib/aur_rpc.sh @@ -95,7 +95,7 @@ aur_print_info() { # Print search line for AUR result: aur/name version (votes) desc aur_format_search_line() { - local name=$1 ver=$2 votes=$3 pop=$4 desc=$5 + local name=$1 ver=$2 votes=$3 desc=$5 local installed= if is_pkg_installed "$name"; then local localver diff --git a/lib/build.sh b/lib/build.sh index 47e2c5a..18c4193 100644 --- a/lib/build.sh +++ b/lib/build.sh @@ -27,8 +27,10 @@ build_pkg() { *) clean_flags+=("$f") ;; esac done + local -a mk_extra=() + mapfile -t mk_extra < <(pac_noconfirm_args) # Always sync deps for repo side via makepkg -s when needed - ${MIAUR_MAKEPKG} -s --noconfirm "${clean_flags[@]}" || exit 1 + ${MIAUR_MAKEPKG} -s "${mk_extra[@]}" "${clean_flags[@]}" || exit 1 ) || die "build failed: $name" if [[ $do_install -eq 1 ]]; then @@ -36,7 +38,10 @@ build_pkg() { mapfile -t pkgs < <(find "$dir" -maxdepth 1 -type f \( -name '*.pkg.tar.zst' -o -name '*.pkg.tar.xz' -o -name '*.pkg.tar.gz' \) ! -name '*-debug-*' | sort) [[ ${#pkgs[@]} -eq 0 ]] && die "no build artifacts found: $name" msg "Installing $name..." - local -a uflags=(-U --noconfirm) + local -a uflags=(-U) + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) + uflags+=("${conf[@]}") [[ $asdeps -eq 1 ]] && uflags+=(--asdeps) as_root ${MIAUR_PACMAN} "${uflags[@]}" "${pkgs[@]}" || die "installation failed: $name" fi @@ -60,6 +65,13 @@ install_aur_packages() { msg "Resolving dependencies..." local -a order=() mapfile -t order < <(aur_resolve_order "${targets[@]}") + # Drop empty line if order was empty print + local -a order_clean=() + local _o + for _o in "${order[@]}"; do + [[ -n $_o ]] && order_clean+=("$_o") + done + order=("${order_clean[@]}") [[ ${#order[@]} -eq 0 ]] && die "no packages to install" # Collect all repo deps across the graph @@ -81,6 +93,28 @@ install_aur_packages() { done < <(collect_deps_for_dir "$dir" 1 1) done + msg "Packages to build (${#order[@]}):" + declare -A is_target_preview=() + for pkg in "${targets[@]}"; do + is_target_preview[$pkg]=1 + done + for pkg in "${order[@]}"; do + if [[ -n ${is_target_preview[$pkg]+x} ]]; then + msg2 "aur/$pkg (target)" + else + msg2 "aur/$pkg" + fi + done + if [[ ${#repo_deps[@]} -gt 0 ]]; then + msg "Repository dependencies (${#repo_deps[@]}):" + msg2 "${repo_deps[*]}" + fi + confirm_ask "Proceed with installation?" || { + warn "Cancelled by user" + sudoloop_stop + return 1 + } + install_repo_deps "${repo_deps[@]}" # Review all packages first (like paru) @@ -157,6 +191,7 @@ install_aur_packages() { done if [[ ${#to_rm[@]} -gt 0 ]]; then msg "Removing make dependencies: ${to_rm[*]}" + # Cleanup after an already-confirmed install: avoid a second prompt by default as_root ${MIAUR_PACMAN} -Rns --noconfirm "${to_rm[@]}" || warn "failed to remove make dependencies" fi fi diff --git a/lib/cli.sh b/lib/cli.sh index df5042e..d59a2d4 100644 --- a/lib/cli.sh +++ b/lib/cli.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC2034 # CLI parsing — paru-like short option clusters # Globals set by parse_args @@ -35,6 +36,7 @@ Pacman operations extended with AUR support: Additional operations: miaur {-G --getpkgbuild} [packages] miaur {-B --build} [directories] + miaur self-update Update miaur from Git With no arguments, miaur runs 'miaur -Syu'. @@ -48,6 +50,7 @@ Common commands: miaur -G Download a PKGBUILD miaur -Gp Print a PKGBUILD miaur -Bi . Build and install a local PKGBUILD + miaur self-update Update miaur itself Options: -a, --aur Assume targets are from the AUR @@ -61,9 +64,37 @@ Options: --mflags Pass additional flags to makepkg --sudoloop Keep sudo credentials alive (default) --nosudoloop Disable the sudo keep-alive loop + --noconfirm Bypass any confirmation prompts + --self-update Same as: miaur self-update EOF } +# Strip miaur-only options so the remainder can be forwarded to pacman. +# Prints one argument per line (safe for mapfile with typical package names). +strip_miaur_opts() { + local -a raw=("$@") + local i=0 + while ((i < ${#raw[@]})); do + local arg=${raw[$i]} + case "$arg" in + --aur | --repo | --skipreview | --review | --cleanafter | --nocleanafter | \ + --removemake | --noremovemake | --noremovermake | --bottomup | --topdown | \ + --sudoloop | --nosudoloop | --noconfirm | --self-update) ;; + --clonedir | --mflags | --mode) + i=$((i + 1)) + ;; + --clonedir=* | --mflags=* | --mode=* | --repo-url=* | --branch=*) ;; + --repo-url | --branch) + i=$((i + 1)) + ;; + *) + printf '%s\n' "$arg" + ;; + esac + i=$((i + 1)) + done +} + parse_args() { if [[ $# -eq 0 ]]; then OP=S @@ -72,6 +103,40 @@ parse_args() { return 0 fi + # Subcommand: self-update [--noconfirm] [--repo-url URL] [--branch NAME] + if [[ $1 == self-update || $1 == --self-update ]]; then + OP=selfupdate + local -a raw=("$@") + local i=1 + while ((i < ${#raw[@]})); do + local arg=${raw[$i]} + case "$arg" in + --noconfirm) MIAUR_NOCONFIRM=1 ;; + --repo-url) + i=$((i + 1)) + MIAUR_REPO=${raw[$i]:-} + [[ -n $MIAUR_REPO ]] || die "--repo-url requires a URL" + ;; + --repo-url=*) MIAUR_REPO=${arg#*=} ;; + --branch) + i=$((i + 1)) + MIAUR_BRANCH=${raw[$i]:-} + [[ -n $MIAUR_BRANCH ]] || die "--branch requires a name" + ;; + --branch=*) MIAUR_BRANCH=${arg#*=} ;; + -h | --help) + print_help + exit 0 + ;; + *) + die "unknown self-update option: $arg" + ;; + esac + i=$((i + 1)) + done + return 0 + fi + # No leading dash → interactive search if [[ $1 != -* ]]; then OP=interactive @@ -92,6 +157,9 @@ parse_args() { printf 'miaur v%s\n' "$MIAUR_VERSION" exit 0 ;; + --self-update) + OP=selfupdate + ;; --aur) OP_AUR_ONLY=1 MIAUR_MODE=aur @@ -104,11 +172,14 @@ parse_args() { --cleanafter) MIAUR_CLEANAFTER=1 ;; --nocleanafter) MIAUR_CLEANAFTER=0 ;; --removemake) MIAUR_REMOVEMAKE=1 ;; + --noremovemake) MIAUR_REMOVEMAKE=0 ;; + # Typo alias kept for compatibility with older docs/scripts --noremovermake) MIAUR_REMOVEMAKE=0 ;; --bottomup) MIAUR_BOTTOMUP=1 ;; --topdown) MIAUR_BOTTOMUP=0 ;; --sudoloop) MIAUR_SUDOLOOP=1 ;; --nosudoloop) MIAUR_SUDOLOOP=0 ;; + --noconfirm) MIAUR_NOCONFIRM=1 ;; --clonedir) i=$((i + 1)) MIAUR_CLONEDIR=${raw[$i]:-} @@ -125,6 +196,18 @@ parse_args() { MIAUR_MODE=${raw[$i]:-} ;; --mode=*) MIAUR_MODE=${arg#*=} ;; + --repo-url) + i=$((i + 1)) + MIAUR_REPO=${raw[$i]:-} + [[ -n $MIAUR_REPO ]] || die "--repo-url requires a URL" + ;; + --repo-url=*) MIAUR_REPO=${arg#*=} ;; + --branch) + i=$((i + 1)) + MIAUR_BRANCH=${raw[$i]:-} + [[ -n $MIAUR_BRANCH ]] || die "--branch requires a name" + ;; + --branch=*) MIAUR_BRANCH=${arg#*=} ;; --sync) OP=S ;; --query) OP=Q ;; --remove) @@ -133,7 +216,20 @@ parse_args() { ;; --getpkgbuild) OP=G ;; --build) OP=B ;; - --database | --files | --deptest | --upgrade) + --database) + OP=D + DO_PASSTHRU=1 + ;; + --files) + OP=F + DO_PASSTHRU=1 + ;; + --deptest) + OP=T + DO_PASSTHRU=1 + ;; + --upgrade) + OP=U DO_PASSTHRU=1 ;; --) @@ -144,6 +240,11 @@ parse_args() { done break ;; + --*) + # Unknown long option: keep for pacman passthrough (e.g. --ignore) + PACMAN_EXTRA+=("$arg") + DO_PASSTHRU=1 + ;; -*) local cluster=${arg:1} local j c @@ -158,7 +259,22 @@ parse_args() { ;; G) OP=G ;; B) OP=B ;; - D | F | T | U) DO_PASSTHRU=1 ;; + D) + OP=D + DO_PASSTHRU=1 + ;; + F) + OP=F + DO_PASSTHRU=1 + ;; + T) + OP=T + DO_PASSTHRU=1 + ;; + U) + OP=U + DO_PASSTHRU=1 + ;; y) OP_REFRESH=1 ;; u) OP_UPGRADE=1 ;; s) OP_SEARCH=$((OP_SEARCH + 1)) ;; diff --git a/lib/common.sh b/lib/common.sh index abe77cd..24b6328 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -2,7 +2,7 @@ # shellcheck disable=SC2034 # Common utilities for miaur -MIAUR_VERSION="0.1.0" +MIAUR_VERSION="0.2.0" # Colors (respect pacman Color / NO_COLOR / non-tty) _miaur_color_enabled() { @@ -34,6 +34,28 @@ warn() { printf '%s==>%s WARNING: %s\n' "${C_BOLD}${C_YELLOW}" "${C_RESET}" "$*" error() { printf '%s==>%s ERROR: %s\n' "${C_BOLD}${C_RED}" "${C_RESET}" "$*" >&2; } die() { error "$*"; exit 1; } +# Append --noconfirm when requested (default: ask, like pacman/paru). +pac_noconfirm_args() { + if [[ ${MIAUR_NOCONFIRM:-0} -eq 1 ]]; then + printf '%s\n' --noconfirm + fi +} + +# Prompt Y/n; returns 0 to continue. Skipped when --noconfirm. +confirm_ask() { + local prompt=${1:-Continue?} + if [[ ${MIAUR_NOCONFIRM:-0} -eq 1 ]]; then + return 0 + fi + printf '%s::%s %s [Y/n] ' "${C_BOLD}${C_BLUE}" "${C_RESET}" "$prompt" >&2 + local ans= + read -r ans || return 1 + case "${ans:-y}" in + y | Y | yes | YES | '') return 0 ;; + *) return 1 ;; + esac +} + need_cmd() { local c for c in "$@"; do diff --git a/lib/config.sh b/lib/config.sh index 5e2fb39..114437f 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -10,6 +10,7 @@ MIAUR_CLEANAFTER="${MIAUR_CLEANAFTER:-0}" MIAUR_REMOVEMAKE="${MIAUR_REMOVEMAKE:-1}" MIAUR_BOTTOMUP="${MIAUR_BOTTOMUP:-0}" MIAUR_SUDOLOOP="${MIAUR_SUDOLOOP:-1}" +MIAUR_NOCONFIRM="${MIAUR_NOCONFIRM:-0}" MIAUR_MFLAGS="${MIAUR_MFLAGS:--si}" MIAUR_SORTBY="${MIAUR_SORTBY:-votes}" MIAUR_SEARCHBY="${MIAUR_SEARCHBY:-name-desc}" @@ -21,6 +22,8 @@ MIAUR_MAKEPKG="${MIAUR_MAKEPKG:-makepkg}" MIAUR_PACMAN="${MIAUR_PACMAN:-pacman}" MIAUR_BAT="${MIAUR_BAT:-bat}" MIAUR_EDITOR="${VISUAL:-${EDITOR:-nano}}" +MIAUR_REPO="${MIAUR_REPO:-https://github.com/WHC2006/miaur.git}" +MIAUR_BRANCH="${MIAUR_BRANCH:-main}" _bool_val() { case "${1,,}" in @@ -61,6 +64,7 @@ miaur_load_config() { RemoveMake) MIAUR_REMOVEMAKE=$(_bool_val "$val") ;; BottomUp) MIAUR_BOTTOMUP=$(_bool_val "$val") ;; SudoLoop) MIAUR_SUDOLOOP=$(_bool_val "$val") ;; + Noconfirm | NoConfirm) MIAUR_NOCONFIRM=$(_bool_val "$val") ;; MFlags) MIAUR_MFLAGS=$val ;; SortBy) MIAUR_SORTBY=$val ;; SearchBy) MIAUR_SEARCHBY=$val ;; @@ -71,6 +75,8 @@ miaur_load_config() { Pacman) MIAUR_PACMAN=$val ;; Bat) MIAUR_BAT=$val ;; Editor) MIAUR_EDITOR=$val ;; + RepoUrl | MIAUR_REPO) MIAUR_REPO=$val ;; + Branch | MIAUR_BRANCH) MIAUR_BRANCH=$val ;; esac done <"$conf" } diff --git a/lib/deps.sh b/lib/deps.sh index 7f58a4a..d45776e 100644 --- a/lib/deps.sh +++ b/lib/deps.sh @@ -23,8 +23,8 @@ srcinfo_dump() { # Returns 0 if dep is satisfied (installed or provided by installed pkg) dep_satisfied() { - local dep name - name=$(dep_name "$1") + local _name + _name=$(dep_name "$1") # pacman -T returns unsatisfied deps; empty means all ok local out out=$(pacman -T "$1" 2>/dev/null) || true @@ -55,11 +55,6 @@ resolve_one_dep() { echo "aur:$name" return 0 fi - # Try provides via pacman -Ssq exact? fallback AUR search by name - if [[ ${MIAUR_MODE:-} != repo ]] && aur_exists "$name"; then - echo "aur:$name" - return 0 - fi echo "missing:$name" } @@ -152,5 +147,7 @@ install_repo_deps() { done [[ ${#list[@]} -eq 0 ]] && return 0 msg "Installing repository dependencies: ${list[*]}" - as_root ${MIAUR_PACMAN} -S --needed --asdeps --noconfirm "${list[@]}" + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) + as_root ${MIAUR_PACMAN} -S --needed --asdeps "${conf[@]}" "${list[@]}" } diff --git a/lib/dispatch.sh b/lib/dispatch.sh index 2e623f5..17c77c2 100644 --- a/lib/dispatch.sh +++ b/lib/dispatch.sh @@ -108,7 +108,9 @@ cmd_interactive() { done if [[ ${#repo_pkgs[@]} -gt 0 ]]; then - as_root ${MIAUR_PACMAN} -S --needed --noconfirm "${repo_pkgs[@]}" + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) + as_root ${MIAUR_PACMAN} -S --needed "${conf[@]}" "${repo_pkgs[@]}" fi if [[ ${#aur_pkgs[@]} -gt 0 ]]; then install_aur_packages "${aur_pkgs[@]}" @@ -154,7 +156,9 @@ cmd_sync_install() { done if [[ ${#repo_pkgs[@]} -gt 0 ]]; then - as_root ${MIAUR_PACMAN} -S --needed --noconfirm "${repo_pkgs[@]}" + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) + as_root ${MIAUR_PACMAN} -S --needed "${conf[@]}" "${repo_pkgs[@]}" fi if [[ ${#aur_pkgs[@]} -gt 0 ]]; then install_aur_packages "${aur_pkgs[@]}" @@ -185,33 +189,25 @@ cmd_getpkgbuild() { cmd_clean() { # -Sc: clean pacman cache + AUR clone cache if [[ ${MIAUR_MODE:-} != aur ]]; then - as_root ${MIAUR_PACMAN} -Sc --noconfirm || true + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) + as_root ${MIAUR_PACMAN} -Sc "${conf[@]}" || true fi aur_clean_cache } dispatch() { - # Full passthrough ops (R, D, F, T, U, or unknown) - if [[ $DO_PASSTHRU -eq 1 && $OP != S && $OP != Q ]]; then - # Rebuild approximate pacman command from original? Simpler: use saved extras - local -a args=() - case "$OP" in - R) args+=(-R) ;; - *) - # Fall back: user should use pacman; try to forward - args+=(-"${OP}") - ;; - esac - local c - for c in "${PACMAN_EXTRA[@]}"; do - args+=("-$c") - done - args+=("${TARGETS[@]}") - pacman_passthru "${args[@]}" - return $? + # Passthrough for R/D/F/T/U is handled in main() via strip_miaur_opts. + # Keep a reconstruction fallback only if somehow reached without orig argv. + if [[ $DO_PASSTHRU -eq 1 && $OP != S && $OP != Q && $OP != G && $OP != B && $OP != interactive && $OP != selfupdate ]]; then + die "internal error: passthrough should be handled in main" fi case "$OP" in + selfupdate) + need_cmd git + cmd_self_update + ;; interactive) need_cmd curl jq cmd_interactive "${TARGETS[@]}" @@ -233,9 +229,11 @@ dispatch() { fi if [[ $OP_UPGRADE -eq 1 ]]; then need_cmd curl jq + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) if [[ $OP_AUR_ONLY -eq 1 ]]; then # -Sua or -Su --aur - [[ $OP_REFRESH -eq 1 ]] && as_root ${MIAUR_PACMAN} -Sy --noconfirm + [[ $OP_REFRESH -eq 1 ]] && as_root ${MIAUR_PACMAN} -Sy "${conf[@]}" cmd_upgrade_aur else cmd_upgrade_all "$OP_REFRESH" @@ -243,11 +241,17 @@ dispatch() { return fi if [[ $OP_REFRESH -eq 1 && ${#TARGETS[@]} -eq 0 ]]; then - as_root ${MIAUR_PACMAN} -Sy --noconfirm + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) + as_root ${MIAUR_PACMAN} -Sy "${conf[@]}" return fi # Install packages; optionally refresh first - [[ $OP_REFRESH -eq 1 ]] && as_root ${MIAUR_PACMAN} -Sy --noconfirm + if [[ $OP_REFRESH -eq 1 ]]; then + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) + as_root ${MIAUR_PACMAN} -Sy "${conf[@]}" + fi need_cmd curl jq cmd_sync_install "${TARGETS[@]}" ;; @@ -268,7 +272,12 @@ dispatch() { local -a qargs=(-Q) local c for c in "${PACMAN_EXTRA[@]}"; do - qargs+=("-$c") + # Long options like --explicit may land in PACMAN_EXTRA as full tokens + if [[ $c == --* ]]; then + qargs+=("$c") + else + qargs+=("-$c") + fi done [[ $OP_AUR_ONLY -eq 1 ]] && qargs+=(-m) [[ $OP_SEARCH -gt 0 ]] && qargs+=(-s) diff --git a/lib/review.sh b/lib/review.sh index 67b8df2..bf6604a 100644 --- a/lib/review.sh +++ b/lib/review.sh @@ -1,7 +1,37 @@ #!/usr/bin/env bash # PKGBUILD review -# Review PKGBUILD in dir. Returns 0 to continue, 1 to abort. +# Show a file with bat/less/cat +_review_show_file() { + local file=$1 + [[ -f $file ]] || return 0 + msg2 "File: ${file#"$2"/}" + if command -v "${MIAUR_BAT}" >/dev/null 2>&1; then + "${MIAUR_BAT}" --style=plain --paging=always --language=bash "$file" || true + elif command -v less >/dev/null 2>&1; then + less -R "$file" || true + else + cat "$file" + fi +} + +# List reviewable files in a package directory (relative paths) +_review_list_files() { + local dir=$1 + ( + cd "$dir" || exit 1 + # PKGBUILD first, then install scripts and other shell helpers + [[ -f PKGBUILD ]] && printf '%s\n' PKGBUILD + find . -maxdepth 1 -type f \( \ + -name '*.install' -o \ + -name '*.sh' -o \ + -name 'apply-patches' -o \ + -name '*.hook' \ + \) ! -name 'PKGBUILD' -printf '%P\n' 2>/dev/null | sort + ) +} + +# Review PKGBUILD in dir. Returns 0 to continue, 1 to abort, 2 to skip. review_pkgbuild() { local dir=$1 local name=${2:-$(basename "$dir")} @@ -11,23 +41,17 @@ review_pkgbuild() { local pb="$dir/PKGBUILD" [[ -f $pb ]] || die "PKGBUILD not found: $dir" - msg "Reviewing the PKGBUILD for $name" - if command -v "${MIAUR_BAT}" >/dev/null 2>&1; then - "${MIAUR_BAT}" --style=plain --paging=always --language=bash "$pb" || true - elif command -v less >/dev/null 2>&1; then - less -R "$pb" || true - else - cat "$pb" + msg "Reviewing $name" + local -a files=() + mapfile -t files < <(_review_list_files "$dir") + if [[ ${#files[@]} -gt 1 ]]; then + msg2 "Files: ${files[*]}" fi - # Also show install script / other files briefly - local f - for f in "$dir"/*.install "$dir"/PKGBUILD; do - [[ -f $f ]] || continue - done + _review_show_file "$pb" "$dir" while true; do - printf '%s::%s View diff (d), edit (e), continue (y), skip (s), or quit (q)? [Y/d/e/s/q] ' \ + printf '%s::%s View diff (d), files (f), edit (e), continue (y), skip (s), or quit (q)? [Y/d/f/e/s/q] ' \ "${C_BOLD}${C_BLUE}" "${C_RESET}" local ans= read -r ans || return 1 @@ -42,9 +66,40 @@ review_pkgbuild() { e | edit) "${MIAUR_EDITOR}" "$pb" ;; + f | files) + local -a again=() + mapfile -t again < <(_review_list_files "$dir") + local i + for i in "${!again[@]}"; do + printf ' %s%2d%s %s\n' "${C_DIM}" "$((i + 1))" "${C_RESET}" "${again[$i]}" + done + printf '%s::%s Open file number (empty to return): ' \ + "${C_BOLD}${C_BLUE}" "${C_RESET}" + local pick= + read -r pick || true + if [[ $pick =~ ^[0-9]+$ ]]; then + local idx=$((pick - 1)) + if ((idx >= 0 && idx < ${#again[@]})); then + _review_show_file "$dir/${again[$idx]}" "$dir" + else + warn "Invalid number: $pick" + fi + fi + ;; d | diff) if [[ -d $dir/.git ]]; then - (cd "$dir" && ${MIAUR_GIT} diff -- PKGBUILD) || true + ( + cd "$dir" || exit 0 + # Prefer unstaged/staged local edits; fall back to last fetch range + if ! ${MIAUR_GIT} diff --stat --quiet HEAD 2>/dev/null; then + ${MIAUR_GIT} --no-pager diff -- HEAD + elif ${MIAUR_GIT} rev-parse --verify 'HEAD@{1}' >/dev/null 2>&1; then + ${MIAUR_GIT} --no-pager diff -- 'HEAD@{1}'..HEAD + else + warn "No prior revision available for comparison" + ${MIAUR_GIT} --no-pager diff -- PKGBUILD || true + fi + ) || true else warn "No Git history is available for comparison" fi diff --git a/lib/self_update.sh b/lib/self_update.sh new file mode 100644 index 0000000..49c110e --- /dev/null +++ b/lib/self_update.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Self-update miaur from its Git repository + +# Detect install prefix from the running binary location. +# Prints: user|/path/prefix or system|/path/prefix +miaur_detect_prefix() { + case "$MIAUR_HOME" in + "${HOME}/.local/share/miaur") + printf 'user|%s\n' "${HOME}/.local" + ;; + /usr/local/share/miaur) + printf 'system|%s\n' /usr/local + ;; + *) + # Dev checkout or custom layout — default to a user install + printf 'user|%s\n' "${HOME}/.local" + ;; + esac +} + +# Read MIAUR_VERSION=... from a common.sh path +_miaur_read_version_file() { + local file=$1 + [[ -f $file ]] || return 1 + local line + line=$(grep -E '^MIAUR_VERSION=' "$file" | head -n1) || return 1 + line=${line#MIAUR_VERSION=} + line=${line#\"} + line=${line%\"} + line=${line#\'} + line=${line%\'} + printf '%s\n' "$line" +} + +# Install tree from SRC into PREFIX (user|system via $1 mode) +_miaur_install_tree() { + local mode=$1 + local prefix=$2 + local src=$3 + local share="${prefix}/share/miaur" + local binlink="${prefix}/bin/miaur" + + [[ -f $src/miaur && -d $src/lib ]] || die "incomplete source tree: $src" + + _run() { + if [[ $mode == system ]]; then + as_root "$@" + else + "$@" + fi + } + + msg "Installing to ${share}" + _run mkdir -p "$share" "${prefix}/bin" + _run rm -rf "$share" + _run mkdir -p "$share" + _run cp -a "$src/miaur" "$src/lib" "$share/" + if [[ -d $src/completions ]]; then + _run cp -a "$src/completions" "$share/" + fi + if [[ -d $src/config ]]; then + _run cp -a "$src/config" "$share/" + fi + _run chmod 755 "$share/miaur" + _run ln -sfn "$share/miaur" "$binlink" + + if [[ -d $src/completions ]]; then + if [[ $mode == user ]]; then + mkdir -p "${HOME}/.local/share/bash-completion/completions" + cp -f "$src/completions/miaur.bash" \ + "${HOME}/.local/share/bash-completion/completions/miaur" 2>/dev/null || true + else + if [[ -d /usr/share/bash-completion/completions ]]; then + as_root cp -f "$src/completions/miaur.bash" \ + /usr/share/bash-completion/completions/miaur 2>/dev/null || true + fi + fi + fi + + printf '%s\n' "$binlink" +} + +# Fetch latest sources into a temp dir; prints path to clone +_miaur_fetch_latest() { + local repo=$1 + local branch=$2 + local tmp + tmp=$(mktemp -d) + # Caller cleans up via SELF_UPDATE_TMP + SELF_UPDATE_TMP=$tmp + msg2 "Cloning ${repo} (${branch})..." + ${MIAUR_GIT} clone --depth 1 --branch "$branch" "$repo" "$tmp/miaur" \ + || die "failed to clone ${repo} (branch ${branch}); set RepoUrl / MIAUR_REPO if you forked" + [[ -f $tmp/miaur/miaur && -d $tmp/miaur/lib ]] || die "cloned tree looks incomplete" + printf '%s\n' "$tmp/miaur" +} + +cmd_self_update() { + need_cmd git + + local repo=${MIAUR_REPO:-https://github.com/WHC2006/miaur.git} + local branch=${MIAUR_BRANCH:-main} + local info mode prefix + info=$(miaur_detect_prefix) + mode=${info%%|*} + prefix=${info#*|} + + local old_ver=${MIAUR_VERSION} + msg "Current version: v${old_ver}" + msg2 "Install prefix: ${prefix} (${mode})" + msg2 "Source: ${repo} @ ${branch}" + + if [[ $MIAUR_HOME != "${prefix}/share/miaur" ]]; then + warn "Running from ${MIAUR_HOME}; update will install into ${prefix}/share/miaur" + fi + + confirm_ask "Update miaur from Git?" || { + warn "Cancelled by user" + return 1 + } + + SELF_UPDATE_TMP= + # shellcheck disable=SC2064 + trap '[[ -n ${SELF_UPDATE_TMP:-} ]] && rm -rf "$SELF_UPDATE_TMP"; sudoloop_stop' RETURN + + local src new_ver binlink + src=$(_miaur_fetch_latest "$repo" "$branch") + new_ver=$(_miaur_read_version_file "$src/lib/common.sh") || new_ver=unknown + + if [[ $new_ver == "$old_ver" ]]; then + msg "Remote reports the same version (v${new_ver}); reinstalling anyway" + else + msg "Updating: v${old_ver} -> v${new_ver}" + fi + + if [[ $mode == system ]]; then + sudoloop_start + fi + + binlink=$(_miaur_install_tree "$mode" "$prefix" "$src") + + msg "Done: ${binlink}" + if [[ -x $binlink ]]; then + "$binlink" -V + fi +} diff --git a/lib/upgrade.sh b/lib/upgrade.sh index aa4dc39..cfb7459 100644 --- a/lib/upgrade.sh +++ b/lib/upgrade.sh @@ -60,8 +60,8 @@ cmd_query_aur_upgrades() { # Upgrade all outdated AUR packages cmd_upgrade_aur() { local -a pkgs=() - local name old arrow new - while read -r name old arrow new; do + local name old _arrow new + while read -r name old _arrow new; do [[ -z $name ]] && continue pkgs+=("$name") msg2 "$name ${old} -> ${new}" @@ -79,16 +79,14 @@ cmd_upgrade_aur() { cmd_upgrade_all() { local refresh=${1:-0} sudoloop_start - local -a pac_args=() - [[ $refresh -eq 1 ]] && pac_args+=(-y) - pac_args+=(-Su) + local -a conf=() + mapfile -t conf < <(pac_noconfirm_args) msg "Synchronizing databases and upgrading repository packages..." - # Use -Syu style via pacman if [[ $refresh -eq 1 ]]; then - as_root ${MIAUR_PACMAN} -Syu --noconfirm || die "repository upgrade failed" + as_root ${MIAUR_PACMAN} -Syu "${conf[@]}" || die "repository upgrade failed" else - as_root ${MIAUR_PACMAN} -Su --noconfirm || die "repository upgrade failed" + as_root ${MIAUR_PACMAN} -Su "${conf[@]}" || die "repository upgrade failed" fi msg "Checking for AUR updates..." diff --git a/miaur b/miaur old mode 100644 new mode 100755 index fc1e329..4f75ae5 --- a/miaur +++ b/miaur @@ -21,6 +21,8 @@ source "$MIAUR_HOME/lib/deps.sh" source "$MIAUR_HOME/lib/build.sh" # shellcheck source=lib/upgrade.sh source "$MIAUR_HOME/lib/upgrade.sh" +# shellcheck source=lib/self_update.sh +source "$MIAUR_HOME/lib/self_update.sh" # shellcheck source=lib/cli.sh source "$MIAUR_HOME/lib/cli.sh" # shellcheck source=lib/dispatch.sh @@ -29,28 +31,16 @@ source "$MIAUR_HOME/lib/dispatch.sh" main() { miaur_load_config - # Fast path: operations that are pure pacman wrappers with no AUR flags - # Keep original argv for -R/-D/-F/-T/-U when no miaur-specific long opts mixed in awkwardly. - # We still parse to detect them. local -a orig=("$@") parse_args "$@" - # Special: if DO_PASSTHRU and OP empty, forward entire original command - if [[ $DO_PASSTHRU -eq 1 && -z ${OP:-} ]]; then - # Replace program name conceptually — strip miaur-only long options already consumed - as_root ${MIAUR_PACMAN} "${orig[@]}" - return $? - fi - - # For -R with extras like -Rns: OP=R DO_PASSTHRU=1 - if [[ ${OP:-} == R ]]; then - local -a rargs=(-R) - local c - for c in "${PACMAN_EXTRA[@]}"; do - rargs+=("-$c") - done - rargs+=("${TARGETS[@]}") - as_root ${MIAUR_PACMAN} "${rargs[@]}" + # Pure pacman passthrough: forward original argv minus miaur-only options + # so long options like --ignore survive (unlike short-cluster reconstruction). + if [[ $DO_PASSTHRU -eq 1 && $OP != S && $OP != Q && $OP != G && $OP != B && $OP != interactive && $OP != selfupdate ]]; then + local -a filtered=() + mapfile -t filtered < <(strip_miaur_opts "${orig[@]}") + [[ ${#filtered[@]} -gt 0 ]] || die "nothing to pass through to pacman" + as_root ${MIAUR_PACMAN} "${filtered[@]}" return $? fi diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..64f37d7 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# Lightweight self-tests for miaur (no Arch/pacman required) +set -euo pipefail + +ROOT=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd) +# shellcheck source=lib/common.sh +source "$ROOT/lib/common.sh" +# shellcheck source=lib/cli.sh +source "$ROOT/lib/cli.sh" + +fail() { echo "FAIL: $*" >&2; exit 1; } +pass() { echo "ok - $*"; } + +# --- strip_miaur_opts --- +{ + mapfile -t out < <(strip_miaur_opts -Rns --nosudoloop --ignore linux foo) + [[ ${#out[@]} -eq 4 ]] || fail "strip count=${#out[@]} contents=${out[*]}" + [[ ${out[0]} == -Rns && ${out[1]} == --ignore && ${out[2]} == linux && ${out[3]} == foo ]] \ + || fail "strip values: ${out[*]}" + pass "strip_miaur_opts keeps pacman argv" +} + +{ + mapfile -t out < <(strip_miaur_opts -S --noconfirm --aur pkg) + [[ ${#out[@]} -eq 2 && ${out[0]} == -S && ${out[1]} == pkg ]] \ + || fail "strip noconfirm/aur: ${out[*]}" + pass "strip_miaur_opts drops miaur-only flags" +} + +# --- parse_args: --noremovemake --- +{ + OP= + MIAUR_REMOVEMAKE=1 + MIAUR_NOCONFIRM=0 + DO_PASSTHRU=0 + OP_REFRESH=0 + OP_UPGRADE=0 + TARGETS=() + PACMAN_EXTRA=() + parse_args --noremovemake -S + [[ $MIAUR_REMOVEMAKE -eq 0 ]] || fail "noremovemake not applied" + [[ $OP == S ]] || fail "OP after noremovermake+S" + pass "parse_args --noremovemake" +} + +{ + OP= + MIAUR_REMOVEMAKE=1 + TARGETS=() + PACMAN_EXTRA=() + parse_args --noremovermake + [[ $MIAUR_REMOVEMAKE -eq 0 ]] || fail "typo alias noremovermake not applied" + pass "parse_args typo alias --noremovermake" +} + +{ + OP= + MIAUR_NOCONFIRM=0 + OP_REFRESH=0 + OP_UPGRADE=0 + TARGETS=() + PACMAN_EXTRA=() + parse_args --noconfirm -Syu + [[ $MIAUR_NOCONFIRM -eq 1 ]] || fail "noconfirm not set" + [[ $OP == S && $OP_REFRESH -eq 1 && $OP_UPGRADE -eq 1 ]] || fail "Syu parse" + pass "parse_args --noconfirm -Syu" +} + +{ + OP= + DO_PASSTHRU=0 + TARGETS=() + PACMAN_EXTRA=() + parse_args -R --ignore linux foo + [[ $OP == R && $DO_PASSTHRU -eq 1 ]] || fail "R passthrough flags" + found=0 + for x in "${PACMAN_EXTRA[@]}"; do + [[ $x == --ignore ]] && found=1 + done + [[ $found -eq 1 ]] || fail "PACMAN_EXTRA missing --ignore: ${PACMAN_EXTRA[*]}" + [[ ${TARGETS[0]} == linux && ${TARGETS[1]} == foo ]] || fail "targets: ${TARGETS[*]}" + pass "parse_args -R --ignore passthrough tokens" +} + +# --- pac_noconfirm_args / confirm_ask --- +{ + MIAUR_NOCONFIRM=0 + mapfile -t c < <(pac_noconfirm_args) + [[ ${#c[@]} -eq 0 ]] || fail "expected no noconfirm by default" + MIAUR_NOCONFIRM=1 + mapfile -t c < <(pac_noconfirm_args) + [[ ${#c[@]} -eq 1 && ${c[0]} == --noconfirm ]] || fail "noconfirm args: ${c[*]}" + pass "pac_noconfirm_args" +} + +{ + MIAUR_NOCONFIRM=1 + confirm_ask "should skip" || fail "confirm_ask should succeed with noconfirm" + pass "confirm_ask respects noconfirm" +} + +# --- install.sh resolve_src: process substitution must not cd-fail --- +{ + rs_out=$( + bash <( + # shellcheck disable=SC2016 + sed -n '/^resolve_src()/,/^}/p' "$ROOT/install.sh" + cat <<'INNER' +REPO_URL=https://example.invalid/miaur.git +BRANCH=main +die() { echo "die:$*" >&2; exit 1; } +git() { echo "git-called"; exit 0; } +resolve_src +echo "SRC=${SRC:-}" +INNER + ) 2>&1 + ) || true + [[ $rs_out != *'No such file or directory'* ]] || fail "process substitution still cd-fails: $rs_out" + [[ $rs_out == *'Cloning'* || $rs_out == *'git-called'* || $rs_out == *'die:'* ]] \ + || fail "unexpected resolve_src output: $rs_out" + pass "resolve_src survives process substitution" +} + +# --- self-update CLI --- +# shellcheck source=lib/self_update.sh +source "$ROOT/lib/self_update.sh" + +{ + OP= + MIAUR_NOCONFIRM=0 + MIAUR_REPO=https://example.invalid/miaur.git + MIAUR_BRANCH=main + TARGETS=() + PACMAN_EXTRA=() + parse_args self-update --noconfirm --repo-url https://example.com/fork.git --branch dev + [[ $OP == selfupdate ]] || fail "OP selfupdate" + [[ $MIAUR_NOCONFIRM -eq 1 ]] || fail "noconfirm for self-update" + [[ $MIAUR_REPO == https://example.com/fork.git ]] || fail "repo url: $MIAUR_REPO" + [[ $MIAUR_BRANCH == dev ]] || fail "branch: $MIAUR_BRANCH" + pass "parse_args self-update options" +} + +{ + OP= + parse_args --self-update + [[ $OP == selfupdate ]] || fail "OP from --self-update" + pass "parse_args --self-update" +} + +{ + HOME=/tmp/miaur-home-test + MIAUR_HOME=/tmp/miaur-home-test/.local/share/miaur + info=$(miaur_detect_prefix) + [[ $info == "user|/tmp/miaur-home-test/.local" ]] || fail "user prefix detect: $info" + MIAUR_HOME=/usr/local/share/miaur + info=$(miaur_detect_prefix) + [[ $info == "system|/usr/local" ]] || fail "system prefix detect: $info" + pass "miaur_detect_prefix" +} + +{ + ver=$(_miaur_read_version_file "$ROOT/lib/common.sh") + [[ $ver == "$MIAUR_VERSION" ]] || fail "version read: $ver vs $MIAUR_VERSION" + pass "_miaur_read_version_file" +} + +# Mocked self-update install into a temp HOME +{ + fake_home=$(mktemp -d) + fake_src=$(mktemp -d) + mkdir -p "$fake_src/lib" "$fake_src/completions" "$fake_src/config" + printf '%s\n' '#!/usr/bin/env bash' 'echo miaur-fake' >"$fake_src/miaur" + chmod +x "$fake_src/miaur" + printf '%s\n' 'MIAUR_VERSION="9.9.9"' >"$fake_src/lib/common.sh" + printf '%s\n' '# fake' >"$fake_src/completions/miaur.bash" + printf '%s\n' '# fake' >"$fake_src/config/miaur.conf.example" + + HOME=$fake_home + bin=$(_miaur_install_tree user "$fake_home/.local" "$fake_src") + [[ -x $bin ]] || fail "bin missing: $bin" + [[ -f $fake_home/.local/share/miaur/lib/common.sh ]] || fail "lib not installed" + grep -q '9.9.9' "$fake_home/.local/share/miaur/lib/common.sh" || fail "version file not copied" + pass "_miaur_install_tree user mode" + rm -rf "$fake_home" "$fake_src" +} + +echo "All tests passed."