cd /news/open-source/rubish-a-unix-shell-written-in-pure-… Β· home β€Ί topics β€Ί open-source β€Ί article
[ARTICLE Β· art-11074] src=github.com β†— pub= topic=open-source verified=true sentiment=↑ positive

Rubish: A Unix shell written in pure Ruby

Rubish is a UNIX shell written in pure Ruby that parses shell syntax and compiles it to Ruby code for execution by the Ruby VM. It is fully compatible with bash, allowing existing bash scripts to run without modification, while deeply integrating Ruby so users can seamlessly mix shell commands with Ruby code, blocks, iterators, and libraries. The shell also supports Ruby method call syntax for commands, Ruby iterator blocks for processing output, direct evaluation of Ruby code from the prompt, and Ruby-style function definitions with named parameters.

read10 min views23 publishedMay 23, 2026

A UNIX shell written in pure Ruby.

Shell syntax is parsed and compiled to Ruby code, then executed by the Ruby VM.

Rubish supports all the features of bash, and the shell syntax is fully compatible. You can run your existing bash scripts without modification. If you found any bash script that doesn't work in rubish, we consider it a bug, so please report it!

Rubish is not just a shell implemented in Ruby, but a shell that deeply integrates Ruby. You can seamlessly mix shell commands and Ruby code, and even use Ruby's powerful features like blocks, iterators, and libraries in your shell scripts.

brew tap amatsuda/rubish
brew install --HEAD rubish
git clone https://github.com/amatsuda/rubish.git
cd rubish
bundle install
bundle exec exe/rubish

bin/rubish

is a small bash launcher that finds a usable Ruby on its own (probes ~/.rbenv/shims/ruby

, /opt/homebrew/bin/ruby

, /usr/local/bin/ruby

, system Ruby; honors $RUBY

). Use it when bundler isn't around β€” for example as a login shell, from a .app

bundle, or anywhere PATH

may be minimal:

./bin/rubish
RUBY=/opt/homebrew/opt/ruby@3.4/bin/ruby ./bin/rubish   # explicit override

Start an interactive shell:

rubish

Run a single command:

rubish -c 'echo hello'

Run a script:

rubish script.sh

Or you can even use this as a login shell!

echo "$(which rubish)" | sudo tee -a /etc/shells
chsh -s "$(which rubish)"

Use Ruby expressions as conditions in if

, while

, and until

by wrapping them in { }

. Shell variables are automatically bound as local variables in the Ruby expression:

COUNT=5
if { count.to_i > 3 }
  echo 'count is greater than 3'
end

while { count.to_i > 0 }
  echo $COUNT
  COUNT=$((COUNT - 1))
done

Commands can be invoked using Ruby method call syntax with parentheses, in addition to the traditional UNIX style with spaces:

ls -la
ls('-la')

cat(file.txt)
grep('pattern', file.txt)

Commands can be chained with Ruby methods using dot notation, forming a pipeline. The chain has to be opened by a parenthesized call, an array literal, or a block β€” once you're in chain context, subsequent methods can be bare:

ls().sort

ls().sort.uniq

cat(file.txt).grep(/error/)

ls.select { it.end_with?('.rb') }.each { |f| puts f.upcase }

The first segment needs the parens because bare cmd.method

is ambiguous with paths and dotted filenames (./script.sh

, file.tar.gz

) β€” once ()

confirms a method-call form, the lexer knows it's safe to chain.

Ruby iterator methods (.each

, .map

, .select

, .detect

) can take blocks to process command output line by line:

ls.each { |f| puts f.upcase }
cat(file.txt).map { |line| line.strip }
ls.select { it.end_with?('.rb') }

Any line starting with a capital letter is evaluated as Ruby code directly. This means you can use Ruby classes, methods, and expressions right from the shell prompt without any special syntax:

rubish$ Time.now
=> 2025-01-01 12:00:00 +0900

rubish$ Dir.glob('*.rb').sort
=> ["Gemfile", "Rakefile"]

rubish$ ENV['HOME']
=> "/Users/you"

Multi-line blocks work too β€” rubish detects unfinished Ruby (open do

, missing end

, unterminated strings, etc.) and prompts for continuation lines, both at the interactive prompt and inside sourced rcfiles. Only single-line interactive expressions get the IRB-style => …

value printed; multi-line blocks and sourced statements run silently for their side effects.

Ruby array literals can be used directly in shell context. Rubish distinguishes them from glob patterns like [a-z]

automatically:

rubish$ [1, 2, 3].map { |x| x * x }
=> [1, 4, 9]

You can execute any Ruby code by surrounding it with a lambda expression (-> { }

):

rubish$ -> { 2 ** 10 }
=> 1024

In addition to the standard shell function syntax, rubish supports Ruby-style def...end

with named parameters and splat args:

def greet(name)
  echo "Hello, $name"
end

def log(level, *messages)
  echo "[$level] $messages"
end

greet world    # => Hello, world

Define your prompt as a Ruby function for full programmatic control. The function is called on every prompt render, so it can include dynamic content:

def rubish_prompt
  branch = `git branch --show-current 2>/dev/null`.strip
  dir = Dir.pwd.sub(ENV['HOME'], '~')
  "\e[36m#{dir}\e[0m \e[33m#{branch}\e[0m $ "
end

def rubish_right_prompt
  Time.now.strftime('%H:%M:%S')
end

You can also use the traditional PS1

/RPROMPT

variables with bash (\X

) or zsh (%X

) escape codes.

Rubish tab-completes most commands out of the box, with no per-tool setup files to install. The completer derives subcommands and flags from each tool's own --help

output, on demand:

rubish$ git <Tab>             # add  branch  checkout  clone  commit  …
rubish$ gh pr <Tab>           # checkout  close  create  diff  edit  list  …
rubish$ rails generate <Tab>  # scaffold  model  controller  channel  mailer  …
rubish$ kubectl get <Tab>     # pods  services  deployments  …
rubish$ cargo build --<Tab>   # --release  --target  --verbose  …

It walks arbitrarily deep β€” rails generate scaffold --<Tab>

parses scaffold's own flags, aws s3 cp --<Tab>

reaches AWS sub-sub-commands, and so on.

How it stays correct, fast, and side-effect-free:

  • For a list of well-known tools ( gem

,brew

,cargo

,aws

,npm

,gh

,kubectl

,pyenv

,rbenv

,launchctl

,composer

,hg

, …) rubish curates the right help invocation (gem help commands

,cargo --list

,launchctl help

, etc.), so the cleanest output is used. For anything else it falls back to<cmd> --help

then<cmd> -h

. - Each result is cached for 30 minutes; repeat <Tab>

s against the same chain are instant. - On macOS the help command runs inside a sandbox-exec

profile with no network access and writes restricted to/tmp

. Completing on an unfamiliar binary can never modify your filesystem β€”touch help

won't accidentally create a file. - When zsh has a _<cmd>

completion file in$fpath

, rubish reads it too β€” improving completion for tools whose own--help

is sparse.

Other completion features:

Inline auto-suggestion(fish style) is on by default: as you type, the most likely completion appears in dim text after the cursor β€” press<Right>

to accept it.<Tab>

opens the full popup; seeCompletion dialog colorsto style it.Abbreviated path completion(zsh style):a/c/a<Tab>

expands toapp/controllers/application_controller.rb

β€” each component matches by prefix.Slow help commands: framework CLIs likerails

boot their full app just to print--help

, which may exceed the default 5-second timeout. Bump it viaRUBISH_HELP_TIMEOUT=10

.Debugging:RUBISH_DEBUG_COMPLETION=1

prints each help-command invocation, its duration, and whether it produced parseable output on stderr.

Bash- and zsh-style programmable completion (complete -F

, compgen

, compdef

, compinit

, autoload

) is supported too, for tools where the auto-help path isn't enough. Existing completion scripts Just Work.

Rubish uses Reline's inline completion dialog (the fish-style suggestions popup) for tab-completion. Its colors are configured via Reline::Face. Drop a regular Ruby block in your rcfile:

Reline::Face.config(:completion_dialog) do |conf|
  conf.define :default,   foreground: :cyan,  background: :black
  conf.define :enhanced,  foreground: :black, background: :cyan, style: :bold
  conf.define :scrollbar, foreground: :white, background: :black
end

The three face names map to different parts of the dialog: :default

for the unselected rows, :enhanced

for the currently highlighted row, and :scrollbar

for the scrollbar (visible when results overflow the popup). Colors can be a Reline symbol (:black

, :red

, :green

, :yellow

, :blue

, :magenta

, :cyan

, :white

, plus :bright_*

/ :gray

variants) or a hex truecolor string like '#abcdef'

. :style

accepts :bold

, :faint

, :italicized

, :underlined

, :blinking

, :negative

, :concealed

, :crossed_out

β€” or an array of those for multiple effects.

This works because rubish's inline Ruby evaluation handles multi-line blocks (see Inline Ruby evaluation), so the natural do … end

form is fine in both rcfiles and at the interactive prompt β€” no need for one-line reformatting.

Slow shell initializations (e.g., rbenv init

, nvm

, pyenv

) can be deferred to a background thread using lazy_load

. The block runs immediately in the background, and its result (a string of shell code) is applied before the next prompt. This keeps shell startup instant:

lazy_load {
  `rbenv init - --no-rehash bash`
}

lazy_load {
  `nodenv init - bash`
}

Multiple lazy_load

blocks run in parallel. By the time you type your first command, they're usually done.

Running rubish -r

disables all Ruby integration features (inline evaluation, lambdas, blocks, Ruby conditions, and array literals) for executing untrusted scripts safely. Only standard shell syntax is allowed.

In addition to full Bash compatibility, rubish also supports zsh-style features:

setopt

/unsetopt

compdef

/compinit

autoload

withfpath

%X

prompt codes andRPROMPT

/RPS1

Login shells load (in order):

/etc/profile

~/.config/rubish/profile

or~/.rubish_profile

(or~/.bash_profile

/~/.bash_login

/~/.profile

)

Interactive shells load:

~/.config/rubish/config

or~/.rubishrc

(or~/.bashrc

)./.rubishrc

(project-local)

Logout:

~/.config/rubish/logout

or~/.rubish_logout

(or~/.bash_logout

)

Rubish exposes a public API so other Ruby programs (terminal emulators, IDE plugins, GUI front-ends) can drive a rubish session in-process β€” no fork+exec, no JSON serialization, just method calls. The sibling Echoes terminal emulator uses this to render syntax-highlighted prompts and decide command-execution shape ahead of time.

require 'rubish'

repl = Rubish::REPL.new(login_shell: true)

repl.run

repl.tokenize('ls | grep foo')         # => Array of Rubish::Lexer::Token (each
repl.try_parse('if true; then')        # => :ok | :incomplete | :error
repl.parse_ast('echo hi')              # => AST root, or nil on parse failure.
repl.complete_at(line: 'gi', point: 2) # => Array of completion candidates at
repl.prompt_segments                   # => Array of styled-text segments
repl.right_prompt_segments             # => same shape for the right prompt,

The default Rubish::Frontend::Tty

wraps Reline + stdin/stdout. Hosts that own their own line editor can subclass Rubish::Frontend::Base

and pass an instance into the REPL:

class MyFrontend < Rubish::Frontend::Base
  def read_line(prompt:, rprompt: nil)
  end
end

Rubish::REPL.new(frontend: MyFrontend.new).run

To run setup code in every forked child between fork()

and exec()

(e.g. to attach a per-command controlling tty so the line discipline can deliver Ctrl-C

to the child):

Rubish::Command.child_pre_exec_hook = -> {
  Process.setsid
}
Category Commands
Directory cd , pwd , pushd , popd , dirs
I/O echo , printf , read , mapfile , readarray
Variables export , declare , typeset , readonly , unset , local , shift , set
Process exit , logout , exec , kill , wait , times
Job control jobs , fg , bg , disown , suspend
Functions function , return , caller
Aliases alias , unalias
History history , fc
Execution eval , source , . , command , builtin
Testing test , [ , [[ , (( )) , let
Control break , continue , trap
Completion complete , compgen , compopt , bind
Config shopt , setopt , unsetopt
Info help , type , which , hash
Other true , false , : , getopts , umask , ulimit , enable
bundle install
bundle exec rake test

Bug reports and pull requests are welcome on GitHub at https://github.com/amatsuda/rubish.

MIT

── more in #open-source 4 stories Β· sorted by recency
── more on @rubish 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/rubish-a-unix-shell-…] indexed:0 read:10min 2026-05-23 Β· β€”