Skip to content
ZStud edited this page Feb 24, 2026 · 12 revisions

Reef

Bash compatibility layer for fish shell. Paste bash, it just works.

Roadmap

v0.1 — Initial Release (Done)

  • Three-tier translation: keyword wrappers → AST translation → bash passthrough
  • 299/299 bash constructs passing
  • reef on/off, reef display, reef history
  • Auto .bashrc sourcing
  • AUR package

v0.1.1 — Arithmetic & Community Feedback (Done)

  • Native (( i++ )), (( sum += n )), (( x = 10 )) translation to fish math
  • Standalone (( )) detection added
  • Shipped from r/fishshell feedback same day

v0.2 — Custom Parser & Optimization Pass (Done)

Replaced conch-parser with a purpose-built recursive-descent parser. Full optimization pass across every subsystem.

Binary & Build

  • Binary: 1.18MB → 468KB (60% reduction)
  • 23 crate dependencies → 0 (clap, conch-parser, and 21 transitive crates removed)
  • conch-parser was flagged future-incompat by rustc — would break on a future Rust release
  • Release profile: opt-level 3 → "z", added panic = "abort", codegen-units = 1
  • Release compile: 6.07s → 2.65s (56% faster)
  • Edition 2024, if-let chains

Custom Parser

  • Zero-copy AST with &'a str lifetimes replaces conch-parser's String-owning nodes, zero allocations during parse
  • First-class AST variants for every bash-ism (<<<, [[ && ]], (( )), &>, brace ranges) — no more preprocessor hacks
  • Eliminated 12 deeply-nested generic type aliases (TLCmd, TLWord, AOList, etc.)
  • Eliminated 5 preprocessing rewrite passes and 6 helper functions, parser handles all constructs natively
  • Three clean layers: Lexer → AST → Parser
  • Better error messages with byte offsets (parse error at byte 42: expected 'done')

Allocation Elimination

  • 8 Vec<char> / .chars().collect() allocations eliminated, now all scanning is byte-level on &[u8]
  • 32 .to_string() / .clone() calls eliminated, zero remain in translator
  • Error types: ParseError(String) / Unsupported(String)Parse(ParseError) / Unsupported(&'static str), zero-allocation errors with proper std::error::Error trait
  • shell_escape returns Cow<'_, str> — zero allocation for common paths
  • shell_escape_for_bash rewritten with byte-level iteration
  • itoa() helper writes integers directly into output buffer (avoids format!("{}", n))
  • String::with_capacity pre-sizing (13 call sites across passthrough, env_diff, and translator)

Architecture Cleanup

  • Thread-local Cell<bool> + Cell<u32> replaced with explicit Ctx struct threaded through 43 emit functions
  • Subshell-aware scoping: exitreturn, setset -l inside subshells
  • Env snapshot no longer spawns bash, uses std::env::vars() directly (now infallible)
  • Eliminated duplicate env parsing, extracted shared diff_and_print_env
  • Hand-rolled arg parser replaces clap derive macros
  • ensure_bare() / ensure_dquoted() helpers extracted in ANSI-C quote emitter
  • Functions that can't fail don't return Result
  • Option<&T> parameters instead of &Option<T>
  • let...else for cleaner early returns, unnested or-patterns, inlined format args

Performance

  • Detect runs single byte scan for trigger characters, bails immediately for plain fish commands
  • Keyword .contains() checks gated behind has_keyword_char flag, skipped entirely for simple input
  • Brace range detection skips quoted sections
  • Single-quote skipping prevents false positives on strings containing bash keywords
  • Sorted SKIP_VARS with binary search (was linear scan)
  • Single-buffer stdout write in env diff (was one writeln! syscall per command)
  • Bytes over chars for all ASCII operations
  • #[inline] on hot-path functions: lexer, parser, translator

Test Coverage

  • Tests: 130 → 484 (272% increase)
  • Full coverage: arithmetic, parameter expansion (all 20+ forms), control flow, functions, redirections, arrays, traps, real-world patterns (pip, docker, npm, cargo, git, ssh, rsync, curl, nvm, conda, pyenv)
  • Zero clippy warnings (pedantic triaged: 48 fixed, 11 deliberately allowed)

Bugs Found & Fixed

  • Found and fixed _ variable leak bug during optimization
  • Fixed parser infinite loop on empty case arm bodies a) ;;skip_separators() was eating the ;; terminator
  • Fixed detection false positives on single-quoted strings containing bash keywords

v0.3 — Persistence, Confirm Mode & Library Crate (Done)

Persistence Modes

Bash state now survives across commands with two opt-in modes:

  • reef persist state — exported variables persist via state file (~0.4ms overhead). After each bash passthrough, reef saves the exported environment to a state file. The next passthrough restores it before running.
  • reef persist full — persistent bash coprocess, everything persists (~1.6ms, ~2-4MB). Spawns a long-lived bash process per fish session. All bash-detected commands route through this single process. Variables, functions, aliases, traps, fds, cwd — everything survives.
  • Automatic daemon lifecycle management (start on enable, cleanup on fish exit via trap)
  • reef persist off (default) — current behavior, zero overhead

Confirm Mode

Preview what reef will do before it executes:

  • reef confirm on — shows the fish translation (T2) or indicates bash passthrough (T3), then waits for [Y/n] confirmation
  • Works on all paths: T2 translation, T3 passthrough, daemon routes, source commands
  • Y / Enter (default) → execute, n → cancel (original command stays for editing), Ctrl-C → cancel
  • Zero overhead when off (default)

Library Crate

Reef is now both a binary and a Rust library:

  • cargo add reef-shell / use reef::translate::translate_bash_to_fish
  • Full public API: detection, parsing, AST types, translation, passthrough, env diffing, daemon control
  • All types #[non_exhaustive] for forward compatibility
  • Published on crates.io as reef-shell (binary and library both named reef)

Binary & Build

  • Binary: 468KB → ~490KB (persistence, confirm mode, library API added ~22KB)
  • Still zero crate dependencies

Code Quality Audit

  • #![forbid(unsafe_code)] — no unsafe anywhere
  • #![deny(missing_docs)] — every public item documented
  • #![warn(clippy::all)] — clippy clean
  • #[must_use] on all public functions returning values
  • #[non_exhaustive] on all public enums
  • Hash derive on Param and ParseError
  • # Panics doc sections on functions with .expect()
  • # Examples doc tests on all public entry points (compile-verified)
  • EnvSnapshot::diff_into() — zero intermediate allocations
  • 498 unit tests + 11 doc tests

v0.3 — Distribution (Done)

Reef v0.3.0 is available on every major package manager:

Channel Install Status
AUR yay -S reef Live
crates.io cargo install reef-shell Live
Homebrew brew tap ZStud/reef && brew install reef Live (homebrew-core needs 225+ stars)
Nix flake nix profile install github:ZStud/reef Live
nixpkgs nix-env -iA nixpkgs.reef v0.2.0 live, v0.3.0 PR pending
Fedora/Copr sudo dnf copr enable zstud/reef Live
Debian/Ubuntu Not viable (Ubuntu ships Rust 1.82, reef needs 1.85+ for edition 2024)

Planned

  • Fish completions for reef subcommands
  • homebrew-core submission (needs 225+ stars or 90+ forks/watchers)
  • nixpkgs v0.3.0 update (PR open)

How It Works

Reef intercepts bash syntax in fish using three tiers:

  • Tier 1 — Keyword Wrappers (<0.1ms): Fish functions handle export, unset, source, declare, local, readonly, shopt. Native fish speed.
  • Tier 2 — AST Translation (~0.4ms): Rust binary parses bash into a zero-copy AST, walks it, emits fish equivalents. Handles for/do/done, if/then/fi, while/until, case/esac, arithmetic (full C-style with bitwise ops), all 20+ parameter expansion forms, process substitution, arrays, traps, redirections, functions, here-strings, heredocs, brace ranges.
  • Tier 2.5 — Confirm Mode (optional): Preview the translation or passthrough type with [Y/n] prompt before execution.
  • Tier 3 — Bash Passthrough (~1.6ms): Anything too complex runs through bash directly. Environment changes are captured and applied back to fish.

A fish_command_not_found handler catches bash that slips past the Enter key hook, running it through the same Tier 2 → Tier 3 pipeline.

If detection finds no bash syntax, reef does nothing. Native fish commands have virtually zero overhead — detection is a single byte scan that bails on the first non-bash character.

Persistence adds two optional modes on top of this: reef persist state saves exported vars between commands, and reef persist full runs a long-lived bash coprocess where everything persists (vars, functions, fds, traps, cwd).

Contributing

Reef is a library + binary crate with compile-time enforcement:

  • #![forbid(unsafe_code)] — no unsafe anywhere
  • #![deny(missing_docs)] — every public item must be documented
  • #![warn(clippy::all)] — clippy clean
cargo test          # 498 unit tests + 11 doc tests
cargo clippy        # must be warning-free

Found a bash construct that doesn't work? Open an issue with the command and expected output. That becomes a test case and a fix.

AI was used to help write the tests and fix my horrible comments across the codebase. I also started using it to push PKGBUILDs, I'm lazy. Code is mine.