ush

CI Shellcheck Dependencies License: MIT

ush is an experimental Rust shell with a simple idea:

[!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.

Concept

ush is split into two layers:

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:

Status

This repository is an MVP focused on architecture and interaction experiments.

Implemented today:

Not there yet:

Workspace layout:

Usage

Quick 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:

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 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

Interactive Editing

The REPL is tuned around rustyline’s Emacs mode with extra bindings for shell-heavy navigation:

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.

Structured Helpers

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:

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 -> [...]).

Stylish Mode

The shell-level -s / --stylish flag swaps some Unix commands into richer output without changing their names:

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

Config is resolved from:

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"
  }
}

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 Scripts

.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:

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:

Install

curl

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:

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 | 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

nix profile install github:ubugeeei/ush

source

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"

Docker

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"]

Benchmarks

cargo bench -p ush_shell

The initial benchmark targets parser + helper-pipeline shapes and is intended as a seed for more aggressive profiling.

Release

GitHub Actions provides two release paths:

Cut Release asks for a version like v0.2.0 and a target ref such as main.

CI

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.

Contributing

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.

License

MIT