Rubish: A Unix shell written in pure Ruby
Summary
Rubish is a Unix shell written in pure Ruby that aims for full bash compatibility while deeply integrating Ruby features such as blocks, iterators, and method chaining.
View Cached Full Text
Cached at: 05/23/26, 09:29 AM
amatsuda/rubish
Source: https://github.com/amatsuda/rubish
Rubish
A UNIX shell written in pure Ruby.
Shell syntax is parsed and compiled to Ruby code, then executed by the Ruby VM.
Concept
Fully Bash-compatible
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!
Deep Ruby integration
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.
Installation
Homebrew (macOS)
brew tap amatsuda/rubish
brew install --HEAD rubish
From source
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/[email protected]/bin/ruby ./bin/rubish # explicit override
Usage
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!
Set as login shell
echo "$(which rubish)" | sudo tee -a /etc/shells
chsh -s "$(which rubish)"
Features beyond Bash
Ruby conditions
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
Ruby method call style
Commands can be invoked using Ruby method call syntax with parentheses, in addition to the traditional UNIX style with spaces:
# These are equivalent:
ls -la
ls('-la')
# Arguments can be passed as method arguments:
cat(file.txt)
grep('pattern', file.txt)
Method chaining
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:
# Equivalent to `ls | sort`
ls().sort
# Equivalent to `ls | sort | uniq`
ls().sort.uniq
# Equivalent to `cat file.txt | grep error`
cat(file.txt).grep(/error/)
# Chains can be combined with blocks (see "Ruby iterator blocks" below)
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 blocks
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') }
Inline Ruby evaluation
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"
Ruby array and regexp literals
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]
Lambda expressions
You can execute any Ruby code by surrounding it with a lambda expression (-> { }):
rubish$ -> { 2 ** 10 }
=> 1024
Ruby-style function definitions
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
Custom Ruby prompts
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.
Lazy loading
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:
# In ~/.rubishrc
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.
Restricted mode
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.
Zsh compatibility
In addition to full Bash compatibility, rubish also supports zsh-style features:
setopt/unsetoptcompdef/compinitautoloadwithfpath%Xprompt codes andRPROMPT/RPS1- Abbreviated path expansion: type
a/c/a<Tab>and it expands toapp/controllers/application_controller.rb
Configuration files
Login shells load (in order):
/etc/profile~/.config/rubish/profileor~/.rubish_profile(or~/.bash_profile/~/.bash_login/~/.profile)
Interactive shells load:
~/.config/rubish/configor~/.rubishrc(or~/.bashrc)./.rubishrc(project-local)
Logout:
~/.config/rubish/logoutor~/.rubish_logout(or~/.bash_logout)
Embedding in a Ruby program
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)
# Run interactively (default).
repl.run
# Or drive it programmatically.
repl.tokenize('ls | grep foo') # => Array of Rubish::Lexer::Token (each
# with :type and :value) for syntax
# highlighting; never raises.
repl.try_parse('if true; then') # => :ok | :incomplete | :error
# (use to decide PS2 vs. submit).
repl.parse_ast('echo hi') # => AST root, or nil on parse failure.
repl.complete_at(line: 'gi', point: 2) # => Array of completion candidates at
# the cursor.
repl.prompt_segments # => Array of styled-text segments
# {text:, fg:, bg:, bold:, italic:,
# underline:, inverse:}, ANSI codes
# already parsed.
repl.right_prompt_segments # => same shape for the right prompt,
# or nil if unset.
Custom I/O backend
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)
# ...feed input from your own UI here
end
end
Rubish::REPL.new(frontend: MyFrontend.new).run
Child-process pre-exec hook
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
# ...ioctls, signal handlers, etc.
}
Builtins
| 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 |
Development
bundle install
bundle exec rake test
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/amatsuda/rubish.
License
MIT
Similar Articles
Why Ruby Still Feels Like Home After All These Years
The author reflects on 15 years of using Ruby, praising its hidden features like refinements, delegation, and the new ZJIT JIT compiler, and notes that Ruby with ZJIT is closing the performance gap with faster languages like Go and Rust.
Zerostack – A Unix-inspired coding agent written in pure Rust
Zerostack is a Unix-inspired coding agent built entirely in Rust, designed to assist developers with code generation and automation.
From Rust to Ruby
Developer describes converting a 15,000-line Rust web application to Ruby on Rails using an LLM, finding the Ruby version significantly shorter and evaluating trade-offs in development speed, safety, and testability.
Bun's Rust rewrite has been merged
Bun, the JavaScript runtime and package manager, has merged a rewrite of its core from Zig to Rust, potentially improving performance and maintainability.
@charliermarsh: https://x.com/charliermarsh/status/2057953314419990670
Ruff is an extremely fast Python linter and code formatter written in Rust, aiming to replace multiple existing tools like Flake8, Black, and isort.