Skip to content

Releases: jdstanhope/huck

Release list

v0.2.0

Choose a tag to compare

@jdstanhope jdstanhope released this 24 Jun 15:30

huck 0.2.0

The first substantial release since v0.1.0 — 38 numbered iterations (v174 → v211) covering the biggest architectural change in the project's history, a wave of bash-compatibility fixes from three independent corpus sweeps, and a new public embedding API.

Headline: huck is now embeddable as a library

The biggest change in 0.2.0 is that huck has been refactored into a 3-crate workspace and exposes a stable embedding API. You can now drive a huck shell session from your own Rust program — run scripts, capture output, complete tab-completion candidates, sandbox the exec — without spawning the CLI.

use huck_engine::Engine;

let mut engine = Engine::new();
let out = engine.exec("a=(x y z); echo \"${a[@]^^}\"").capture();
assert_eq!(out.stdout, "X Y Z\n");
assert_eq!(out.exit_code, 0);

The embedding crate is huck-engine; the lexer/parser/AST frontend is huck-syntax (separately consumable for shell linters and tooling — see its examples/ directory).

Embedding API surface (v203-v208)

  • huck_engine::Engine — persistent session owning Rc<RefCell<Shell>>. Methods: new / builder / from_shell_cell for construction; run / capture / run_file / run_script for execution; var / set_var / set_args / set_arg0 / last_status for state.
  • Engine::exec(src).run() / .capture() / .stdin(...) / .merge_stderr() / .cwd(...) / .restricted(true) / .timeout(d) — per-call builder for IO routing, sandboxing, and timeouts.
  • Engine::exec(src).on_stdout_line(cb).on_stderr_line(cb) — real-time streaming output callbacks; no Send bound, no drainer threads visible at the API surface.
  • Engine::complete(line, cursor) — tab-completion on the embedded session: returns Completion { start, candidates } where each Candidate is tagged with CandidateKind::{Command, Variable, File, Directory, Custom}.
  • huck_engine::Output { stdout, stderr, exit_code } — capture return type.

Crate split

Crate Role
huck-syntax Shell-free frontend: lexer, parser, command AST, source generator. No runtime dependency; usable for linters and tooling.
huck-engine Embeddable terminal-free interpreter. Owns Engine.
huck-cli REPL + rustyline line editor.
huck The binary.

huck-syntax was polished in v211 for external publication: Display + std::error::Error impls on the error types, #[non_exhaustive] on the AST enums (forward-compatible matching for downstream consumers), curated root re-exports, a try_split_assignment_ref peek variant, and a module-level doc with a doctest-checked Quick example.

Bash-compatibility expansion

Whole-array parameter-expansion modifiers (v209, v210)

${a[@]^^}, ${a[@]%.*}, ${a[@]/a/X}, ${a[@]@U}, ${a[@]@A}, ${a[@]@K}, ${a[@]@k}, ${a[@]@a} — every per-element and whole-array ${var@OP}-style operator now works across both indexed and associative arrays. v209 covers per-element ops (Case / RemovePrefix / RemoveSuffix / Substitute / per-element Transform); v210 covers whole-array ops (@A declare-style assignment string, @K/@k key/value pairs, @a attribute flags). Resolves M-127 and M-93.

declare -A m=([k]=v1 [j]=v2)
echo "${m[@]@A}"   # declare -A m=([k]="v1" [j]="v2" )
declare -irx mix=7
echo "${mix@a}"    # irx

Other bash-compat highlights

  • set -x xtrace for compound commands (v198) — for-loops, case, (( )), [[ ]], C-for loops, etc. Resolves L-21(a).
  • $"…" / $'…' quote forms outside double quotes (v181) — locale-aware identity and ANSI-C escape.
  • shopt globstar correctness (v193) — ** is single-level when off, recursive when on. Resolves M-53 (narrowed).
  • Legacy $[ expr ] arithmetic (v188) — works again, equivalent to $(( )).
  • Quoted glob/regex metachars match literally (v199-v201) — closes a quote-provenance bug family in [[ =~ ]], ${var:-W}, and ${var/pat/repl} operands.
  • coproc support (v157) — single coproc, with NAME[0]/NAME[1]/NAME_PID triple.
  • declare -p / bare declare byte-faithful to bash for associative arrays (v211 closes L-44 a+b) — bareword subscript keys when safe, trailing space on assoc body.
  • shift over-range, trap '' KILL/STOP, ${!1} on unset positional, $0 not rebound in functions, trap … 0 = EXIT, multi-line assoc-array init — runtime bugs caught and fixed.
  • kill -l <N> now covers signals 1-31 (v189).

Parser robustness

Closed roughly 70 real-script parse gaps discovered by a 4191-script corpus sweep (v174-v192): comments inside delimiters, function-name charset, function-def trailing redirects, (( arith-vs-subshell disambiguation, case inside $(), for (( C-for, $[ ], multi-line array-init line continuations, name=\<NL>(array) line continuations, redirect-on-pipeline-stage, lexer-level |& desugar, and more.

Coverage + observability

The tests/scripts/*_diff_check.sh harness count grew from ~110 to 130+ harnesses — each runs the same shell fragments through bash and huck and asserts byte-identical output. Coverage rose to 88.7% line / 88.4% region / 96.0% function via the v188-era cargo-llvm-cov sweep.

Distribution

Distribution is self-hosted off GitHub Releases:

  • Homebrew tap jdstanhope/homebrew-huck — build-from-source formula, ships for macOS via brew install jdstanhope/huck/huck.
  • .deb package attached to the GitHub release for Debian-derived Linux: dpkg -i huck_0.2.0_amd64.deb.
  • curl | sh installercurl -fsSL https://raw.githubusercontent.com/jdstanhope/huck/v0.2.0/scripts/install.sh | sh.

Quality

  • cargo test --workspace --quiet: 744 unit + ~1668 integration + ~130 harness scripts, all green.
  • cargo clippy --workspace --all-targets -- -D warnings: clean.
  • macOS portability is a standing constraint; cfg-gated platform-specifics throughout.

Compatibility notes

  • Public API additions in huck-engine (the new Engine facade) and huck-syntax (Display + Error impls, try_split_assignment_ref, root re-exports) are additive.
  • #[non_exhaustive] on AST enums in huck-syntax 0.2.0 is a one-time SemVer floor for downstream consumers — future variants won't be breaking. Downstream matches must include a _ => arm.
  • set_last_status mirror after run in Engine::run is a deliberate deviation from "exit trap fires AFTER run completes" — embedders see the correct exit code via last_status() after a successful run.

Full iteration-by-iteration history: see docs/superpowers/{specs,plans}/ in the source tree, or git log v0.1.0..v0.2.0 --grep='^Merge v' for the merge stream.

v0.1.0

Choose a tag to compare

@jdstanhope jdstanhope released this 17 Jun 01:56

huck 0.1.0