Rubish: 一个纯 Ruby 编写的 Unix 外壳
摘要
Rubish 是一个纯 Ruby 编写的 Unix 外壳,旨在实现与 bash 的完全兼容,同时深度整合 Ruby 的特性,如块、迭代器和方法链。
查看缓存全文
缓存时间: 2026/05/23 09:29
amatsuda/rubish 源: https://github.com/amatsuda/rubish
Rubish
一个纯 Ruby 编写的 UNIX shell。Shell 语法被解析并编译为 Ruby 代码,然后由 Ruby VM 执行。
概念
完全兼容 Bash
Rubish 支持 bash 的所有特性,并且 shell 语法完全兼容。你可以直接运行现有的 bash 脚本而无需修改。如果你发现任何 bash 脚本在 rubish 中无法工作,我们认为这是一个 bug,欢迎报告!
深度 Ruby 集成
Rubish 不仅是一个用 Ruby 实现的 shell,更是一个深度集成 Ruby 的 shell。你可以在 shell 脚本中无缝混合使用 shell 命令和 Ruby 代码,甚至可以使用 Ruby 强大的特性,如块、迭代器和库。
安装
Homebrew (macOS)
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 是一个小型 bash 启动器,它会自行查找可用的 Ruby(依次探测 ~/.rbenv/shims/ruby、/opt/homebrew/bin/ruby、/usr/local/bin/ruby、系统 Ruby;尊重 $RUBY 环境变量)。当 bundler 不可用时(例如作为登录 shell、从 .app 包中启动,或 PATH 可能极简的情况下),请使用它:
./bin/rubish
RUBY=/opt/homebrew/opt/[email protected]/bin/ruby ./bin/rubish # 显式覆盖
用法
启动交互式 shell:
rubish
执行单个命令:
rubish -c 'echo hello'
运行脚本:
rubish script.sh
你甚至可以将其用作登录 shell!
设置为登录 shell
echo "$(which rubish)" | sudo tee -a /etc/shells
chsh -s "$(which rubish)"
超越 Bash 的特性
Ruby 条件表达式
在 if、while 和 until 的条件中使用 Ruby 表达式,只需用 { } 包裹即可。Shell 变量会自动作为局部变量绑定到 Ruby 表达式中:
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 方法调用风格
除了传统的 UNIX 空格分隔风格外,命令还可以使用 Ruby 方法调用语法(带括号)来调用:
# 这些是等价的:
ls -la
ls('-la')
# 参数可作为方法参数传递:
cat(file.txt)
grep('pattern', file.txt)
方法链
可以使用点符号将命令与 Ruby 方法链式组合,形成管道。链必须通过带括号的调用、数组字面量或块来 打开 —— 一旦进入链上下文,后续的方法可以不加括号:
# 等价于 `ls | sort`
ls().sort
# 等价于 `ls | sort | uniq`
ls().sort.uniq
# 等价于 `cat file.txt | grep error`
cat(file.txt).grep(/error/)
# 链可以与块结合(参见下面的"Ruby 迭代器块")
ls.select { it.end_with?('.rb') }.each { |f| puts f.upcase }
第一个片段需要括号,因为裸 cmd.method 可能被误解为路径或带点的文件名(./script.sh、file.tar.gz)—— 一旦 () 确认了方法调用形式,词法分析器就知道可以安全地链式调用。
Ruby 迭代器块
Ruby 迭代器方法(.each、.map、.select、.detect)可以接受块来逐行处理命令输出:
ls.each { |f| puts f.upcase }
cat(file.txt).map { |line| line.strip }
ls.select { it.end_with?('.rb') }
内联 Ruby 求值
任何以大写字母开头的行都会直接作为 Ruby 代码求值。这意味着你可以在 shell 提示符下直接使用 Ruby 类、方法和表达式,无需任何特殊语法:
rubish$ Time.now
=> 2025-01-01 12:00:00 +0900
rubish$ Dir.glob('*.rb').sort
=> ["Gemfile", "Rakefile"]
rubish$ ENV['HOME']
=> "/Users/you"
Ruby 数组和正则字面量
Ruby 数组字面量可以直接在 shell 上下文中使用。Rubish 会自动将它们与 glob 模式(如 [a-z])区分开:
rubish$ [1, 2, 3].map { |x| x * x }
=> [1, 4, 9]
Lambda 表达式
你可以通过 lambda 表达式(-> { })包裹任意 Ruby 代码来执行:
rubish$ -> { 2 ** 10 }
=> 1024
Ruby 风格函数定义
除了标准的 shell 函数语法外,rubish 还支持 Ruby 风格的 def...end,具有命名参数和 splat 参数:
def greet(name)
echo "Hello, $name"
end
def log(level, *messages)
echo "[$level] $messages"
end
greet world # => Hello, world
自定义 Ruby 提示符
将提示符定义为 Ruby 函数,获得完全的程序化控制。该函数在每次渲染提示符时被调用,因此可以包含动态内容:
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
你还可以使用传统的 PS1/RPROMPT 变量以及 bash(\X)或 zsh(%X)转义码。
懒加载
慢速的 shell 初始化(例如 rbenv init、nvm、pyenv)可以通过 lazy_load 推迟到后台线程中执行。该块会立即在后台运行,其结果(一段 shell 代码字符串)会在下一个提示符之前应用。这可以使 shell 启动瞬间完成:
# 在 ~/.rubishrc 中
lazy_load { `rbenv init - --no-rehash bash` }
lazy_load { `nodenv init - bash` }
多个 lazy_load 块会并行运行。到你输入第一个命令时,它们通常已经完成。
受限模式
运行 rubish -r 会禁用所有 Ruby 集成特性(内联求值、lambda、块、Ruby 条件和数组字面量),以便安全地执行不受信任的脚本。只允许使用标准的 shell 语法。
Zsh 兼容性
除了完全兼容 Bash 外,rubish 还支持 zsh 风格的功能:
setopt/unsetoptcompdef/compinit- 带有
fpath的autoload %X提示符码和RPROMPT/RPS1- 缩写路径扩展:输入
a/c/a会自动扩展为app/controllers/application_controller.rb
配置文件
登录 shell 按顺序加载:
/etc/profile~/.config/rubish/profile或~/.rubish_profile(或~/.bash_profile/~/.bash_login/~/.profile)
交互式 shell 加载:
~/.config/rubish/config或~/.rubishrc(或~/.bashrc)./.rubishrc(项目本地)
注销:
~/.config/rubish/logout或~/.rubish_logout(或~/.bash_logout)
在 Ruby 程序中嵌入
Rubish 公开了一个公共 API,以便其他 Ruby 程序(终端模拟器、IDE 插件、GUI 前端)可以在进程中驱动 rubish 会话 —— 无需 fork+exec,无需 JSON 序列化,只需方法调用。同系列的 Echoes (https://github.com/amatsuda/echoes) 终端模拟器使用此 API 渲染语法高亮的提示符,并提前决定命令执行形状。
require 'rubish'
repl = Rubish::REPL.new(login_shell: true)
# 交互式运行(默认)。
repl.run
# 或者以编程方式驱动。
repl.tokenize('ls | grep foo')
# => Rubish::Lexer::Token 的数组(每个 Token 包含 :type 和 :value)
# 用于语法高亮;永远不会抛出异常。
repl.try_parse('if true; then')
# => :ok | :incomplete | :error
# (用于决定显示 PS2 还是提交)
repl.parse_ast('echo hi')
# => AST 根节点,解析失败时返回 nil
repl.complete_at(line: 'gi', point: 2)
# => 光标位置的补全候选数组
repl.prompt_segments
# => 样式文本段数组 {text:, fg:, bg:, bold:, italic:,
# underline:, inverse:},ANSI 码已解析
repl.right_prompt_segments
# => 右侧提示符的相同结构,如果未设置则返回 nil
自定义 I/O 后端
默认的 Rubish::Frontend::Tty 包装了 Reline + 标准输入输出。拥有自己行编辑器的宿主可以继承 Rubish::Frontend::Base 并将实例传入 REPL:
class MyFrontend < Rubish::Frontend::Base
def read_line(prompt:, rprompt: nil)
# ...从你自己的 UI 提供输入
end
end
Rubish::REPL.new(frontend: MyFrontend.new).run
子进程预执行钩子
要在每个 fork 出的子进程的 fork() 和 exec() 之间运行设置代码(例如,为每个命令附加一个控制 tty,以便行规程可以将 Ctrl-C 传递给子进程):
Rubish::Command.child_pre_exec_hook = -> {
Process.setsid
# ...ioctls, 信号处理器等
}
内建命令
| 分类 | 命令 |
|---|---|
| 目录 | cd, pwd, pushd, popd, dirs |
| I/O | echo, printf, read, mapfile, readarray |
| 变量 | export, declare, typeset, readonly, unset, local, shift, set |
| 进程 | exit, logout, exec, kill, wait, times |
| 作业控制 | jobs, fg, bg, disown, suspend |
| 函数 | function, return, caller |
| 别名 | alias, unalias |
| 历史 | history, fc |
| 执行 | eval, source, ., command, builtin |
| 测试 | test, [, [[, (( )), let |
| 控制 | break, continue, trap |
| 补全 | complete, compgen, compopt, bind |
| 配置 | shopt, setopt, unsetopt |
| 信息 | help, type, which, hash |
| 其他 | true, false, :, getopts, umask, ulimit, enable |
开发
bundle install
bundle exec rake test
贡献
Bug 报告和拉取请求欢迎提交至 GitHub: https://github.com/amatsuda/rubish。
许可证
MIT
相似文章
为什么多年来 Ruby 依然让人有家的感觉
作者回顾了使用 Ruby 的 15 年经历,称赞了其隐藏特性,如 refinements、delegation 以及新的 ZJIT JIT 编译器,并指出 Ruby 搭配 ZJIT 正在缩小与 Go 和 Rust 等更快语言的性能差距。
Zerostack – 一个受Unix启发的纯Rust编写的编码助手
Zerostack 是一个完全用 Rust 构建的受 Unix 启发的编码助手,旨在帮助开发人员进行代码生成和自动化。
从Rust到Ruby
开发人员描述使用LLM将一个15,000行的Rust Web应用转换为Ruby on Rails,发现Ruby版本明显更短,并评估了开发速度、安全性和可测试性方面的权衡。
Bun 的 Rust 重写已合并
Bun,JavaScript 运行时和包管理器,已合并其核心从 Zig 到 Rust 的重写,可能提升性能和可维护性。
@charliermarsh: https://x.com/charliermarsh/status/2057953314419990670
Ruff 是一个用 Rust 编写的超快速 Python 代码检查器和格式化工具,旨在替代 Flake8、Black 和 isort 等多个现有工具。