diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a8591b..3292030 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,16 +37,21 @@ jobs: Invoke-Pester -Configuration $conf linux: - name: Linux shellcheck + name: Linux shellcheck + bats runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: ShellCheck + - name: Install shellcheck + bats run: | sudo apt-get update - sudo apt-get install -y shellcheck - shellcheck linux/install.sh linux/phpvm.sh + sudo apt-get install -y shellcheck bats + + - name: ShellCheck + run: shellcheck linux/install.sh linux/phpvm.sh + + - name: Bats (offline) + run: bats tests/linux/ version-consistency: name: Version consistency diff --git a/README.md b/README.md index 228071c..ceee39b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,31 @@ phpvm hook uninstall # remove the hook `.phpvmrc` accepts a full semver (`8.3.0`), a major.minor (`8.3` — picks the highest installed patch), or a leading `v` (`v8.3.0`). Lines starting with `#` are comments. If the version is not installed locally, phpvm warns but never auto-installs. +### Auto-Switch with `.phpvmrc` (Linux) + +Same `.phpvmrc` file works on Linux — the format is identical. + +```bash +echo "8.3" > .phpvmrc +phpvm auto # one-shot switch from the current directory +``` + +For automatic switching on every `cd`, enable the shell hook: + +```bash +phpvm hook enable # writes $PHPVM_DIR/.auto-hook flag +exec $SHELL # or restart your terminal +``` + +phpvm registers the hook in the shell it detects: + +| Shell | Mechanism | +|---|---| +| zsh | `add-zsh-hook chpwd _phpvm_auto` — runs on every directory change | +| bash | `PROMPT_COMMAND="_phpvm_auto -s; ..."` — runs before every prompt | + +Manage with `phpvm hook status` / `phpvm hook disable`. Because `phpvm.sh` is already sourced into your shell rc, there is no separate file edit step — the hook activates the next time the shell loads. + ### Extension Management ```bash diff --git a/linux/install.sh b/linux/install.sh index 364a18e..752bbe5 100644 --- a/linux/install.sh +++ b/linux/install.sh @@ -6,7 +6,7 @@ set -e -PHPVM_VERSION="1.6.0" +PHPVM_VERSION="1.6.5" PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}" PHPVM_REPO="https://raw.githubusercontent.com/devhardiyanto/phpvm/main" diff --git a/linux/phpvm.sh b/linux/phpvm.sh index ce2fe44..282aaa9 100644 --- a/linux/phpvm.sh +++ b/linux/phpvm.sh @@ -10,7 +10,7 @@ # phpvm use 8.3.0 # ============================================================================== -PHPVM_VERSION="1.6.0" +PHPVM_VERSION="1.6.5" PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}" PHPVM_VERSIONS="$PHPVM_DIR/versions" PHPVM_CURRENT="$PHPVM_DIR/current" @@ -84,6 +84,107 @@ _phpvm_use_path() { export PATH="$bin:$PATH" } +# ── Auto-switch (.phpvmrc) ──────────────────────────────────────────────────── +# Walk from $1 (default $PWD) up to / looking for .phpvmrc. +# shellcheck disable=SC2120 # optional arg with PWD default - tests pass an arg. +_phpvm_find_rc() { + local dir="${1:-$PWD}" + while [[ -n "$dir" ]]; do + if [[ -f "$dir/.phpvmrc" ]]; then + echo "$dir/.phpvmrc" + return 0 + fi + [[ "$dir" == "/" ]] && return 1 + dir=$(dirname "$dir") + done + return 1 +} + +# First non-comment, non-empty line; strip inline #-comments and a leading `v`. +_phpvm_read_rc() { + local file="$1" + [[ -f "$file" ]] || return 1 + local line + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%%#*}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + if [[ -n "$line" ]]; then + echo "${line#v}" + return 0 + fi + done < "$file" + return 1 +} + +# Map an rc version onto an installed version dir. Full semver passes through +# if installed; major.minor picks the highest installed patch. +_phpvm_resolve_rc() { + local requested="$1" + [[ -z "$requested" ]] && return 1 + if [[ -d "$PHPVM_VERSIONS/$requested/bin" ]]; then + echo "$requested" + return 0 + fi + if [[ "$requested" =~ ^[0-9]+\.[0-9]+$ ]]; then + local match + match=$(find "$PHPVM_VERSIONS" -mindepth 1 -maxdepth 1 -type d -name "${requested}.*" 2>/dev/null \ + | while read -r d; do [[ -x "$d/bin/php" ]] && basename "$d"; done \ + | sort -V | tail -1) + if [[ -n "$match" ]]; then + echo "$match" + return 0 + fi + fi + return 1 +} + +# Apply .phpvmrc to current shell. Session-only PATH change; tracked via +# $PHPVM_AUTO_ACTIVE so repeat calls no-op and leaving a project cleans up. +# Flags: -s / --silent for hook usage. +_phpvm_auto() { + local silent=0 + [[ "$1" == "-s" || "$1" == "--silent" ]] && silent=1 + + local rc resolved requested + if ! rc=$(_phpvm_find_rc); then + if [[ -n "${PHPVM_AUTO_ACTIVE:-}" ]]; then + local old="$PHPVM_VERSIONS/$PHPVM_AUTO_ACTIVE/bin" + PATH=$(echo "$PATH" | tr ':' '\n' | grep -vxF "$old" | paste -sd ':') + export PATH + unset PHPVM_AUTO_ACTIVE + [[ $silent -eq 0 ]] && _dim "Cleared auto PHP (no .phpvmrc upstream)." + fi + return 0 + fi + + if ! requested=$(_phpvm_read_rc "$rc"); then + [[ $silent -eq 0 ]] && _warn "$rc is empty or comment-only." + return 0 + fi + + if ! resolved=$(_phpvm_resolve_rc "$requested"); then + [[ $silent -eq 0 ]] && { + _warn "PHP $requested (from $rc) is not installed." + _dim "Run: phpvm install $requested" + } + return 0 + fi + + [[ "${PHPVM_AUTO_ACTIVE:-}" == "$resolved" ]] && return 0 + + if [[ -n "${PHPVM_AUTO_ACTIVE:-}" ]]; then + local old="$PHPVM_VERSIONS/$PHPVM_AUTO_ACTIVE/bin" + PATH=$(echo "$PATH" | tr ':' '\n' | grep -vxF "$old" | paste -sd ':') + fi + + local new="$PHPVM_VERSIONS/$resolved/bin" + export PATH="$new:$PATH" + export PHPVM_AUTO_ACTIVE="$resolved" + if [[ $silent -eq 0 ]]; then _ok "Auto-switched to PHP $resolved (from $rc)"; fi + return 0 +} + # ── Get current version ─────────────────────────────────────────────────────── _phpvm_current_version() { if [[ -L "$PHPVM_CURRENT" ]]; then @@ -630,6 +731,12 @@ phpvm_help() { phpvm ini Open active php.ini in \$EDITOR phpvm deps Print dependency install command + AUTO-SWITCH (.phpvmrc) + phpvm auto Switch to the version named in .phpvmrc + phpvm hook enable Enable auto-switching on cd (bash/zsh) + phpvm hook disable Disable the hook + phpvm hook status Check whether the hook is enabled + SELF UPDATE phpvm upgrade Upgrade phpvm to latest version phpvm version Show current phpvm version @@ -718,6 +825,49 @@ phpvm_upgrade() { _dim "Backup of old version: $backup" } +# ============================================================================== +# AUTO-SWITCH COMMANDS (phpvm auto, phpvm hook) +# ============================================================================== +phpvm_auto() { + _phpvm_auto +} + +phpvm_hook() { + local sub="${1:-status}" + local flag="$PHPVM_DIR/.auto-hook" + case "$sub" in + enable|install) + mkdir -p "$PHPVM_DIR" + touch "$flag" + _ok "Hook enabled. Restart your shell, or run:" + _dim " source \"$PHPVM_DIR/phpvm.sh\"" + ;; + disable|uninstall|remove) + if [[ -f "$flag" ]]; then + rm -f "$flag" + _ok "Hook disabled. Restart your shell to fully unregister." + else + _warn "Hook is not enabled." + fi + ;; + status) + if [[ -f "$flag" ]]; then + _ok "Hook is enabled ($flag)" + else + _dim "Hook is disabled. Run: phpvm hook enable" + fi + ;; + *) + echo "" + echo " phpvm hook - manage the shell auto-switch hook" + echo " phpvm hook enable Enable .phpvmrc auto-switching on cd" + echo " phpvm hook disable Disable the hook" + echo " phpvm hook status Check whether the hook is enabled" + echo "" + ;; + esac +} + # ============================================================================== # ENTRY POINT # ============================================================================== @@ -738,6 +888,8 @@ phpvm() { ini) phpvm_ini ;; deps) phpvm_deps ;; ext) phpvm_ext "$@" ;; + auto) phpvm_auto ;; + hook) phpvm_hook "$@" ;; upgrade|update) phpvm_upgrade ;; version|-v) _ok "phpvm $PHPVM_VERSION" ;; help|--help) phpvm_help ;; @@ -745,7 +897,24 @@ phpvm() { esac } -# Auto-activate current version if set (on shell load) -if [[ -L "$PHPVM_CURRENT" ]]; then - _phpvm_use_path +# Tests / external sourcing can set PHPVM_NO_INIT=1 to skip the source-time +# side effects below (PATH manipulation + hook registration). +if [[ -z "${PHPVM_NO_INIT:-}" ]]; then + # Auto-activate current version if set (on shell load) + if [[ -L "$PHPVM_CURRENT" ]]; then + _phpvm_use_path + fi + + # Register .phpvmrc auto-switch hook if user opted in. + if [[ -f "$PHPVM_DIR/.auto-hook" ]]; then + if [[ -n "${ZSH_VERSION:-}" ]]; then + autoload -U add-zsh-hook 2>/dev/null && add-zsh-hook chpwd _phpvm_auto >/dev/null 2>&1 + elif [[ -n "${BASH_VERSION:-}" ]]; then + case "${PROMPT_COMMAND:-}" in + *_phpvm_auto*) ;; + *) PROMPT_COMMAND="_phpvm_auto -s${PROMPT_COMMAND:+; $PROMPT_COMMAND}" ;; + esac + fi + _phpvm_auto -s + fi fi diff --git a/tests/linux/auto.bats b/tests/linux/auto.bats new file mode 100644 index 0000000..65cbd4b --- /dev/null +++ b/tests/linux/auto.bats @@ -0,0 +1,216 @@ +#!/usr/bin/env bats +# Pester equivalent for the Linux .phpvmrc auto-switch logic. +# Run from repo root: bats tests/linux/ + +setup() { + export PHPVM_DIR="$BATS_TEST_TMPDIR/phpvm" + export PHPVM_VERSIONS="$PHPVM_DIR/versions" + export PHPVM_NO_INIT=1 + export PHPVM_NO_UPDATE_CHECK=1 + mkdir -p "$PHPVM_VERSIONS" + + # shellcheck disable=SC1091 + . "$BATS_TEST_DIRNAME/../../linux/phpvm.sh" +} + +teardown() { + unset PHPVM_AUTO_ACTIVE +} + +# ---------- _phpvm_find_rc ---------- + +@test "find_rc: returns 1 when no .phpvmrc is in the chain" { + mkdir -p "$BATS_TEST_TMPDIR/a/b/c" + run _phpvm_find_rc "$BATS_TEST_TMPDIR/a/b/c" + [ "$status" -eq 1 ] +} + +@test "find_rc: finds .phpvmrc in current dir" { + mkdir -p "$BATS_TEST_TMPDIR/p1" + echo "8.3.0" > "$BATS_TEST_TMPDIR/p1/.phpvmrc" + run _phpvm_find_rc "$BATS_TEST_TMPDIR/p1" + [ "$status" -eq 0 ] + [ "$output" = "$BATS_TEST_TMPDIR/p1/.phpvmrc" ] +} + +@test "find_rc: walks up to find .phpvmrc in a parent" { + mkdir -p "$BATS_TEST_TMPDIR/proj/src/api" + echo "7.4" > "$BATS_TEST_TMPDIR/proj/.phpvmrc" + run _phpvm_find_rc "$BATS_TEST_TMPDIR/proj/src/api" + [ "$status" -eq 0 ] + [ "$output" = "$BATS_TEST_TMPDIR/proj/.phpvmrc" ] +} + +@test "find_rc: innermost .phpvmrc wins" { + mkdir -p "$BATS_TEST_TMPDIR/ws/sub/src" + echo "8.3" > "$BATS_TEST_TMPDIR/ws/.phpvmrc" + echo "7.4" > "$BATS_TEST_TMPDIR/ws/sub/.phpvmrc" + run _phpvm_find_rc "$BATS_TEST_TMPDIR/ws/sub/src" + [ "$status" -eq 0 ] + [ "$output" = "$BATS_TEST_TMPDIR/ws/sub/.phpvmrc" ] +} + +# ---------- _phpvm_read_rc ---------- + +@test "read_rc: plain version" { + echo "8.3.0" > "$BATS_TEST_TMPDIR/rc" + run _phpvm_read_rc "$BATS_TEST_TMPDIR/rc" + [ "$status" -eq 0 ] + [ "$output" = "8.3.0" ] +} + +@test "read_rc: trims whitespace" { + printf " 8.3.0 \n" > "$BATS_TEST_TMPDIR/rc" + run _phpvm_read_rc "$BATS_TEST_TMPDIR/rc" + [ "$output" = "8.3.0" ] +} + +@test "read_rc: skips comment-only lines" { + cat > "$BATS_TEST_TMPDIR/rc" < "$BATS_TEST_TMPDIR/rc" + run _phpvm_read_rc "$BATS_TEST_TMPDIR/rc" + [ "$output" = "8.3.0" ] +} + +@test "read_rc: strips leading v" { + echo "v8.3.0" > "$BATS_TEST_TMPDIR/rc" + run _phpvm_read_rc "$BATS_TEST_TMPDIR/rc" + [ "$output" = "8.3.0" ] +} + +@test "read_rc: empty / comments-only returns 1" { + printf "# comment\n\n" > "$BATS_TEST_TMPDIR/rc" + run _phpvm_read_rc "$BATS_TEST_TMPDIR/rc" + [ "$status" -eq 1 ] +} + +# ---------- _phpvm_resolve_rc ---------- + +_install_fake() { + local v="$1" + mkdir -p "$PHPVM_VERSIONS/$v/bin" + echo '#!/bin/sh' > "$PHPVM_VERSIONS/$v/bin/php" + chmod +x "$PHPVM_VERSIONS/$v/bin/php" +} + +@test "resolve_rc: full installed semver passes through" { + _install_fake 8.3.0 + run _phpvm_resolve_rc 8.3.0 + [ "$status" -eq 0 ] + [ "$output" = "8.3.0" ] +} + +@test "resolve_rc: partial picks highest installed patch" { + _install_fake 8.3.0 + _install_fake 8.3.29 + _install_fake 8.3.10 + run _phpvm_resolve_rc 8.3 + [ "$status" -eq 0 ] + [ "$output" = "8.3.29" ] +} + +@test "resolve_rc: returns 1 when nothing matches" { + _install_fake 8.3.29 + run _phpvm_resolve_rc 5.6 + [ "$status" -eq 1 ] +} + +@test "resolve_rc: full semver not installed returns 1" { + _install_fake 8.3.29 + run _phpvm_resolve_rc 8.3.99 + [ "$status" -eq 1 ] +} + +# ---------- _phpvm_auto ---------- + +@test "auto: prepends resolved version dir to PATH" { + _install_fake 8.3.29 + mkdir -p "$BATS_TEST_TMPDIR/proj" + echo "8.3" > "$BATS_TEST_TMPDIR/proj/.phpvmrc" + + cd "$BATS_TEST_TMPDIR/proj" + _phpvm_auto -s + [ "$PHPVM_AUTO_ACTIVE" = "8.3.29" ] + case ":$PATH:" in + :"$PHPVM_VERSIONS/8.3.29/bin":*) ;; + *) printf 'PATH does not start with expected dir: %s\n' "$PATH" >&2; return 1 ;; + esac +} + +@test "auto: second call is a no-op when active matches" { + _install_fake 7.4.33 + mkdir -p "$BATS_TEST_TMPDIR/proj" + echo "7.4.33" > "$BATS_TEST_TMPDIR/proj/.phpvmrc" + + cd "$BATS_TEST_TMPDIR/proj" + _phpvm_auto -s + local before="$PATH" + _phpvm_auto -s + [ "$PATH" = "$before" ] +} + +@test "auto: removes previous prepend when switching projects" { + _install_fake 7.4.33 + _install_fake 8.3.0 + mkdir -p "$BATS_TEST_TMPDIR/a" "$BATS_TEST_TMPDIR/b" + echo "7.4.33" > "$BATS_TEST_TMPDIR/a/.phpvmrc" + echo "8.3.0" > "$BATS_TEST_TMPDIR/b/.phpvmrc" + + cd "$BATS_TEST_TMPDIR/a"; _phpvm_auto -s + cd "$BATS_TEST_TMPDIR/b"; _phpvm_auto -s + + [ "$PHPVM_AUTO_ACTIVE" = "8.3.0" ] + case ":$PATH:" in + *":$PHPVM_VERSIONS/7.4.33/bin:"*) printf 'old 7.4.33 still on PATH\n' >&2; return 1 ;; + esac + case ":$PATH:" in + :"$PHPVM_VERSIONS/8.3.0/bin":*) ;; + *) printf 'PATH does not start with 8.3.0 dir: %s\n' "$PATH" >&2; return 1 ;; + esac +} + +@test "auto: clears prepend when no .phpvmrc upstream" { + _install_fake 8.3.0 + mkdir -p "$BATS_TEST_TMPDIR/proj" "$BATS_TEST_TMPDIR/orphan" + echo "8.3.0" > "$BATS_TEST_TMPDIR/proj/.phpvmrc" + + cd "$BATS_TEST_TMPDIR/proj"; _phpvm_auto -s + cd "$BATS_TEST_TMPDIR/orphan"; _phpvm_auto -s + + [ -z "${PHPVM_AUTO_ACTIVE:-}" ] + case ":$PATH:" in + *":$PHPVM_VERSIONS/8.3.0/bin:"*) printf 'old prepend still on PATH\n' >&2; return 1 ;; + esac +} + +# ---------- phpvm hook ---------- + +@test "hook: enable creates the flag file" { + run phpvm_hook enable + [ "$status" -eq 0 ] + [ -f "$PHPVM_DIR/.auto-hook" ] +} + +@test "hook: disable removes the flag file" { + touch "$PHPVM_DIR/.auto-hook" + run phpvm_hook disable + [ "$status" -eq 0 ] + [ ! -f "$PHPVM_DIR/.auto-hook" ] +} + +@test "hook: status reports correctly" { + run phpvm_hook status + [[ "$output" == *"disabled"* ]] + touch "$PHPVM_DIR/.auto-hook" + run phpvm_hook status + [[ "$output" == *"enabled"* ]] +} diff --git a/version.txt b/version.txt index dc1e644..9f05f9f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.6.0 +1.6.5 diff --git a/windows/install.ps1 b/windows/install.ps1 index 92b9b4e..559590f 100644 --- a/windows/install.ps1 +++ b/windows/install.ps1 @@ -7,7 +7,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -$PHPVM_VERSION = "1.6.0" +$PHPVM_VERSION = "1.6.5" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $PHPVM_BIN = "$PHPVM_DIR\bin" diff --git a/windows/phpvm.ps1 b/windows/phpvm.ps1 index b15b408..b5c7b6b 100644 --- a/windows/phpvm.ps1 +++ b/windows/phpvm.ps1 @@ -14,7 +14,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # -- Constants ----------------------------------------------------------------- -$PHPVM_VERSION = "1.6.0" +$PHPVM_VERSION = "1.6.5" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $VERSIONS_DIR = "$PHPVM_DIR\versions" $CURRENT_LINK = "$PHPVM_DIR\current"