{"slug": "rubish-a-unix-shell-written-in-pure-ruby", "title": "Rubish: A Unix shell written in pure Ruby", "summary": "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.", "body_md": "A UNIX shell written in pure Ruby.\n\nShell syntax is parsed and compiled to Ruby code, then executed by the Ruby VM.\n\nRubish 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!\n\nRubish 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.\n\n```\nbrew tap amatsuda/rubish\nbrew install --HEAD rubish\ngit clone https://github.com/amatsuda/rubish.git\ncd rubish\nbundle install\nbundle exec exe/rubish\n```\n\n`bin/rubish`\n\nis a small bash launcher that finds a usable Ruby on its own (probes `~/.rbenv/shims/ruby`\n\n, `/opt/homebrew/bin/ruby`\n\n, `/usr/local/bin/ruby`\n\n, system Ruby; honors `$RUBY`\n\n). Use it when bundler isn't around — for example as a login shell, from a `.app`\n\nbundle, or anywhere `PATH`\n\nmay be minimal:\n\n```\n./bin/rubish\nRUBY=/opt/homebrew/opt/ruby@3.4/bin/ruby ./bin/rubish   # explicit override\n```\n\nStart an interactive shell:\n\n```\nrubish\n```\n\nRun a single command:\n\n```\nrubish -c 'echo hello'\n```\n\nRun a script:\n\n```\nrubish script.sh\n```\n\nOr you can even use this as a login shell!\n\n```\necho \"$(which rubish)\" | sudo tee -a /etc/shells\nchsh -s \"$(which rubish)\"\n```\n\nUse Ruby expressions as conditions in `if`\n\n, `while`\n\n, and `until`\n\nby wrapping them in `{ }`\n\n. Shell variables are automatically bound as local variables in the Ruby expression:\n\n```\nCOUNT=5\nif { count.to_i > 3 }\n  echo 'count is greater than 3'\nend\n\nwhile { count.to_i > 0 }\n  echo $COUNT\n  COUNT=$((COUNT - 1))\ndone\n```\n\nCommands can be invoked using Ruby method call syntax with parentheses, in addition to the traditional UNIX style with spaces:\n\n```\n# These are equivalent:\nls -la\nls('-la')\n\n# Arguments can be passed as method arguments:\ncat(file.txt)\ngrep('pattern', file.txt)\n```\n\nCommands 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:\n\n```\n# Equivalent to `ls | sort`\nls().sort\n\n# Equivalent to `ls | sort | uniq`\nls().sort.uniq\n\n# Equivalent to `cat file.txt | grep error`\ncat(file.txt).grep(/error/)\n\n# Chains can be combined with blocks (see \"Ruby iterator blocks\" below)\nls.select { it.end_with?('.rb') }.each { |f| puts f.upcase }\n```\n\nThe first segment needs the parens because bare `cmd.method`\n\nis ambiguous with paths and dotted filenames (`./script.sh`\n\n, `file.tar.gz`\n\n) — once `()`\n\nconfirms a method-call form, the lexer knows it's safe to chain.\n\nRuby iterator methods (`.each`\n\n, `.map`\n\n, `.select`\n\n, `.detect`\n\n) can take blocks to process command output line by line:\n\n```\nls.each { |f| puts f.upcase }\ncat(file.txt).map { |line| line.strip }\nls.select { it.end_with?('.rb') }\n```\n\nAny 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:\n\n``` js\nrubish$ Time.now\n=> 2025-01-01 12:00:00 +0900\n\nrubish$ Dir.glob('*.rb').sort\n=> [\"Gemfile\", \"Rakefile\"]\n\nrubish$ ENV['HOME']\n=> \"/Users/you\"\n```\n\nMulti-line blocks work too — rubish detects unfinished Ruby (open `do`\n\n, missing `end`\n\n, 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 `=> …`\n\nvalue printed; multi-line blocks and sourced statements run silently for their side effects.\n\nRuby array literals can be used directly in shell context. Rubish distinguishes them from glob patterns like `[a-z]`\n\nautomatically:\n\n``` js\nrubish$ [1, 2, 3].map { |x| x * x }\n=> [1, 4, 9]\n```\n\nYou can execute any Ruby code by surrounding it with a lambda expression (`-> { }`\n\n):\n\n``` php\nrubish$ -> { 2 ** 10 }\n=> 1024\n```\n\nIn addition to the standard shell function syntax, rubish supports Ruby-style `def...end`\n\nwith named parameters and splat args:\n\n``` python\ndef greet(name)\n  echo \"Hello, $name\"\nend\n\ndef log(level, *messages)\n  echo \"[$level] $messages\"\nend\n\ngreet world    # => Hello, world\n```\n\nDefine your prompt as a Ruby function for full programmatic control. The function is called on every prompt render, so it can include dynamic content:\n\n```\ndef rubish_prompt\n  branch = `git branch --show-current 2>/dev/null`.strip\n  dir = Dir.pwd.sub(ENV['HOME'], '~')\n  \"\\e[36m#{dir}\\e[0m \\e[33m#{branch}\\e[0m $ \"\nend\n\ndef rubish_right_prompt\n  Time.now.strftime('%H:%M:%S')\nend\n```\n\nYou can also use the traditional `PS1`\n\n/`RPROMPT`\n\nvariables with bash (`\\X`\n\n) or zsh (`%X`\n\n) escape codes.\n\nRubish 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`\n\noutput, on demand:\n\n``` bash\nrubish$ git <Tab>             # add  branch  checkout  clone  commit  …\nrubish$ gh pr <Tab>           # checkout  close  create  diff  edit  list  …\nrubish$ rails generate <Tab>  # scaffold  model  controller  channel  mailer  …\nrubish$ kubectl get <Tab>     # pods  services  deployments  …\nrubish$ cargo build --<Tab>   # --release  --target  --verbose  …\n```\n\nIt walks arbitrarily deep — `rails generate scaffold --<Tab>`\n\nparses scaffold's own flags, `aws s3 cp --<Tab>`\n\nreaches AWS sub-sub-commands, and so on.\n\nHow it stays correct, fast, and side-effect-free:\n\n- For a list of well-known tools (\n`gem`\n\n,`brew`\n\n,`cargo`\n\n,`aws`\n\n,`npm`\n\n,`gh`\n\n,`kubectl`\n\n,`pyenv`\n\n,`rbenv`\n\n,`launchctl`\n\n,`composer`\n\n,`hg`\n\n, …) rubish curates the right help invocation (`gem help commands`\n\n,`cargo --list`\n\n,`launchctl help`\n\n, etc.), so the cleanest output is used. For anything else it falls back to`<cmd> --help`\n\nthen`<cmd> -h`\n\n. - Each result is cached for 30 minutes; repeat\n`<Tab>`\n\ns against the same chain are instant. - On macOS the help command runs inside a\n`sandbox-exec`\n\nprofile with no network access and writes restricted to`/tmp`\n\n. Completing on an unfamiliar binary can never modify your filesystem —`touch help`\n\nwon't accidentally create a file. - When zsh has a\n`_<cmd>`\n\ncompletion file in`$fpath`\n\n, rubish reads it too — improving completion for tools whose own`--help`\n\nis sparse.\n\nOther completion features:\n\n**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>`\n\nto accept it.`<Tab>`\n\nopens the full popup; see[Completion dialog colors](#completion-dialog-colors)to style it.**Abbreviated path completion**(zsh style):`a/c/a<Tab>`\n\nexpands to`app/controllers/application_controller.rb`\n\n— each component matches by prefix.**Slow help commands**: framework CLIs like`rails`\n\nboot their full app just to print`--help`\n\n, which may exceed the default 5-second timeout. Bump it via`RUBISH_HELP_TIMEOUT=10`\n\n.**Debugging**:`RUBISH_DEBUG_COMPLETION=1`\n\nprints each help-command invocation, its duration, and whether it produced parseable output on stderr.\n\nBash- and zsh-style programmable completion (`complete -F`\n\n, `compgen`\n\n, `compdef`\n\n, `compinit`\n\n, `autoload`\n\n) is supported too, for tools where the auto-help path isn't enough. Existing completion scripts Just Work.\n\nRubish uses Reline's inline completion dialog (the fish-style suggestions popup) for tab-completion. Its colors are configured via [ Reline::Face](https://docs.ruby-lang.org/en/master/Reline/Face.html). Drop a regular Ruby block in your rcfile:\n\n```\n# In ~/.rubishrc or ~/.config/rubish/config\nReline::Face.config(:completion_dialog) do |conf|\n  conf.define :default,   foreground: :cyan,  background: :black\n  conf.define :enhanced,  foreground: :black, background: :cyan, style: :bold\n  conf.define :scrollbar, foreground: :white, background: :black\nend\n```\n\nThe three face names map to different parts of the dialog: `:default`\n\nfor the unselected rows, `:enhanced`\n\nfor the currently highlighted row, and `:scrollbar`\n\nfor the scrollbar (visible when results overflow the popup). Colors can be a Reline symbol (`:black`\n\n, `:red`\n\n, `:green`\n\n, `:yellow`\n\n, `:blue`\n\n, `:magenta`\n\n, `:cyan`\n\n, `:white`\n\n, plus `:bright_*`\n\n/ `:gray`\n\nvariants) or a hex truecolor string like `'#abcdef'`\n\n. `:style`\n\naccepts `:bold`\n\n, `:faint`\n\n, `:italicized`\n\n, `:underlined`\n\n, `:blinking`\n\n, `:negative`\n\n, `:concealed`\n\n, `:crossed_out`\n\n— or an array of those for multiple effects.\n\nThis works because rubish's inline Ruby evaluation handles multi-line blocks (see [Inline Ruby evaluation](#inline-ruby-evaluation)), so the natural `do … end`\n\nform is fine in both rcfiles and at the interactive prompt — no need for one-line reformatting.\n\nSlow shell initializations (e.g., `rbenv init`\n\n, `nvm`\n\n, `pyenv`\n\n) can be deferred to a background thread using `lazy_load`\n\n. 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:\n\n```\n# In ~/.rubishrc\nlazy_load {\n  `rbenv init - --no-rehash bash`\n}\n\nlazy_load {\n  `nodenv init - bash`\n}\n```\n\nMultiple `lazy_load`\n\nblocks run in parallel. By the time you type your first command, they're usually done.\n\nRunning `rubish -r`\n\ndisables all Ruby integration features (inline evaluation, lambdas, blocks, Ruby conditions, and array literals) for executing untrusted scripts safely. Only standard shell syntax is allowed.\n\nIn addition to full Bash compatibility, rubish also supports zsh-style features:\n\n`setopt`\n\n/`unsetopt`\n\n`compdef`\n\n/`compinit`\n\n`autoload`\n\nwith`fpath`\n\n`%X`\n\nprompt codes and`RPROMPT`\n\n/`RPS1`\n\n- Abbreviated path expansion (see\n[Tab completion](#tab-completion))\n\n**Login shells** load (in order):\n\n`/etc/profile`\n\n`~/.config/rubish/profile`\n\nor`~/.rubish_profile`\n\n(or`~/.bash_profile`\n\n/`~/.bash_login`\n\n/`~/.profile`\n\n)\n\n**Interactive shells** load:\n\n`~/.config/rubish/config`\n\nor`~/.rubishrc`\n\n(or`~/.bashrc`\n\n)`./.rubishrc`\n\n(project-local)\n\n**Logout**:\n\n`~/.config/rubish/logout`\n\nor`~/.rubish_logout`\n\n(or`~/.bash_logout`\n\n)\n\nRubish 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](https://github.com/amatsuda/echoes) terminal emulator uses this to render syntax-highlighted prompts and decide command-execution shape ahead of time.\n\n```\nrequire 'rubish'\n\nrepl = Rubish::REPL.new(login_shell: true)\n\n# Run interactively (default).\nrepl.run\n\n# Or drive it programmatically.\nrepl.tokenize('ls | grep foo')         # => Array of Rubish::Lexer::Token (each\n                                       #    with :type and :value) for syntax\n                                       #    highlighting; never raises.\nrepl.try_parse('if true; then')        # => :ok | :incomplete | :error\n                                       #    (use to decide PS2 vs. submit).\nrepl.parse_ast('echo hi')              # => AST root, or nil on parse failure.\nrepl.complete_at(line: 'gi', point: 2) # => Array of completion candidates at\n                                       #    the cursor.\nrepl.prompt_segments                   # => Array of styled-text segments\n                                       #    {text:, fg:, bg:, bold:, italic:,\n                                       #     underline:, inverse:}, ANSI codes\n                                       #    already parsed.\nrepl.right_prompt_segments             # => same shape for the right prompt,\n                                       #    or nil if unset.\n```\n\nThe default `Rubish::Frontend::Tty`\n\nwraps Reline + stdin/stdout. Hosts that own their own line editor can subclass `Rubish::Frontend::Base`\n\nand pass an instance into the REPL:\n\n``` python\nclass MyFrontend < Rubish::Frontend::Base\n  def read_line(prompt:, rprompt: nil)\n    # ...feed input from your own UI here\n  end\nend\n\nRubish::REPL.new(frontend: MyFrontend.new).run\n```\n\nTo run setup code in every forked child between `fork()`\n\nand `exec()`\n\n(e.g. to attach a per-command controlling tty so the line discipline can deliver `Ctrl-C`\n\nto the child):\n\n``` php\nRubish::Command.child_pre_exec_hook = -> {\n  Process.setsid\n  # ...ioctls, signal handlers, etc.\n}\n```\n\n| Category | Commands |\n|---|---|\n| Directory | `cd` , `pwd` , `pushd` , `popd` , `dirs` |\n| I/O | `echo` , `printf` , `read` , `mapfile` , `readarray` |\n| Variables | `export` , `declare` , `typeset` , `readonly` , `unset` , `local` , `shift` , `set` |\n| Process | `exit` , `logout` , `exec` , `kill` , `wait` , `times` |\n| Job control | `jobs` , `fg` , `bg` , `disown` , `suspend` |\n| Functions | `function` , `return` , `caller` |\n| Aliases | `alias` , `unalias` |\n| History | `history` , `fc` |\n| Execution | `eval` , `source` , `.` , `command` , `builtin` |\n| Testing | `test` , `[` , `[[` , `(( ))` , `let` |\n| Control | `break` , `continue` , `trap` |\n| Completion | `complete` , `compgen` , `compopt` , `bind` |\n| Config | `shopt` , `setopt` , `unsetopt` |\n| Info | `help` , `type` , `which` , `hash` |\n| Other | `true` , `false` , `:` , `getopts` , `umask` , `ulimit` , `enable` |\n\n```\nbundle install\nbundle exec rake test\n```\n\nBug reports and pull requests are welcome on GitHub at [https://github.com/amatsuda/rubish](https://github.com/amatsuda/rubish).\n\nMIT", "url": "https://wpnews.pro/news/rubish-a-unix-shell-written-in-pure-ruby", "canonical_source": "https://github.com/amatsuda/rubish", "published_at": "2026-05-23 06:32:00+00:00", "updated_at": "2026-05-23 09:35:00.821407+00:00", "lang": "en", "topics": ["open-source", "developer-tools", "products"], "entities": ["Rubish", "Ruby", "bash", "GitHub", "Homebrew"], "alternates": {"html": "https://wpnews.pro/news/rubish-a-unix-shell-written-in-pure-ruby", "markdown": "https://wpnews.pro/news/rubish-a-unix-shell-written-in-pure-ruby.md", "text": "https://wpnews.pro/news/rubish-a-unix-shell-written-in-pure-ruby.txt", "jsonld": "https://wpnews.pro/news/rubish-a-unix-shell-written-in-pure-ruby.jsonld"}}