Rubish:一个用纯 Ruby 编写的 Unix shell
Rubish: A Unix shell written in pure Ruby

原始链接: https://github.com/amatsuda/rubish

**Rubish** 是一个完全用 Ruby 实现且完全兼容 Bash 的 UNIX Shell。它通过 Ruby 虚拟机解析并执行 Shell 语法,使得现有的 Bash 脚本无需修改即可运行。 除了兼容性,Rubish 还将 Ruby 深度集成到了 Shell 体验中: * **混合语法**:无缝混合 Shell 命令与 Ruby 代码。使用 Ruby 代码块、迭代器和表达式来实现条件判断(例如 `if { ... }`)或管道操作(例如 `ls.sort.uniq`)。 * **原生求值**:以大写字母或 Ruby 字面量开头的行会被直接求值。用户可以利用 Ruby 的强大功能来定义函数、提示符和逻辑。 * **性能优化**:提供 `lazy_load` 等特性,通过将繁重的初始化过程转移到后台线程,实现 Shell 的秒级启动。 * **开发者友好**:Rubish 可通过其公共 API 嵌入到 Ruby 应用程序中,非常适合开发自定义终端模拟器或 IDE 插件。它还包含类似 Zsh 的高级补全和路径展开等功能。 Rubish 可通过 Homebrew 或源码安装,支持作为登录 Shell 使用,并提供了一个安全的 `-r` 模式,可在执行不可信脚本时禁用 Ruby 功能。

```Hacker News 最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Rubish:一个用纯 Ruby 编写的 Unix shell (github.com/amatsuda) 13 分由 winebarrel 在 2 小时前发布 | 隐藏 | 过往 | 收藏 | 2 条评论 帮助 ifh-hn 9 分钟前 | 下一条 [–] 比起方法链,我更喜欢管道。 回复 minraws 14 分钟前 | 上一条 [–] 是只有我一个人把 Rubish 看成垃圾(rubbish)而不是 Rubi-sh 吗……我觉得这可能就是笑点所在。嗯…… 回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索: ```
相关文章

原文

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/[email protected]/bin/ruby ./bin/rubish   # explicit override

Start an interactive shell:

Run a single command:

Run a script:

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:

# These are equivalent:
ls -la
ls('-la')

# Arguments can be passed as method arguments:
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:

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

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]

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

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.

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.

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 with fpath
  • %X prompt codes and RPROMPT/RPS1
  • Abbreviated path expansion: type a/c/a<Tab> and it expands to app/controllers/application_controller.rb

Login shells load (in order):

  1. /etc/profile
  2. ~/.config/rubish/profile or ~/.rubish_profile (or ~/.bash_profile / ~/.bash_login / ~/.profile)

Interactive shells load:

  1. ~/.config/rubish/config or ~/.rubishrc (or ~/.bashrc)
  2. ./.rubishrc (project-local)

Logout:

  1. ~/.config/rubish/logout or ~/.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.

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

联系我们 contact @ memedata.com