🐚 dsh-bash-native
Rust 实现的 Windows 原生 POSIX bash 执行器
给 DSH 的 shell 提供一个真正原生的 bash —— rubash 引擎,不依赖 WSL,也不需要 MSYS 兼容层
适配三档权限策略 · 后台作业 · 支持持久化终端
中文 · English
🐟 为什么需要它
避免肥鱼被 cmd 咬,也规避了子系统和 MSYS 兼容层的性能开销
| 方案 | 命令在哪里执行 | 每条命令的代价 |
|---|---|---|
| Git for Windows | MSYS2 兼容层上的移植 bash,POSIX 语义由运行时模拟 | 兼容层每次起子 shell 或外部命令都要完整 fork 一个 MSYS2 进程(挂起线程、另起进程、经管道搬运内存),开销随子 shell 启动频率线性放大 |
| WSL | 命令在 Linux VM 的发行版里执行 | 每条命令都要经 Windows/Linux 边界往返一次,延迟对高频短命令尤为不利 |
| 本插件 rubash 引擎 + 工具链 |
纯 Rust 实现的原生二进制,无模拟层、无 VM | grep、sed、awk 等来自随包工具链,其余由引擎在进程内处理,没有兼容层或 VM 边界 |
⚡ 实测:启动与子 shell 的代价
同一台机器(Windows 11 / x64)上的同一组脚本,每项取 5 次最小值,负载只用 shell 内建与一次外部进程,排除工具链差异。
复现:仓库里的
node scripts/bench-engines.mjs。
| 工作负载 | rubash(本插件) | Git Bash / MSYS2 |
|---|---|---|
bash -c :(启动到退出) |
7 ms | 25 ms |
echo hi |
6 ms | 26 ms |
| 算术循环 10k 次 | 65 ms | 75 ms |
[ ] 判定 20k 次 |
167 ms | 181 ms |
| 命令替换 300 次 | 11 ms | 3052 ms |
| 子 shell 300 次 | 25 ms | 2497 ms |
外部进程 20 次(cmd /c exit) |
192 ms | 560 ms |
差距来自 fork:MSYS2 每次命令替换与子 shell 都要 fork 一个兼容层进程(约 8–10 ms),rubash 两样都做在进程内(每次约 0.04 ms),300 次就相差 2–3 秒。
🧭 路径不会被“自动替换”
Git Bash / MSYS2 把参数交给原生程序时会猜哪些像路径并改写(/flag → C:/Program Files/Git/flag,于是脚本和 agent 被迫记着 MSYS_NO_PATHCONV=1);连数据位也一起改:用 MSYS 调原生 awk.exe 跑 awk '/x/{print}',程序位到手是 X:/{print},直接语法错误、进错路径。
rubash 不猜:按子进程方言,整条 argv 要么原样交给带 POSIX 层的程序,要么统一翻成 Win32 拼写;随包工具的脚本、模式、程序位永不改写,极大减少了找不到路径的情况。
📦 安装
CLI 安装
以
webprofile 为例
dsh plugin --profile web add dsh-bash-native
dsh 网页版 / 桌面版安装
dsh 插件页面 → 右上角 添加插件 → 填入插件名
dsh-bash-native或仓库地址:
https://github.com/SpookyWaste/dsh-bash-native
重启后,在 Settings → General 里把 Native Bash (Windows) 设为默认 preset(或在会话里切过去)。
持久化终端组件默认关闭,在插件管理中可以打开。见持久终端组件。
| 平台 | Windows x64 |
| DSH | >=0.2.0-rc.2 <0.3.0-0(0.2.0-rc.2 - 0.2.2-rc.1实测) |
| 持久终端组件 | 需要 DSH >=0.2.1-alpha.1才能开启 |
📝 Changelog
- 0.3.1:引擎由
brush更换为rubash,修复多项已知缺陷,提供更好的原生 POSIX 兼容 - 0.3.3:引擎升级到
rubash1.5.0——三笔补丁150-001–150-003,旧补丁随上游合入退役 - 0.3.5:更新工具链
jaq和findutils到最新 release 。为du命令添加新补丁,优化多文件的统计速度(见下)。
| 树 | 旧(逐条目) | 补丁(枚举) |
|---|---|---|
| 20,000 文件 | 1.29 s | 45 ms |
| 228k 文件 工作区 | >600 s | 3.2 s |
⚙️ 引擎
引擎是 rubash(unixwin/rubash,Rust 写的 bash 5.3 兼容 shell,MIT),本仓库从源码构建它、按 engine.lock.json 里的 commit 应用三笔补丁,产物 engine/win32-x64/rubash.exe(约 14 MB)随包提交。
🧩 补丁清单(点击展开)兼容 MSYS 的
uname -s身份(MSYS_NT-…),所以脚本里按 MSYS 分支的逻辑也能成立;并发、取消与作业语义由引擎自己实现,跟 MSYS 运行时无关。
| 补丁 | 做了什么 |
|---|---|
150-001-cd-into-virtual-roots-with-a-real-directory |
cd 进 /tmp 这类背后有真实目录的虚拟根时,把会话 cwd 也一起移动(1.5.0 里 cwd 是会话状态,不再是进程 cwd):原来只改 PWD 就返回,于是 pwd 报新目录、相对操作数与子进程却还在旧目录。PWD、pwd -P、DIRSTACK 仍然显示 /tmp;/usr、/etc、/bin 没有目录可去,/ 没有唯一目标,这四个名字保持只改逻辑状态。 |
150-002-default-a-unset-locale-family-to-utf-8-on-windows |
没设 LC_ALL/LC_CTYPE/LANG 时,Windows 上默认按 UTF-8 字符语义:${#中文} 是 2、[[:alpha:]] 认 CJK、${v:0:2} 切两个字符。1.5.0 把空的 locale 族解释成 C locale(按字节),这条把它改回 1.4.3 的默认;显式写 LC_ALL=C 仍然按字节。 |
150-003-identity-tools-answer-without-a-path-program |
PATH 上没有 uname/arch 程序时仍由引擎作答(MSYS 人设):1.5.0 的进程内仿真要先解析到程序,否则让位给一次必然失败的 spawn。可解析的一侧不变——PATH 上的同名脚本仍然会运行。 |
💡 每次解析都按 lock 里的 sha256 校验产物,并按盖章跳过重复校验:产物的 size 与 mtime 记进
%LOCALAPPDATA%\dsh-bash-native\verified\,没变就不再读那 14 MB(verifyArtifacts: 'always'可恢复每次都哈希)。
🔧 实现
- 注册
ctx.shell:ShellExecutor的 Service Provider,构建在@deepseek-ai/dsh-bash-local之上,每条命令交给解析到的 rubash 引擎;引擎与工具链都随包分发,并在解析时按 lock 与 manifest 校验。 - 服从 DSH 的三档文件策略(
read-only/workspace-write/danger-full-access):受限档位把引擎 argv 交给ctx.sandbox,不限档位直接 spawn 并如实报告档位。 - 后台作业经
ctx.jobs汇报,超出内存上限的输出落到 spill 文件、路径随结果返回;echo、cat、sort等空PATH也能跑,因为随包工具链先于宿主PATH。 - Web 面用自带的 agent preset 挂载,agent 面用 overlay 显式启用;它不替换宿主层的
ctx.shell,因此其他 preset 的pwsh工具不受影响。
🎛 两个预设
cordis.patch.yml 只有一行注册器(dsh-bash-native/presets),两个 preset 由插件在运行时注册:启动时读本机安装的 presets/{standard,minimal}.patch.yml,把 shipped 的 shell 面换成插件自己的 realm group,不影响其他 preset。
| 预设 | 挂载什么 | 什么时候选 |
|---|---|---|
| Native Bash (Windows) id bash-native,order 5 |
读本机 harness 那条线的 standard 并只换 shell 面:每次调用都是全新的 bash,文件、作业、委派等全套工具跟着那一版上游走 |
日常使用 |
| Native Bash (Windows, minimal) id bash-native-minimal,order 6 |
读本机那条线的 minimal 并只换 shell 面:一个 persona 与一个持久 bash。 |
只要一个 shell 的轻量会话 |
📟 持久终端组件
插件页面里 dsh-bash-native 提供两个可分别开关的组件:dsh-bash-native/presets(两个预设,默认开)与 dsh-bash-native/terminal(提供 6 个交互式持久终端工具,默认关)。
该组件和内置的 持久终端(@deepseek-ai/dsh-experimental-terminal-bundle)注册同名工具,不能也不需要同时开启。 (tool "terminal_open" is already registered)
🛠 配置
除下表外,执行预算继承自
@deepseek-ai/dsh-bash-local:cwd、timeoutMs、maxTimeoutMs、maxOutputBytes、maxSpillBytes、graceMs,默认值与上限由那一侧的 schema 单一出处提供。
| 键 | 默认 | 含义 |
|---|---|---|
confine |
true |
是否服从 ctx.sandbox;关闭即放弃文件策略与拒绝标记 |
requireEngineOnLoad |
false |
无引擎时是加载失败,还是每次调用失败(两个预设都设 true) |
toolsDir |
'' |
POSIX 工具链目录;空 = 先看每用户目录里自建的那份(%LOCALAPPDATA%\dsh-bash-native\tools\bin),没有程序就用随包工具链。填了则照用,空目录也算数(不会悄悄换成随包那份) |
rcFile |
'' |
持久 PTY 会话读的启动文件;空 = %LOCALAPPDATA%\dsh-bash-native\bash-native-rc.sh |
denialSignatureAdditions |
['os error 5'] |
追加的拒绝签名,默认值让非英文 Windows 也标得出拒绝 |
shellEnvOverrides |
{} |
追加环境变量,层叠在模型友好默认值之上 |
verifyArtifacts |
'stamped' |
校验力度:stamped 只在产物或运行副本的 size/mtime 变化时重新哈希;always 每次解析都哈希(引擎 14 MB、工具链 37 MB) |
🧰 工具链
工具链随包分发,装完即用。
📚 101 个名字,公布其中 20 个| 类别 | 内容 |
|---|---|---|
| 公布(20) | grep、sed、awk、jq、find、xargs、diff、cmp、which、timeout、stat、ps,以及 tty、nohup、nice、uptime、hostid、pathchk、locate、updatedb |
| 只装不公布(81) | uutils coreutils 的 88 个程序(cat、ls、rm、cp、head、tail、wc、sort、seq 等——引擎不再自带这些),以及 shell 内建要用的程序形态 echo、printf、pwd、test、true、false、kill |
支持与引擎相同的
/tmp与盘符别名,“程序再启动程序”的链同样成立:printf '/tmp/f\n' | xargs awk '{print}'与printf '/tmp/x\n' | xargs rm都拿到真实路径。数据位由两边同一张表保护:开关
-e/-f的值、grep/sed/awk 的首个位置操作数、find 的-name/-path类主项、basename/dirname 的位置参数,都不翻译——引擎启动的子进程与工具链里“程序再启动程序”的那一半用的是同一份清单。顺带把
bash与sh放到PATH上(工具链目录在前,名字目录紧随其后,两者都排在宿主原有PATH之前):src/shim.ts在%LOCALAPPDATA%\dsh-bash-native\shim\<引擎 sha256>\里把这两个名字指向已校验引擎——同卷时是硬链接,两个名字共用引擎那一份字节。
⚠️ 已知限制
两条与进程内副本无关的平台差异:
形态 实测 cd /absent 2>err.txt | cat诊断既不在 err.txt也不在 shell 的 stderr 上printf … | head -c 1的PIPESTATUS[0]内建写端不报断管, 0而不是141断管(读端提前关闭)时,工具与外部程序用它们自己的措辞和退出码——这是 Windows 没有
SIGPIPE的后果(每个工具各自决定断管怎么办):命令 写端退出码 备注 seq 1 200000 | head -c 10stderr 打 write error: Broken pipeyes | head -c 10不吭声 cat big | head -c 113— ls -R 某目录 | head -c 11— 引擎自己的内建 0不报断管(见上一条 PIPESTATUS[0])
🧹 磁盘足迹与清理
运行时状态都在 %LOCALAPPDATA%\dsh-bash-native 下,并且按内容寻址:每个引擎构建一个 shim/<引擎 sha256>/。
| 目录 | 谁写的 | 大小 | 删掉会怎样 |
|---|---|---|---|
shim/<摘要>/ |
每次解析 | 引擎大小(同卷硬链接时约 0) | 下次解析重建 |
toolchain/<摘要>/ |
每次解析 | 约 36 MB(同卷硬链接时约 0) | 下次解析重建 |
verified/ |
每次校验 | 几十 KB | 只会多一次哈希 |
显式清理(停掉 DSH 之后运行):
将替换为你的profile路径,例如:
%USERPROFILE%\.dsh\profiles\<profile>
node <dsh_profile>/node_modules/dsh-bash-native/bin/clean-state.mjs # 只报告,不动文件
node <dsh_profile>/node_modules/dsh-bash-native/bin/clean-state.mjs --yes # 删掉被取代的 shim/ 与 toolchain/ 缓存
📄 许可
| 内容 | 许可 | 声明与扫描 |
|---|---|---|
| 本插件 | MIT | LICENSE |
| rubash 引擎 | MIT | engine/LICENSE.rubash、依赖扫描 engine/THIRD-PARTY.md |
| 随包工具链 | MIT | uutils coreutils/findutils/grep/sed、jaq、goawk、仓库内 posix-extra;toolchain/LICENSES/ 与 toolchain/THIRD-PARTY.md |
🙏 参考与致谢
为 Agent 提供原生 bash 这条路上,oh-my-pi 是最早的先行实现之一,本插件早期针对 brush 的诊断与修法也受益于它留下的记录。
No comments yet. Be the first to write one.