ush is an experimental Rust shell with a simple idea:
- Be POSIX-first. Existing shell scripts and one-liners should keep working.
- Keep stdio Unix-simple by default, but allow richer human-facing output with
--stylish. - Add a modern
.ushlanguage, but compile it to portableshinstead of inventing a separate runtime. - Make the interactive shell feel fast, lightweight, and pleasant to edit in.
Warning
ush is still an early WIP / prototype.
Expect rough edges, missing POSIX coverage, incomplete language features, and breaking changes.
It is not yet a production shell replacement.
ush is split into two layers:
ush_shell: an interactive shell runtime with POSIX-friendly execution, REPL UX, stylish mode, and structured helper pipelinesush_compiler: a.ushtoshcompiler, where scripts stay portable because execution still happens through POSIXsh
That means the project can explore a more modern shell language without giving up the portability and compatibility of sh.
The no_std target is only the compiler core. The app binary and interactive shell runtime are still intentionally std-based because they need processes, terminals, files, and OS integration.
Language direction:
- aim for a real small language closer to MoonBit in seriousness than to shell macros
- keep the everyday feel somewhere between Rust, Zig, and Go
- prefer strong data modeling and a practical stdlib over clever shell-only tricks
- keep POSIX
shas the lowered runtime contract
This repository is an MVP focused on architecture and interaction experiments.
Implemented today:
- Interactive shell REPL in Rust
-ccommand execution, including multi-line command strings- POSIX session shapes:
-i,-l, and the--prefixedargv[0]that terminal emulators use to mark a login shell - Startup tiers: an
env.sh/~/.ush_envfile read on every invocation (the.zshenvtier), a login profile that also picks up/etc/profile, and an interactive rc file &&,||,;, and newline-separated command lists run insideush, so builtins, helpers, and aliases work in a chained line.sh/ POSIX scripts executed through/bin/sh.ushscripts compiled toshand then executed by/bin/sh- Generated
.ushoutput stays within POSIXshsyntax and POSIX command usage - An effect system modelled on Effekt: an effect row after the return type (
-> String / { net, log }) saying what a function needs from its caller, inferred for every function whether or not it is written;effectdeclarations,doto perform one, andtry … withhandlers that discharge it; six built-in rows (io,fs,env,net,exec,task) inferred from the stdlib; andush effectsto list them ush explainto map a/bin/shline number from a failing generated script back to the.ushline behind it- Prototype typed language features:
type { ... }, enums, traits, markerimpl,match, typedfn, Zig-style error signatures likeProblem!String, and Rust-like?propagation - Rust-like tail expressions in value-returning functions, where the final expression returns and
;keeps it as a statement - Rust-like
std::module::functionpaths plususeimports for selected std helpers, includingstd::env,std::path,std::fs,std::command,std::string,std::http, andstd::regexwith capture support - Labeled function arguments plus parameter attributes such as
#[default(...)]and#[alias("n")] alias name = "..."declarations in.ushbin.ushas a generated CLI entrypoint, with flags/defaults/completion derived from thebin(...)signaturecrates/ush_compilerbuilds asno_std + allocwith CompactString, SmallVec, bumpalo, memchr, phf, and Fx-hashed maps in the core pathapps/ushandcrates/ush_shellremainstd-based by design- Installer patterns such as
curl -fsSL https://... | share detected from the parsed pipeline and executed through POSIX/bin/sh .ushinline shell escapes via$ command ..., alongsideshell exprfor dynamic command strings- Prompt templates from
USH_PROMPT,PS1, orshell.prompt, with the familiarPS1escapes plus\g(git branch) and\?(last status) - Builtins:
:,.,cd,pwd,echo,true,false,alias,unalias,jobs,wait,disown,fg,bg,port,stop,history,export,unset,confirm,input,select,env,command,which,type,test,[,help,source,rm - Login/profile startup loading via
--login,--profile-file,--rc-file, legacy~/.ush_profile/~/.ushrc, and rc defaults from~/.config/ush/.config.ushor~/.config.ush - Builtin utility:
sammaryfor recursive file and type summaries across paths and globs, with lockfiles excluded by default - Safety prompt for dangerous recursive
rmunless--yesorUSH_INTERACTION=false - Stylish renderers for
pwd,ls,cat,ps, andkill - Stylish command introspection for
which,type,command -v, andcommand -V, withwhichlisting every candidate and marking the oneushwill run - Structured helpers:
len,lines,json,xml,html,car,cdr,head,tail,take,drop,nth,enumerate,swap,fst,snd,frev,fsort,funiq,fjoin,map,fmap,flat,ffmap,fzip,each,filter,ffilter,any,fany,some,fsome - Environment-variable expansion,
~expansion, and simple glob expansion - Criterion benchmark skeleton for parser/profiling work
- GitHub Releases,
curlinstaller,nix, and Docker packaging entry points - Emacs-style and opt-in Vi-style REPL editing modes
ush formatandush checkcommands for formatter and typechecking passesush_lspwith document formatting, diagnostics, and semantic tokens for editor integration
Not there yet:
- Full native POSIX grammar coverage inside the Rust runtime
- Richer typed structured values beyond text / JSON helpers
- Broader language features such as HM inference, generics, HKT, modules,
yield, and real green-thread scheduling - Effect polymorphism, and multi-operation effect interfaces; handlers are tail-resumptive, so there is no
resumeto capture a continuation with - Inherent
impl Type { ... }methods and a Rust-complete type system; the current prototype is still a small subset - A truly finished shell UX; editing, completion, and IME behavior are still being tuned
Workspace layout:
apps/ush: CLI binarycrates/ush_config: config loading and runtime pathscrates/ush_compiler:.ushtoshcompiler core,no_std + alloccapablecrates/ush_shell: interactive shell, parser, stylish I/O, helperscrates/ush_tooling: formatter, diagnostics, and semantic token generationapps/ush_lsp: stdio LSP server for editors
Quick install:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh
exec "$SHELL" -l
ush --versionThe installer tries to stay zero-config:
- it installs into the first writable personal bin directory already on your
PATH - otherwise it falls back to
~/.local/bin - if that directory is not on your
PATH, it appends the export line to~/.zshrc,~/.bashrc, or~/.profile
If you want an explicit location instead:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh -s -- --bin-dir "$HOME/.local/bin"nix profile install ... and the Docker image already expose ush on PATH.
cargo run -p ushRun a one-liner:
cargo run -p ush -- -c 'printf "a\nb\n" | len'Kill the process that is listening on a port:
cargo run -p ush -- -c 'port 3341 | stop'Inspect command resolution order and see which candidate is current:
cargo run -p ush -- -s -c 'which echo'Enable stylish mode:
cargo run -p ush -- -s -c 'ls crates'Force stylish mode globally:
export USH_STYLISH=trueOpt into the Vi-style REPL keymap, which is useful in environments such as Codex Desktop where Cmd shortcuts may be intercepted before they reach the shell:
export USH_KEYMAP=viDisable interactive confirmations:
export USH_INTERACTION=falseSet the prompt from a startup file, with PS1 escapes:
export PS1='\[\e[36m\]\W\[\e[0m\] \g \$ 'Load startup files explicitly:
cargo run -p ush -- --login
cargo run -p ush -- --env-file ~/.ush_env -c 'echo $PATH'
cargo run -p ush -- --profile-file ~/.config/ush/profile.sh -c 'echo $PWD'
cargo run -p ush -- --rc-file ~/.config.ushush reads three tiers, in this order:
| Tier | Read when | Default lookup |
|---|---|---|
| env | always, including ush -c ... |
~/.config/ush/env.sh, ~/.ush_env |
| profile | login shells (-l, or a - in argv[0]) |
/etc/profile, ~/.config/ush/profile.sh, ~/.ush_profile |
| rc | interactive shells (-i, or the bare REPL) |
~/.config/ush/rc.sh, ~/.ushrc, ~/.config.ush, ~/.config/ush/.config.ush |
Anything that has to exist for every invocation — PATH above all —
belongs in the env tier, the same way it would go in ~/.zshenv. Each
tier can be skipped with --no-env / --no-profile / --no-rc.
See what a .ush script touches, and map a shell error back to its source:
cargo run -p ush -- effects examples/effects.ush
cargo run -p ush -- explain examples/hello.ush 452The REPL is tuned around rustyline's Emacs mode with extra bindings for shell-heavy navigation:
Ctrl-A/Ctrl-E: jump to line start/endCtrl-C: interrupt the current prompt or child command and return control toushCtrl-L: clear the screenCtrl-P/Ctrl-N: previous and next history entryCtrl-U/Ctrl-K: kill to the line start/endCtrl-W: kill the previous shell wordUp/Down: move through an open completion menu, or through history when no menu is showingRight/End: accept the highlighted completion candidateShift-Left/Shift-Right: extend a visible character selectionShift-Up/Shift-Down: behaves like normal history movement when the terminal forwards those keysOption-Up/Option-Down: prefix history searchOption-Left/Option-Right: word-wise cursor movementOption-Shift-Left/Option-Shift-Right: extend selection word-by-wordCtrl-Left/Ctrl-Right: word-wise movement on terminals that send control-arrow escapesCtrl-Shift-Up/Ctrl-Shift-Down: select to the line start/end on terminals that map document-edge shortcuts thereCtrl-Alt-Shift-Left/Ctrl-Alt-Shift-Right: extend selection across big shell tokensCtrl-Alt-Shift-Up/Ctrl-Alt-Shift-Down: extra line-edge selection aliases for macOS terminal mappingsHome/End: jump to line start/end, andShift-Home/Shift-Endselects to the edgeCmd-Left/Cmd-Right: jump to line start/end when the terminal forwards them asSupercursor keysCmd-Shift-Left/Cmd-Shift-Right: extend selection to the line edges when the terminal forwardsSuper+Shift, andCmd-Shift-Up/Cmd-Shift-Downmap to the same line-edge selection inside the single-line REPL
When a selection is active, typing replaces it and Backspace / Delete / Ctrl-W / Ctrl-U / Ctrl-K remove it, so keyboard-only editing feels closer to a native text field even inside the terminal.
Tab completion is context-aware instead of just dumping filesystem entries. In particular, git commands now complete subcommands, common flags, branch/tag/remote/stash names, recent commits, and pathspecs relative to the current shell directory, while the inline hint shows a short usage reminder for the argument you are typing.
If you opt into USH_KEYMAP=vi or shell.keymap = "vi", ush switches the REPL to rustyline's Vi editing mode instead. That is the recommended workaround for Codex Desktop, where Cmd-modified keys are often intercepted by the host app before the shell can read them.
ush keeps normal Unix pipes, but helper stages can operate on structured values:
printf "alpha\nbeta\ngamma\n" | filter(\it -> contains(it, "a")) | len
printf "hello\nworld\n" | map(\it -> upper(it))
printf "hello\nworld\n" | fmap(\it -> upper(it))
printf "hello\n" | map(\line -> { upper(line) })
printf "alpha\nbeta\ngamma\n" | car
printf "alpha\nbeta\ngamma\n" | cdr
printf "alpha\nbeta\ngamma\n" | take(2)
printf "alpha\nbeta\ngamma\n" | drop(1)
printf "alpha\nbeta\ngamma\n" | nth(1)
printf "alpha\nbeta\ngamma\n" | enumerate(1)
printf "beta\nalpha\nbeta\n" | fsort | funiq | fjoin(",")
printf "alpha\nbeta\ngamma\n" | flat(\head, rest -> [head, "tail", rest])
printf "alpha\nbeta\n" | fzip(["1", "2"])
printf "alpha\nbeta\n" | fzip(["1", "2"]) | swap
printf "alpha\nbeta\n" | fzip(["1", "2"]) | fst
printf "alpha\nbeta\n" | fzip(["1", "2"]) | snd
cat package.json | json | len
cat feed.xml | xml
curl -fsSL https://example.com | htmlCurrently supported helper forms:
lenlength(compatibility alias)linesjsonxmlhtmlcarcdrheadtailtake(2)drop(1)nth(1)enumerate(1)swapfstsndfrevfsortfuniqfjoin(",")map(\it -> upper(it))fmap(\it -> upper(it))map(\it -> lower(it))map(\it -> trim(it))map(\it -> replace(it, "from", "to"))flat(\head, rest -> [head, rest])ffmap(\head, rest -> [head, rest])fzip(["left", "right"])each(\it -> print(it))filter(\it -> contains(it, "foo"))ffilter(\it -> contains(it, "foo"))filter(\it -> starts_with(it, "foo"))filter(\it -> ends_with(it, "foo"))any(\it -> contains(it, "foo"))fany(\it -> contains(it, "foo"))some(\it -> contains(it, "foo"))fsome(\it -> contains(it, "foo"))
html writes the current stream into a temporary HTML file and opens it in your default browser.
If json cannot parse the stream, ush falls back to this browser flow instead of failing immediately.
xml pretty-prints valid XML and falls back to the same browser flow if the input is not valid XML.
car and cdr are Lisp-style head and tail helpers over the current line stream.
head and tail are plain aliases for car and cdr.
take, drop, nth, and enumerate are Rust-style line-stream helpers, with nth using zero-based indexing.
fst and snd project the first and second fields from tab-separated pair streams such as fzip(...).
swap flips those tab-separated pair streams.
frev, fsort, and funiq reverse, lexicographically sort, and de-duplicate line streams.
fjoin("...") collapses the current line stream into one line using a literal delimiter.
flat is a small stream-level flat-map that binds head and rest, where rest splices the remaining lines into the output list.
fmap, ffmap, ffilter, fany, and fsome are functional aliases for the corresponding helpers.
fzip zips the current line stream against a literal right-hand list or multiline string and emits tab-separated pairs.
Helper lambdas also accept \name -> expr, \name -> { expr }, zero-arg forms like \-> { "ok" }, and two-arg flat(\head, rest -> [...]).
The shell-level -s / --stylish flag swaps some Unix commands into richer output without changing their names:
pwdlscatpskill
When stylish mode is off, ush stays close to classic Unix text output.
Compiled .ush scripts keep POSIX stdio and run under /bin/sh; stylish rendering is only an interactive shell feature.
Config is resolved from:
~/.config/ush/config.pkl~/.config/ush/config.json
Default shell rc loading is separate from structured config. For interactive sessions, ush now prefers ~/.config/ush/.config.ush, falls back to ~/.config.ush, and still accepts legacy rc.sh / .ushrc.
Example config.pkl:
{
shell {
stylishDefault = true
interaction = true
historySize = 10000
keymap = "vi"
prompt = "ush> "
}
aliases {
ll = "ls -la"
gs = "git status -sb"
}
}shell.prompt is a template, not a literal: $VAR is expanded, and
the PS1 escapes apply (\w, \W, \u, \h, \H, \$, \n,
\t, \e, \[, \]) alongside two of ush's own — \g for the
current git branch and \? for the last exit status. USH_PROMPT and
PS1 from the environment take priority over it, so a startup file can
set the prompt with a plain export.
Because Pkl tooling differs by version, ush tries a few pkl eval JSON output flag variants before falling back to JSON config.
Legacy ~/.config/ubsh config files and UBSHELL_* env vars are still accepted for compatibility.
.ush files are compiled to sh; execution still happens in /bin/sh.
Small example:
let greeting = "hello"
print greeting + " world"
$ printf '%s\n' from-ush
match greeting {
"hello" => print "matched"
_ => print "fallback"
}
Current highlights:
let,print,match, typedfn,enum,type, markertrait, and Rust-like tail expressions""" ... """multiline strings with common-indent dedentstd::env,std::path,std::fs,std::command, andstd::stringhelpers via fully-qualified calls or top-leveluse, plus method-style path/string flows likepath.resolve(),path.exists(),path.read_text(), andname.trim_suffix(".ush")raiseplus typed error signatures likeProblem!String, with Rust-like?propagation$ command ...for inline shell execution andshell exprfor dynamic command stringsasync/.awaitbin(...)entrypoints for generated CLI tools#|doc comments for generated--help,--man, and completion text, including std-like sections such as notes, warnings, errors, and see-also links- sectioned sourcemaps with generated-line summaries, reverse source indexes, and runtime failure mapping back to
.ushlines
Useful commands:
cargo run -p ush -- examples/hello.ush
cargo run -p ush -- scripts/bootstrap.sh --flag value
cargo run -p ush -- examples/control_flow.ush
cargo run -p ush -- compile examples/hello.ush
cargo run -p ush -- test examples/smoke_test.ush
cargo run -p ush -- compile examples/hello.ush --sourcemap /tmp/hello.sh.map.json
cargo run -p ush -- format examples/hello.ush --stdout
cargo run -p ush -- check examples/hello.ush
cargo run -p ush_lsp
cargo run -p ush -- examples/std_modules.ush
cargo run -p ush -- examples/http_regex.ush
cargo run -p ush -- -c "sammary 'crates/ush_shell/src'"
cargo run -p ush -- -c "sammary --include-lock .".ush files are compiled and then executed by /bin/sh; .sh files are forwarded straight to /bin/sh with their arguments.
Start here for more detail:
docs/language-vision.mdfor the language design target and ergonomics directionexamples/README.mdfor runnable samplesdocs/README.mdfor guide indexdocs/sourcemaps.mdfor the sourcemap JSON format, sections, reverse lookup, and runtime diagnosticsdocs/typed-errors.mdfor a step-by-step walkthrough ofProblem!T,raise, inferred# raises:,?, and external-commandunknowndocs/lsp.mdfor editor integration withush_lsp
install.sh downloads the matching GitHub Releases archive and installs ush plus ush_lsp.
By default it picks the first writable personal bin directory already on PATH.
If none is available, it falls back to ~/.local/bin and updates your shell rc automatically on POSIX shells.
It refuses to install unless it can verify the archive against the release sha256sums.txt with sha256sum, shasum, openssl, or python3.
Release archives are currently published for:
- macOS
x86_64 - macOS
aarch64 - Linux
x86_64-unknown-linux-gnu - Linux
aarch64-unknown-linux-gnu
The Linux archives are built on ubuntu-latest (x86_64) and
ubuntu-24.04-arm (aarch64), so the glibc baseline is whatever the
GitHub Actions image ships at the time of the build. Distributions
older than the runner image's glibc require building from source.
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | shPin a release version:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh -s -- --version v0.6.0Install into a custom bin directory:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh -s -- --bin-dir "$HOME/.ush/bin"Skip automatic PATH updates:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh -s -- --no-modify-pathOverride the checksum manifest URL:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh -s -- --checksum-url https://example.com/sha256sums.txtnix profile install github:ubugeeei/ushIf your platform does not have a prebuilt release archive yet, build from source:
cargo build --release -p ush -p ush_lsp
mkdir -p "$HOME/.local/bin"
install -m 755 target/release/ush "$HOME/.local/bin/ush"
install -m 755 target/release/ush_lsp "$HOME/.local/bin/ush_lsp"The Docker image is meant to be a distribution target and base image: a small environment where ush is already installed and ready to use.
docker build -t ush .
docker run --rm -it ushUse it as a base image:
FROM ush
RUN printf "a\nb\n" | len
CMD ["ush"]cargo bench -p ush_shellThe parser suite covers the native fast path, the POSIX fallback
path, alias expansion, and helper pipelines; the compiler suite
covers an empty program (the fixed runtime prelude), a small
program, and an ADT-heavy one. CI compares every benchmark against
the main baseline and fails a PR on a regression above 25%.
GitHub Actions provides two release paths:
- Push a
v*tag to run the release pipeline directly - Run
Cut Releasefrom the Actions tab to create and push a tag, then call the same release pipeline
Cut Release asks for a version like v0.2.0 and a target ref such as main.
See docs/release-process.md for the full pre-flight checklist, the matrix of published artefacts, and the rollback procedure.
GitHub Actions runs formatting, the Rust 250-line file limit check, workspace tests, release tests, bench build checks, and the .ush async / ADT examples on every pull request and push to main.
A separate Dependencies workflow watches vendor/rustyline against crates.io on a weekly cron via scripts/check_rustyline_upstream.sh and fails when the pinned tag in vendor/rustyline/UPSTREAM is behind upstream, so security advisories surface even between human reviews.
ush is still a pre-1.0 prototype (see the warning at the top of this file), but the 0.7.0 line ships with a deliberately strict CI / supply-chain posture so a production-style deployment is feasible inside the supported scope:
- Correctness gates —
cargo test --workspaceon Ubuntu and macOS,cargo test --releaseon every published target (Linuxx86_64/aarch64, macOSx86_64/aarch64),no_stdtest suite, MSRV gate (currently 1.88), rustdoc with-D warnings, andush check+ush format --checkover the committed example corpus. - Static analysis — clippy
-D warnings, workspace-wide lints denyingtodo!()/dbg!()/unimplemented!()/unused_must_use, plus CodeQL (Rust,security-and-qualityquery pack) on every PR and a weekly cron. - Supply chain —
cargo audit,cargo deny(licenses / sources / advisories / bans), weekly Gitleaks secret scan, a vendored-rustylinedrift watcher, and Dependabot subscribed to GitHub Actions updates so action versions never silently age. - Runtime hardening — signal helpers use
sigaction(2)with checkedpid_tcasts; the compiler enforcesmatchexhaustiveness at the effects pass;install.shpins HTTPS + TLS 1.2, setsumask 077, and best-effortset -o pipefail; the docker image runs as a non-root user. - Release pipeline — tag push (or the
Cut Releaseworkflow) triggers preflight (tag matchesCargo.toml), per-target build + test + attestation, installer smoke-test against the freshly-built archive, and a GitHub Release withsha256sums.txt. Procedure is documented indocs/release-process.md. - Performance gate — parser and
.ush → shcompile benchmarks are tracked against themainbaseline ongh-pages; PRs fail when a microbench regresses by more than 25%.
See CONTRIBUTING.md for the local-CI flow, workspace layout, and PR conventions. The full list of user-visible changes lives in CHANGELOG.md. Security reports go through the private channel described in SECURITY.md — please do not open a public issue for them.
MIT