-
Notifications
You must be signed in to change notification settings - Fork 2
Home
Bash compatibility layer for fish shell. Paste bash, it just works.
- Three-tier translation: keyword wrappers → AST translation → bash passthrough
- 299/299 bash constructs passing
-
reef on/off,reef display,reef history - Auto
.bashrcsourcing - AUR package
- Native
(( i++ )),(( sum += n )),(( x = 10 ))translation to fishmath - Standalone
(( ))detection added - Shipped from r/fishshell feedback same day
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 strlifetimes 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 properstd::error::Errortrait -
shell_escapereturnsCow<'_, str>— zero allocation for common paths -
shell_escape_for_bashrewritten with byte-level iteration -
itoa()helper writes integers directly into output buffer (avoidsformat!("{}", n)) -
String::with_capacitypre-sizing (13 call sites across passthrough, env_diff, and translator)
Architecture Cleanup
- Thread-local
Cell<bool>+Cell<u32>replaced with explicitCtxstruct threaded through 43 emit functions - Subshell-aware scoping:
exit→return,set→set -linside 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...elsefor 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 behindhas_keyword_charflag, skipped entirely for simple input - Brace range detection skips quoted sections
- Single-quote skipping prevents false positives on strings containing bash keywords
- Sorted
SKIP_VARSwith 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
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 namedreef)
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 -
Hashderive onParamandParseError -
# Panicsdoc sections on functions with.expect() -
# Examplesdoc tests on all public entry points (compile-verified) -
EnvSnapshot::diff_into()— zero intermediate allocations - 498 unit tests + 11 doc tests
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) |
- Fish completions for
reefsubcommands - homebrew-core submission (needs 225+ stars or 90+ forks/watchers)
- nixpkgs v0.3.0 update (PR open)
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).
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-freeFound 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.