ush is an experimental Rust shell with a simple idea:
--stylish..ush language, but compile it to portable sh instead of inventing a separate runtime.[!WARNING]
ushis 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 .ush to sh compiler, where scripts stay portable because execution still happens through POSIX shThat 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:
sh as the lowered runtime contractThis repository is an MVP focused on architecture and interaction experiments.
Implemented today:
-c command execution.sh / POSIX scripts executed through /bin/sh.ush scripts compiled to sh and then executed by /bin/sh.ush output stays within POSIX sh syntax and POSIX command usagetype { ... }, enums, traits, marker impl, match, typed fn, Zig-style error signatures like Problem!String, and Rust-like ? propagation; keeps it as a statementstd::module::function paths plus use imports for selected std helpers, including std::env, std::path, std::fs, std::command, std::string, std::http, and std::regex with capture support#[default(...)] and #[alias("n")]alias name = "..." declarations in .ushbin.ush as a generated CLI entrypoint, with flags/defaults/completion derived from the bin(...) signaturecrates/ush_compiler builds as no_std + alloc with CompactString, SmallVec, bumpalo, memchr, phf, and Fx-hashed maps in the core pathapps/ush and crates/ush_shell remain std-based by designcurl -fsSL https://... | sh are detected from the parsed pipeline and executed through POSIX /bin/sh.ush inline shell escapes via $ command ..., alongside shell expr for dynamic command strings:, ., 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-file, --rc-file, legacy ~/.ush_profile / ~/.ushrc, and rc defaults from ~/.config/ush/.config.ush or ~/.config.ushsammary for recursive file and type summaries across paths and globs, with lockfiles excluded by defaultrm unless --yes or USH_INTERACTION=falsepwd, ls, cat, ps, and killwhich, type, command -v, and command -V, with which listing every candidate and marking the one ush will runlen, 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~ expansion, and simple glob expansioncurl installer, nix, and Docker packaging entry pointsush format and ush check commands for formatter and typechecking passesush_lsp with document formatting, diagnostics, and semantic tokens for editor integrationNot there yet:
yield, and real green-thread schedulingimpl Type { ... } methods and a Rust-complete type system; the current prototype is still a small subsetWorkspace layout:
apps/ush: CLI binarycrates/ush_config: config loading and runtime pathscrates/ush_compiler: .ush to sh compiler core, no_std + alloc capablecrates/ush_shell: interactive shell, parser, stylish I/O, helperscrates/ush_tooling: formatter, diagnostics, and semantic token generationapps/ush_lsp: stdio LSP server for editorsQuick install:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh
exec "$SHELL" -l
ush --version
The installer tries to stay zero-config:
PATH~/.local/binPATH, it appends the export line to ~/.zshrc, ~/.bashrc, or ~/.profileIf 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 ush
Run 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=true
Opt 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=vi
Disable interactive confirmations:
export USH_INTERACTION=false
Load login/profile startup files explicitly:
cargo run -p ush -- --login
cargo run -p ush -- --profile-file ~/.config/ush/profile.sh -c 'echo $PWD'
cargo run -p ush -- --rc-file ~/.config.ush
The 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 to ushCtrl-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: previous and next history entryShift-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, and Shift-Home / Shift-End selects to the edgeCmd-Left / Cmd-Right: jump to line start/end when the terminal forwards them as Super cursor keysCmd-Shift-Left / Cmd-Shift-Right: extend selection to the line edges when the terminal forwards Super+Shift, and Cmd-Shift-Up / Cmd-Shift-Down map to the same line-edge selection inside the single-line REPLWhen 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 | html
Currently 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:
pwdlscatpskillWhen 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.jsonDefault 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"
}
}
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, typed fn, enum, type, marker trait, and Rust-like tail expressions""" ... """ multiline strings with common-indent dedentstd::env, std::path, std::fs, std::command, and std::string helpers via fully-qualified calls or top-level use, plus method-style path/string flows like path.resolve(), path.exists(), path.read_text(), and name.trim_suffix(".ush")raise plus typed error signatures like Problem!String, with Rust-like ? propagation$ command ... for inline shell execution and shell expr for 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.ush linesUseful 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.md for the language design target and ergonomics directionexamples/README.md for runnable samplesdocs/README.md for guide indexdocs/sourcemaps.md for the sourcemap JSON format, sections, reverse lookup, and runtime diagnosticsdocs/typed-errors.md for a step-by-step walkthrough of Problem!T, raise, inferred # raises:, ?, and external-command unknowndocs/lsp.md for editor integration with ush_lspinstall.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:
x86_64aarch64x86_64-unknown-linux-gnuaarch64-unknown-linux-gnuThe 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 | sh
Pin a release version:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh -s -- --version v0.6.0
Install 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-path
Override the checksum manifest URL:
curl -fsSL https://raw.githubusercontent.com/ubugeeei/ush/main/install.sh | sh -s -- --checksum-url https://example.com/sha256sums.txt
nix profile install github:ubugeeei/ush
If 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 ush
Use it as a base image:
FROM ush
RUN printf "a\nb\n" | len
CMD ["ush"]
cargo bench -p ush_shell
The initial benchmark targets parser + helper-pipeline shapes and is intended as a seed for more aggressive profiling.
GitHub Actions provides two release paths:
v* tag to run the release pipeline directlyCut Release from the Actions tab to create and push a tag, then call the same release pipelineCut Release asks for a version like v0.2.0 and a target ref such as main.
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.
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