a structured-data unix shell.
fshell keeps the syntax you already type — pipes, redirections, &&, globs, job control — and moves typed values between pipeline stages instead of raw bytes. ls, ps and friends emit records whose fields you can filter and project directly; @json, @csv, @yaml and @table convert at the boundaries.
a POSIX compatibility engine is bundled for the bash/zsh scripts you already have, so migrating is optional rather than a rewrite.
status: work in progress. it is daily-drivable, but bash and zsh are still more battle-tested; the honest gaps are listed in what's rough today.
- typed pipelines — stages carry
Map,List,Int,String,Bloband friends, not text.filter size > 1K,sort cpu desc,map pid commandwork on fields; noawk/cutcolumn splitting. - your existing scripts still run —
fsh --posix script.shexecutes through the bundled POSIX engine, andsh { ... }runs POSIX code in-process against the same environment. - one binary — line editor, completions, SQLite history, git-aware prompt and themes are compiled in; nothing is sourced at startup.
- stages don't fork —
filter,map,sort,grep,count,limit,traverseandhashare language keywords evaluated in-process. - unix tools that understand structure —
ls,ps,files,z,vault,extract,serve,string,diff, plusjsonandcsvhelpers. - safety rails — catastrophic commands are blocked (or confirmed interactively), and external processes can run under a Landlock (Linux) or Seatbelt (macOS) sandbox with capability gating.
- one language for prompt and scripts — anything you type works in a
.fshfile and the other way round.
- linux or macOS
- a stable Rust toolchain (rustup.rs); the minimum supported version is 1.88
- a C compiler (the bundled SQLite and stacker builds need one; on macOS it ships with the Xcode command line tools)
the default build is minimal — parser, engine, POSIX frontend and the core builtins — and needs no libraries beyond the system toolchain. the full feature set adds the archive extraction (extract), secrets (vault), assistant (ai), http, sql, chart, notify, fuzzy filter (ff), replace and sandbox builtins, and links libarchive statically:
- ubuntu / debian:
sudo apt install clang libclang-dev pkg-config libarchive-dev liblzma-dev libzstd-dev liblz4-dev libb2-dev - macOS:
brew install pkg-config libarchive libb2 xz zstd lz4
git clone https://github.com/FraSharp/fshell
cd fshell
cargo build --release # minimal; binary at target/release/fsh
cargo install --path . # ...or install it into ~/.cargo/bin
cargo install --path . --features full # everything above, with the native packages installedthere are no prebuilt binaries yet; release archives for linux and macOS are produced by the release workflow on v* tags and carry the full feature set.
fsh_path="$(command -v fsh)" # confirm the absolute path is executable
grep -Fx "$fsh_path" /etc/shells # login tools normally require this entry
chsh -s "$fsh_path"if the path is missing from /etc/shells, add it using your system's administrator procedure first (the file is administrator-owned on macOS and most Linux distributions). sign out and back in to exercise the login path; to revert, chsh -s /bin/zsh (or another shell you know is installed).
fsh # interactive shell
fsh -c 'ps | filter cpu > 20.0 | @table' # run a pipeline and exit
fsh deploy.fsh # run a .fsh script
fsh --posix setup.sh # run an existing posix script
fsh -s # start with capability checks enabledfiltering processes by CPU and projecting fields:
# bash / zsh: spawns external processes and parses columns by position
ps aux | awk '{if ($3 > 1.0) print $2, $11, $3}' | head -n 5# fsh: typed fields, in-process stages
ps | filter cpu > 1.0 | sort cpu desc | map pid command cpu | limit 5 | @table| pid | command | cpu |
|-------|------------------------------------------------|------|
| 39063 | command-code | 14.8 |
| 4116 | /Applications/cmux.app/Contents/MacOS/cmux | 6.5 |
| 90016 | /bin/sh -c ... | 2.1 |
finding regular files larger than 1KB, sorted by size, rendered as a table:
# bash / zsh: find, xargs, awk and column
find . -maxdepth 1 -type f -size +1K | xargs ls -lh | awk '{print $9, $5}' | column -t# fsh: structured records, projected fields, table formatter
ls | filter type == "file" and size > 1K | sort size desc | limit 3 | map name type size git_status | @table| name | type | size | git_status |
|-----------------|------|---------|------------|
| test_validator | file | 1417888 | clean |
| Cargo.lock | file | 121665 | clean |
| AUDIT_REPORT.md | file | 50735 | clean |
@json reads JSON — a whole document or a line-delimited stream — into typed records, and serializes records back out. A top-level array becomes one record per element, so the stages downstream iterate it:
# bash / zsh: needs jq or python
cat requests.ndjson | jq -r 'select(.status >= 500) | .path'# fsh: parsed records are ordinary pipeline data
cat requests.ndjson | @json | filter status >= 500 | map path ms | @table| path | ms |
|------|------|
| /api | 940 |
| /db | 1520 |
the other direction works the same way — ps | filter cpu > 20.0 | @json emits one JSON object per line for downstream tools — and json selects fields with a jq-like path (json '.users[0].name', json '.items[].id').
# bash / zsh: different spelling for every format
tar -zxvf archive.tar.gz
unzip bundle.zip# fsh: one command detects the format
extract archive.tar.gz
extract bundle.zipcatastrophic commands are intercepted before they run. interactively, fshell asks for confirmation:
[!] DANGEROUS OPERATION DETECTED: rm -rf /tmp / usr/local/bin
Warning: recursive delete of '/'
Type 'yes' to proceed, or press Enter to cancel:
non-interactively (scripts, -c) the command is refused outright:
Dangerous operation 'rm -rf /tmp / usr/local/bin' (recursive delete of '/') blocked by
default safety guard. Run with 'unsafe <cmd>' or unsetopt confirm_destructive to bypass.
everyday commands run with no friction.
.fsh is a small rust-ish language; types are inferred by default and can be pinned wherever a value is bound:
let port: Int = 8080
fn deploy(service: String, port: Int) -> Bool {
echo "deploying {service} on port {port}"
return true
}
let stage = "prod"
match stage {
"prod" => {
echo "production deployment"
}
_ => {
echo "development environment"
}
}
try {
echo "not-json" | @json
} catch |err| {
echo "caught {err.code}: {err.message}"
}
cat <<EOF > out.toml
[server]
port = $port
EOF
deploy "api" 3000a few conventions worth knowing:
- declarations can pin types:
let port: Int = 8080, or a structural constraint likelet cfg: { host: String, port: Int, .. } = .... a value that does not match fails the declaration; untyped bindings infer as before. matcharms are blocks separated by newlines, not commas.catch |err| { ... }gives you the structured diagnostic.- heredocs (
<<EOF) expand$var,$(...)and$((...)); a bare{...}stays literal so JSON, SQL and config content is not mangled. use a double-quoted string when you want{expr}interpolation. 10KBis 10000 bytes,10KiBis 10240.
- completion menu — Tab opens a fuzzy (nucleo) multi-column menu with category badges:
dir,file,cmd,builtin,alias,fn,var,job,flag,pipe,keyword,history,ref. - predictive suggestions — ghost text from history, filesystem paths and command syntax; Right accepts it, Alt+Right word by word.
- sqlite history — every command is stored with its exit code, duration and directory (
~/.config/fsh/history.db). Ctrl+R searches inline, Ctrl+H opens a full-screen explorer, Alt+R restores the last cancelled command. - aliases expand as you type — a command-position alias expands when you hit the space after it, so you see the real command before running it. one Backspace puts the alias name back.
- git prompt — branch, dirty state and ahead/behind are read from git's indices without spawning
git status; the previous prompt collapses to one line after Enter. - themes — 24-bit color with palettes built on the CSS/X11, Catppuccin, Gruvbox and Nord color dictionaries, plus custom
prompt.toml.config editopens a full-screen editor for options, themes and aliases.
the full reference is in docs/WIDGETS.md.
ls— git-aware listing emitting typed records (size, permissions, git status).ps— process table with typedpid,cpu,mem,user,commandfields.files— recursive directory scanner emitting structured records (replacesfind).z/zi— SQLite-backed frecency directory jumping, with an interactive picker.serve— instant local HTTP static file server.vault— local encrypted secrets store.extract— auto-detects and extracts.tar.gz,.zip,.tar.xz,.7z, and more.string—upper,lower,trim,split,length,replace.diff— structured diff records, usable in a pipeline.json/csv— parse and format structured data.explain— explains diagnostics:explain FSH-TYPE-001, orexplain --listfor every code.ai(optional, with a configured provider) — generate commands from a description, or explain one withai --explain "...".
core stages, evaluated in-process:
| stage | description | example |
|---|---|---|
filter <expr> |
keep items matching a condition | ls | filter size > 1048576 |
map <cols...> |
project fields (a.b reaches nested fields) or compute parenthesized expressions |
ps | map pid (cpu / 100.0) |
sort [col] [asc|desc] |
sort records by field | ls | sort size desc |
grep <pattern> |
keep items matching a string or regex | cat app.log | grep "ERROR 500" |
mark <pattern> |
highlight matching rows without dropping items | cat build.log | mark "WARN" |
count |
count items into an integer | ls | filter size == 0 | count |
limit <N> |
first N items | ps | sort cpu desc | limit 5 |
traverse <edge> |
walk edges of an ObjectGraph |
deps | traverse "depends_on" |
hash [-a 256|512] |
whole-stream or per-record hashes | cat archive.tar | hash |
boundary operators convert between typed streams and text:
| operator | description |
|---|---|
@table |
auto-sized terminal table |
@bar |
horizontal bar chart |
@json |
parse or emit json |
@yaml |
emit yaml |
@csv |
parse or emit csv |
@msgpack |
emit binary messagepack |
@text |
plain text extraction |
the full reference is in docs/PIPELINES.md.
- destructive-command guard —
rm -rf /, recursive permission changes on system roots, and raw block-device writes are blocked or confirmed;unsafe <cmd>bypasses it in scripts,unsetopt confirm_destructivedisables it entirely. - sandboxing — external subprocesses can run under Linux Landlock rulesets or macOS Seatbelt (SBPL) profiles, installed in
pre_exec. - capabilities — granular capability tokens gate filesystem, network, environment and process access;
with caps(...) { ... }grants them for a scope, andfsh -sstarts strict.
details in docs/SECURITY.md.
- language reference — syntax, types, reactive cells (
$=), control flow - pipelines — stages, boundary operators, backpressure,
pipefail - architecture — crate layout and the pipeline execution model
- posix compatibility — the posix engine and its coverage
- built-ins — every builtin
- configuration —
config.toml,prompt.toml, hooks - security — capabilities, sandboxing, safety prompts
- line editor & widgets — editor, keymaps, history explorer, status bar
- migration guide — bash / zsh / fish side by side
fshell is a work in progress, and i'd rather list what isn't done than pretend otherwise:
- posix is a compatibility layer, not a drop-in bash. sourcing existing scripts mostly works, but
set -e,set -u/-xandtraparen't implemented yet, andcommand,readonlyandlocalaren't real builtins. - the sandbox and the capability system are early. external-process sandboxing falls back to doing nothing where Landlock isn't available, and capabilities stay off unless you start with
-s. vaultisn't security-audited. the crypto is hand-rolled; i wouldn't keep anything you'd be sad to lose in it yet.jsonpaths are a documented subset of jq. members, indexes (negative counts from the end) and[]iteration work; filters, pipes and slicing inside a query do not.selectandexecdo less than the docs imply.selectis an interactive picker, not a column projector, andexecruns the command as a normal job instead of replacing the shell process.$?in native scripts is unreliable right now — it can get reset before a command's arguments are expanded. the posix layer's$?is separate.
if something on this list matters to you, open an issue — contributions are more than accepted.
see CONTRIBUTING.md for setup, testing, CI and pull-request guidance.
cargo build --release # build
cargo test # unit + integration tests
cargo clippy --all-targets -- -D warnings # lint
cargo fmt --check # formatting