Rubish: 一个纯 Ruby 编写的 Unix 外壳

Hacker News Top 工具

摘要

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 条件表达式

ifwhileuntil 的条件中使用 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.shfile.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 initnvmpyenv)可以通过 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/unsetopt
  • compdef/compinit
  • 带有 fpathautoload
  • %X 提示符码和 RPROMPT/RPS1
  • 缩写路径扩展:输入 a/c/a 会自动扩展为 app/controllers/application_controller.rb

配置文件

登录 shell 按顺序加载:

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

交互式 shell 加载:

  1. ~/.config/rubish/config~/.rubishrc(或 ~/.bashrc
  2. ./.rubishrc(项目本地)

注销

  1. ~/.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/Oecho, 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 依然让人有家的感觉

Lobsters Hottest

作者回顾了使用 Ruby 的 15 年经历,称赞了其隐藏特性,如 refinements、delegation 以及新的 ZJIT JIT 编译器,并指出 Ruby 搭配 ZJIT 正在缩小与 Go 和 Rust 等更快语言的性能差距。

从Rust到Ruby

Hacker News Top

开发人员描述使用LLM将一个15,000行的Rust Web应用转换为Ruby on Rails,发现Ruby版本明显更短,并评估了开发速度、安全性和可测试性方面的权衡。

Bun 的 Rust 重写已合并

Lobsters Hottest

Bun,JavaScript 运行时和包管理器,已合并其核心从 Zig 到 Rust 的重写,可能提升性能和可维护性。