Skip to content

feat: zsh support — utilities + first test suite/CI - #15

Merged
alexzhangs merged 3 commits into
developfrom
feature/zsh-support
Jun 14, 2026
Merged

feat: zsh support — utilities + first test suite/CI#15
alexzhangs merged 3 commits into
developfrom
feature/zsh-support

Conversation

@alexzhangs

Copy link
Copy Markdown
Collaborator

Summary

Makes the aws library run under zsh (the default shell on modern macOS), in addition to bash 3.2+. xsh executes imported utilities under zsh's ksh emulation; this PR fixes the constructs the emulation doesn't cover and adds the library's first test suite + CI, exercising both shells.

Companion to alexzhangs/xsh#32 (framework zsh support) and xsh-lib/core#5 (core lib zsh support).

Code (function-type utilities only — scripts/ always run via bash)

  • ${!var} name-indirection → portable eval (value, ${!var+x} is-set, ${!var#prefix}, ${!arr[@]} array form): cfg/get, cfg/set, cfn/stack/create+update, cfn/vpn/cluster+config.
  • ${!PREFIX@} variable-name listing → zsh parameters association, eval-wrapped so neither shell's parser chokes on the other's syntax: cfn/vpn/config.
  • ${!#}${*: -1}; positional ${!n}${*:N:1}.
  • ${!arr[@]} index iteration over a contiguous array → counted loop: cfn/vpn/ami.
  • FUNCNAME → built from zsh funcstack where passed to x-trap-return: cfn/deploy, s3/test-upload-performance.
  • BASH_REMATCHsetopt bash_rematch: s3/uri/parser.
  • read -n N -p → zsh read -k N "?prompt" branch: ses/domain-dkim, ses/sandbox/move.
  • status local variable renamed (zsh ties status to $?, read-only): cfn/__init__, cfn/stack/list+status/wait, ses/domain-dkim+domain-identity, ses/sandbox/move, rds/access. (options locals are left as-is — under the ksh emulation a function-local declare -a options safely shadows zsh's special parameter.)
  • cfn STACK_STATUS: the sparse [code]=name array (zsh can't represent sparse arrays; bash 3.2 has no associative arrays) is rebuilt as a flat (code, name) pairs list; the derived *_STABLE/*_SERVICEABLE/… arrays are appended to (consumed by value, never by index, so equivalent).
  • s3/uri/parser OPTIND: the -s/-a/-h/-p/-k branches delegated to xsh /uri/parser directly inside the getopts loop; under zsh OPTIND is shared between ksh-emulated functions, so the nested getopts reset this loop's OPTIND and spun forever. Now captured in a subshell to isolate OPTIND. (Audited the whole lib — this was the only util with that pattern.)

Incidental fix: spt/create had a pre-existing bash syntax error (a missing line-continuation made the function body swallow its closing brace, so the util failed to source under bash at all; it's marked "untested" upstream). Fixed so the test suite can import every util.

Tests

  • New test.sh: import-smokes every function utility under the running shell, then asserts s3/uri/parser, s3/uri/translate, cfg/get (ini fixtures under a throwaway HOME), and the cfn STACK_STATUS classification. Self-sources ~/.xshrc so it runs as a child of bash or zsh; uses an assert helper + failure counter rather than set -e (many utils end in a getopts loop and return its non-zero status on success, which zsh's ERR_EXIT would trip inside $()).
  • New CI (.github/workflows/test.yml): os × {bash, zsh} matrix; loads xsh-lib/core then this library and runs test.sh.

Locally: 14/14 assertions pass and all 76 function utilities import cleanly under both zsh 5.9 and macOS bash 3.2.57.

Sequencing

The zsh CI jobs are continue-on-error for now: they install xsh from alexzhangs/xsh master and xsh-lib/core's latest stable tag, neither of which carries zsh support yet, so they stay red until those release. Remove continue-on-error once a zsh-supporting xsh + core are released.

🤖 Generated with Claude Code

alexzhangs and others added 3 commits June 14, 2026 16:09
xsh runs imported utilities under zsh's ksh emulation (`emulate -L ksh`).
Most utilities already work as-is; this fixes the constructs the emulation
doesn't cover, and adds a test suite + CI exercising both shells.

Code (function-type utilities; scripts/ always run via bash, so unaffected):
- ${!var} name-indirection (value, ${!var+x} is-set, ${!var#prefix},
  ${!arr[@]} array-form) -> portable `eval` — cfg/get, cfg/set, cfn/stack/
  create+update, cfn/vpn/cluster+config.
- ${!PREFIX@} variable-name listing -> zsh `parameters` association
  (eval-wrapped so bash never parses the zsh-only syntax) — cfn/vpn/config.
- ${!#} -> ${*: -1}; positional ${!n} -> ${*:N:1}.
- ${!arr[@]} index iteration over a contiguous array -> counted loop —
  cfn/vpn/ami.
- FUNCNAME -> built from zsh `funcstack` where passed to x-trap-return —
  cfn/deploy, s3/test-upload-performance.
- BASH_REMATCH -> `setopt bash_rematch` — s3/uri/parser.
- `read -n N -p` -> zsh `read -k N "?prompt"` branch — ses/domain-dkim,
  ses/sandbox/move.
- `status` local variable renamed (zsh ties `status` to $?, read-only) —
  cfn/__init__, cfn/stack/list+status/wait, ses/domain-dkim+domain-identity,
  ses/sandbox/move, rds/access. (`options` locals are left as-is: under the
  ksh emulation a function-local `declare -a options` safely shadows zsh's
  special parameter.)
- cfn STACK_STATUS: the sparse `[code]=name` array (zsh can't represent
  sparse arrays; bash 3.2 has no associative arrays) is rebuilt as a flat
  (code, name) pairs list; the derived *_STABLE / *_SERVICEABLE / ... arrays
  are appended to (consumed by value, never by index, so equivalent).
- s3/uri/parser: the -s/-a/-h/-p/-k branches delegated to `xsh /uri/parser`
  directly inside the getopts loop; under zsh OPTIND is shared between
  ksh-emulated functions, so the nested getopts reset this loop's OPTIND and
  spun forever. Capture the nested call in a subshell to isolate its OPTIND.

Incidental: fixed a pre-existing bash syntax error in spt/create (a missing
line-continuation made the function body swallow its closing brace, so the
util failed to source under bash; the util is marked untested upstream).

Tests:
- New test.sh: import-smokes every function utility under the running shell,
  then asserts s3/uri/parser, s3/uri/translate, cfg/get (fixtures, throwaway
  HOME), and the cfn STACK_STATUS classification. Self-sources ~/.xshrc so it
  runs as a child of bash or zsh; uses an assert helper + failure counter
  rather than `set -e` (utils ending in a getopts loop return its non-zero
  status on success, which zsh's ERR_EXIT would trip inside `$()`).
- New CI (.github/workflows/test.yml): os × {bash, zsh} matrix; loads
  xsh-lib/core then this library and runs test.sh. The zsh jobs are
  continue-on-error until a zsh-supporting xsh + xsh-lib/core are released.

Verified: 14/14 assertions pass and all 76 function utilities import cleanly
under both zsh 5.9 and macOS bash 3.2.57.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cfg/get parses ~/.aws/config via xsh-lib/core's /ini/parser. Older core
releases ship an ini/parser.awk that aborts under gawk (most Linux); fixed in
core, but this suite loads core's latest *stable tag*, which may predate the
fix. Probe /ini/parser on the fixture and SKIP (not fail) the cfg/get
assertions when it's unusable, so the suite doesn't go red on a
dependency-version mismatch. Once a core with the gawk-safe parser is released,
the assertions run automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alexzhangs
alexzhangs merged commit 0edff3f into develop Jun 14, 2026
4 checks passed
@alexzhangs
alexzhangs deleted the feature/zsh-support branch June 14, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant