From 58dca596084164792d8966520408ddc098b151f1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 14 Jun 2026 16:14:15 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20zsh=20support=20=E2=80=94=20run=20u?= =?UTF-8?q?tilities=20and=20tests=20under=20zsh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xsh runs imported utilities under zsh's ksh emulation. The hub/* utilities are nearly portable already; the one fix: - hub/account-for-org: `read -ra` (array read) is bash-only — zsh's read has no -a (and -A differs). Replaced with a comma->space substitution + word split, which is portable (org names never contain spaces). The other utilities (account-for-email, account-for-repo, run) and the ssh wrapper need no changes: they use no array-read, no FUNCNAME, no ${!var} indirection, and their nested xsh calls are already captured in $() so the nested getopts can't corrupt the caller's OPTIND. test.sh now self-sources ~/.xshrc when xsh isn't already a function, so it runs as a child of either shell (under bash xsh is inherited as an exported function; zsh can't export functions). CI ci-unittest.yml gains a `shell: [bash, zsh]` matrix dimension (installs zsh on Linux); the zsh jobs are continue-on-error until a zsh-supporting xsh is released to master. Verified: all 25 test.sh assertions pass under both zsh 5.9 and macOS bash 3.2.57. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci-unittest.yml | 20 ++++++++++++++++++-- README.md | 3 +++ functions/hub/account-for-org.sh | 7 +++++-- test.sh | 9 +++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-unittest.yml b/.github/workflows/ci-unittest.yml index 70acee1..db950c3 100644 --- a/.github/workflows/ci-unittest.yml +++ b/.github/workflows/ci-unittest.yml @@ -5,14 +5,27 @@ on: [push, pull_request] jobs: test: runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} / ${{ matrix.shell }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] + # bash is the historical target; zsh is the macOS default login shell. + # Utilities run under zsh's ksh emulation (provided by xsh). + shell: [bash, zsh] + # The zsh jobs install xsh from alexzhangs/xsh master, which doesn't yet + # carry zsh support — so they stay red until that release lands. Keep them + # non-blocking until then. TODO: remove once a zsh-supporting xsh is + # released to master. + continue-on-error: ${{ matrix.shell == 'zsh' }} steps: - uses: actions/checkout@v4 + - name: Install zsh (Linux) + if: matrix.shell == 'zsh' && runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y zsh + - name: Install xsh run: | git clone --depth=50 https://github.com/alexzhangs/xsh.git xsh-source @@ -30,10 +43,13 @@ jobs: BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" xsh load -b "$BRANCH" xsh-lib/git || xsh load xsh-lib/git - - name: Run tests + - name: Run tests (${{ matrix.shell }}) run: | # shellcheck disable=SC1090 source ~/.xshrc xsh version xsh list - bash test.sh + # test.sh self-sources ~/.xshrc, so running it under the matrix shell + # makes the utilities execute under that shell (bash, or zsh's ksh + # emulation). + ${{ matrix.shell }} test.sh diff --git a/README.md b/README.md index b85cb6a..ae3bca2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ Tested with bash: * 5.x and 4.x on Linux (ubuntu-latest in CI) * 3.2.57 on macOS (macos-latest in CI) +The utilities also run under **zsh** (the default shell on modern macOS); xsh +executes them under zsh's ksh emulation. Tested with zsh 5.x. + This project is still at version `0.x` and should be considered immature. diff --git a/functions/hub/account-for-org.sh b/functions/hub/account-for-org.sh index d620e78..81a1f64 100644 --- a/functions/hub/account-for-org.sh +++ b/functions/hub/account-for-org.sh @@ -35,8 +35,11 @@ function account-for-org () { for record in $XSH_GIT_HUB_ACCOUNTS; do IFS=':' read -r r_account _ r_orgs_csv <<< "$record" [[ -z $r_orgs_csv ]] && continue - IFS=',' read -ra r_orgs <<< "$r_orgs_csv" - for o in "${r_orgs[@]}"; do + # split the CSV on comma. `read -a` is bash-only (zsh's read has no -a, + # and -A differs); org names never contain spaces, so replacing commas + # with spaces and word-splitting is portable across bash and zsh. + # shellcheck disable=SC2086 + for o in ${r_orgs_csv//,/ }; do if [[ $o == "$org" ]]; then printf '%s\n' "$r_account" return 0 diff --git a/test.sh b/test.sh index 17b1570..f13a8d4 100644 --- a/test.sh +++ b/test.sh @@ -15,6 +15,15 @@ # unless XSH_GIT_TEST_NETWORK=1 is set explicitly. # +# Make the `xsh` function available when run as a child process. Under bash it +# is inherited as an exported function (no-op here); zsh cannot export functions, +# so a child `zsh test.sh` sources ~/.xshrc to define xsh as a real zsh function +# (otherwise it would only see the bin/xsh shim, which runs bash). +if ! type xsh 2>/dev/null | grep -q 'function'; then + # shellcheck source=/dev/null + . ~/.xshrc +fi + set -e -o pipefail # xsh's __xsh_clean unsets XSH_DEV on every RETURN trap, so a script that makes From 83345b4be9d3969e19717429d5c51ec932db72f2 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 14 Jun 2026 18:16:34 +0800 Subject: [PATCH 2/2] ci: make zsh jobs gating now that xsh 0.7.0 is released Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci-unittest.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci-unittest.yml b/.github/workflows/ci-unittest.yml index db950c3..bce8741 100644 --- a/.github/workflows/ci-unittest.yml +++ b/.github/workflows/ci-unittest.yml @@ -13,11 +13,6 @@ jobs: # bash is the historical target; zsh is the macOS default login shell. # Utilities run under zsh's ksh emulation (provided by xsh). shell: [bash, zsh] - # The zsh jobs install xsh from alexzhangs/xsh master, which doesn't yet - # carry zsh support — so they stay red until that release lands. Keep them - # non-blocking until then. TODO: remove once a zsh-supporting xsh is - # released to master. - continue-on-error: ${{ matrix.shell == 'zsh' }} steps: - uses: actions/checkout@v4