# Rakuish **Repository Path**: skyter10086/rakuish ## Basic Information - **Project Name**: Rakuish - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-30 - **Last Updated**: 2026-11-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Rakuish **双语言交互环境**:能跑 shell 脚本,也能执行 Raku 语句。**只支持 Linux。** 用 Raku 实现的 IPython 式 eval 环境 —— REPL 里默认求值 Raku 表达式(结果按 `Out[n]` 显示),`!` 前缀把整行交给 **shell 引擎**(独立进程,默认真 `bash`), `%` 前缀是 magic 命令(`%run` 跑整个脚本、`%shell` 切 shell 后端等)。 **两套语义分居两个进程**:REPL 进程里**只有 Raku 语义**(`$` 永远是 Raku 变量); shell 语义(展开 / 管道 / heredoc / glob / 算术)在**独立的引擎进程**里 (`bin/rakuish-sh`)⇒ 引擎崩了也带不走 REPL,语义也不互相污染。 **两个维度别混**: | 维度 | 默认 | 怎么改 | 影响范围 | |---|---|---|---| | 输入求值语言 | **Raku** | 裸输入走 Raku;`!` / `` `cmd` `` / auto-shell 才走 shell | 决定一行输入交给谁 | | shell 后端 | **bash** | `%shell bash\|native`、`!@bash …` / `!@native …` | 只影响 `!`、反引号、auto-shell;**不影响裸输入的 Raku 求值** | ## 当前状态 **主线是 Raku 实现**(`lib/` + `bin/rakuish`)—— 2026-10-30 定案(依据见 [`HANDOFF.md`](HANDOFF.md) 第 5 节)。`v/` 下的 V 实现**已停止维护**,保留下来 只作 shell 层(lexer / parser / expand)的**移植素材**,详见 [`v/README.md`](v/README.md)。 | 组件 | 状态 | |---|---| | 交互层依赖(Prompt / Terminal::LineEditor / Commands) | ✅ 已装 | | `Rakuish::Incomplete`(多行输入判定) | ✅ 13 条断言,MoarVM 全绿 | | `Rakuish::Platform`(实现差异隔离层) | ✅ 30 条断言,MoarVM 全绿 | | `Rakuish::Eval`(快档求值 + `Out[n]` + 错误渲染) | ✅ 57 条断言,MoarVM 全绿 | | `Rakuish::Loop`(交互主循环:分流 / 多行 / magic) | ✅ 97 条断言,MoarVM 全绿 | | 脚本模式(`rakuish run x.rakuish` / **按 shebang 选解释器**) | ✅ t/43 16 + t/52 14 条断言,MoarVM 全绿 | | **`Rakuish::Result`**(`run` → 结果对象 / 链式加工 / `run-pipe` 真管道) | ✅ t/44 26 条断言,MoarVM 全绿 | | **`Rakuish::Builtins`**(`cd`/`pwd`/`export`/`unset`/`alias`/`type`/`exit`/`read`/`source`/`test` 归 REPL 宿主) | ✅ t/45 81 条断言,MoarVM 全绿 | | **DSL 糖**(`run … :in('data')` / `Result.paths` / 裸 `run` 直接展示 `.out`) | ✅ t/46 13 + t/47 15 条断言,MoarVM 全绿 | | **PATH 补全**(命令 + 内建 + 别名合并去重 / `run` 首参补成带引号 / 按 PATH 串缓存) | ✅ t/48 17 条断言,MoarVM 全绿 | | **脚本模式认 shebang**(`rakuish run x.sh` 走 bash / python3,不再当 Raku 求值) | ✅ t/52 14 条断言,MoarVM 全绿 | | **native 后端语义对齐矩阵**(99 条 bash 构造:**一致 / 明确不支持**两档,**0 静默不同**;清单入对拍每次验收都回归) | ✅ `tools/native-scan.sh` + [`docs/NATIVE-PARITY.md`](docs/NATIVE-PARITY.md),MoarVM 全绿 | | **脚本模式错误输出无装饰**(stderr 只有「文件:行号」+ 消息,没有 REPL 的分级头 / 出错行 / 提示) | ✅ t/43,MoarVM 全绿 | | **`cd -`**(回上一个目录;内建行不再被「未完成格」吃掉;两个实现都同步 `PWD`/`OLDPWD`) | ✅ t/19、t/04,MoarVM 全绿 | | **`read -p`** 提示语走 **stderr** 且**只在 stdin 是终端时**打印(bash 语义) | ✅ t/45、对拍 21-readp,MoarVM 全绿 | | `Rakuish::Slow`(慢档:真 sh 跑 shell) | ✅ 77 条断言,MoarVM 全绿 | | `Rakuish::Shell::Lexer`(引号感知词法) | ✅ t/31,MoarVM 全绿 | | `Rakuish::Shell::Parser`(Pipeline / Command / Redirect) | ✅ t/32,MoarVM 全绿 | | `Rakuish::Shell::Expand`(引号感知展开 + glob) | ✅ t/33,MoarVM 全绿 | | `Rakuish::Shell::Exec`(原生管道 / 重定向:posix_spawn + pipe + dup2) | ✅ t/34,MoarVM 全绿 | | `Rakuish::Shell::Session`(跨格存活的 shell 变量表) | ✅ t/35,MoarVM 全绿 | | 原生 `&&` / `||` 顺序语义(左结合,与真 sh 逐条对拍) | ✅ t/36,MoarVM 全绿 | | 原生命令替换 `$(...)` 与反引号 + bash 展开顺序(展开→分词→glob) | ✅ t/37,MoarVM 全绿 | | 原生 heredoc(`<` 打印**它自己**,与 bash 一致) | ✅ t/19、t/22 | | `cd` 报错文案**两处实现统一**(`cd` 与 `%cd` 逐字相同:没有这个目录 / 不是目录 / 进不去) | ✅ t/19 | | 命令高亮 / 值渲染 / 颜色开关 / 词法作用域 | ✅ t/15、t/17、t/21、t/16 | | 启动器冒烟(shebang / 可执行位 / 语法检查) | ✅ t/14 | **测试规模**:53 个 `.t`、**789 条断言**(prove 口径),`bash tools/run-all.sh` 全绿 (问题总数 = 0)。 自写的多行判定不是权宜之计:**Raku 实现在这件事上都靠不住** —— 它既没有 `` 位置标记,也不区分"未写完"与"写错了"(`1 +` 和 `1 +)` 的消息一模一样), 所以只能自己扫。它是纯逻辑,因此行为可预期 —— 这正是 [Platform 层隔离](docs/DESIGN.md)思路的第一块试金石。 ⚠ **2026-11-05 起只跑 MoarVM**(不再跑 rakupp):rakupp 的实现差异逼出了一堆 「为了兼容它而写」的变通,让代码偏离 idiomatic Raku、也偏离 bash 语义。 项目**只在 Rakudo(MoarVM)上实现与验收** —— 详见 [HANDOFF.md](HANDOFF.md) §1.3。 ### shell 引擎(2026-11-05 起:独立进程 + 可插拔) `!cmd`、反引号捕获、自动回退 shell 全部交给**独立的引擎进程** `bin/rakuish-sh`: | shell 后端 | 说明 | |---|---| | **bash**(默认) | 真 `bash -c` —— **语义 100% 准、零维护**(brace 展开、`[[ ]]`、数组、`case`、函数…全都有) | | native | 本项目自己实现的 Raku shell 层(词法 / 语法 / 展开 / 算术 / 执行,走 `posix_spawn` + `pipe(2)` + `dup2`)—— 不依赖系统 bash,但只覆盖 bash 的一部分 | `%shell bash|native` 随时切换(会话变量两边都互通)。 **逐行委托**(不切默认后端,只让**这一行**走另一个后端): ``` !@bash echo hi → 这一行走 bash 后端 !@native echo hi → 这一行走 native 后端 ! echo hi → 走当前后端(%shell 设的那个,默认 bash) ``` ⚠ `@` 起头是刻意的:shell 的命令名**不可能**以 `@` 开头 ⇒ 这个前缀不会与真实命令冲突。 **不许跨后端委托**:native 后端遇到自己不支持的语法 ⇒ **明确报错 + 退出码 2 + 委托提示**(告诉你用 `!@bash …`),**绝不**偷偷把整行交给 `sh -c` —— 本机 `/bin/sh` 是 dash,它与 bash 的差异会**静默泄漏**(没有 brace 展开、 没有 `[[ ]]`…)⇒ 用户写 bash 语法却跑出另一种结果,比明确报错更糟。 | native 后端暂不支持 | 表现 | |---|---| | `cd /tmp && ls`(shell 内建) | 退出码 2 + 委托提示(**默认的 bash 后端正常**) | | `{ cmd; }` 复合命令 / `( cmd )` 子 shell | 同上 | | `export X=1 \| cat`(会话内建在管道里) | 同上 | | `[[ … ]]` / 数组 / `case` / 函数 | 同上 | **为什么把 shell 语义搬出 REPL 进程**: 1. **崩溃隔离** —— 原生层是全项目**唯一**做裸 libc 调用的地方 (`pipe`/`dup2`/`posix_spawnp`/`malloc`…):一个野指针就是段错误 ⇒ 会带走整个 REPL。 独立进程后,引擎崩了只是这一条命令失败。 2. **语义不互相污染** —— REPL 进程里**只有 Raku 语义**(`$` 永远是 Raku 变量); shell 的 `$` 只存在于引擎那边。 3. **准确度** —— 默认走真 bash ⇒ 不再自己实现 brace 展开、数组、`case`…… **会话变量互通**:引擎跑完把**完整环境**回传给 REPL,REPL 与自己的环境求差 ⇒ `!export FOO=bar` 跨格存活、`!echo $FOO` 读得到、`$?` 也是上一条命令的退出码 ✓ **终端方言**:引擎进程会把 `dumb` / 空的 `TERM` 提升为 `xterm-256color` —— 否则 uutils `ls` 之类**即使显式 `--color=always` 也不上色**(本机实测)。 只影响引擎子进程;REPL 自己的 `%*ENV` 不会被回写。 `!ls` / `! --numbers ls` / `!@bash ls` 都会补 `--color=always`。 ### 原生 shell 层(自研实现,2026-10-31 起;2026-11-05 更新) `native` 后端的实现:词法(引号边界)/ 语法(管道、重定向、顺序执行)/ 展开(变量、命令替换、glob)三件套, 执行走 `posix_spawn` + `pipe(2)` + `dup2`(file actions)自己实现。 | 能力 | 说明 | |---|---| | 管道 | `!ls \| wc -l` 真的用 pipe(2) 串起来,**不走 sh** | | 重定向 | `<` `>` `>>` `2>` `2>&1` `>&2` `&>` `&>>` 全部落到 fd 上 | | 引号语义 | 单引号内一切原样;双引号内可展开;裸位置反斜杠转义任意字符 | | 变量 / glob | `$VAR` `${VAR}` 走会话变量表(回退 `%*ENV`);glob 按 **bash 的顺序**做:**展开 → 分词 → glob** —— 所以 `echo $VAR`(`VAR='*.txt'`)也会 glob | | 顺序执行 | `&&` `\|\|` **原生**支持:`&&` 是前一条成功才跑、`\|\|` 是失败才跑,**同优先级左结合**(与真 sh 逐条对拍) | | 会话变量 | `!export FOO=bar` / `!unset FOO` / `!FOO=bar` —— **跨格存活**,shell 展开、子进程、Raku 侧(`%*ENV`)三边都看得到 | | glob 元字符 | `*` `?` `[abc]` `[a-z]` `[!abc]` 都支持;引号内 / 被转义的元字符**不生效**(bash 语义) | | 命令替换 | `$(cmd)` 与反引号 **原生**支持:只取 stdout、去掉尾随换行;裸位置的结果**参与分词**(`$(echo a b)` → 两个参数),双引号内不分词 | | heredoc | `!cat < ⚠ 一条硬约束(实测踩出来的):**不要用 fork + 在子进程里 exec**。 > MoarVM 是多线程运行时,fork 后其它线程持有的锁在子进程里永远解不开 —— > 子进程只要触发一次 GC/调度器就会永久卡死(连「只调 libc 原生函数」也会, > 因为 NativeCall 调度器自己会分配内存)。 > 所以进程创建交给 **`posix_spawn(3)`**(libc 内部 clone,我们的运行时不参与), > 管道/重定向靠 `posix_spawn_file_actions_adddup2` 描述。 > 压测:2400 次原生执行零失败。细节见 [`HANDOFF.md`](HANDOFF.md) §4.5。 ## 运行 REPL 项目是"教学型"双语言 REPL,直接跑 `bin/rakuish` 即可(需要 tty / 真实尺寸的 pty): ```bash # 方式一:显式用 raku 跑(和开发环境一致,最稳) raku bin/rakuish # 方式二:加可执行位后直接跑(已加好) ./bin/rakuish ``` 模块搜索路径按脚本自身位置推算,所以从任意目录调用都能找到 `lib/`,不依赖当前工作目录。 依赖 `Prompt` 模块(提供 LineEditor 行编辑器 / TAB 补全)。若缺失,启动会给出一句人话提示: ```bash zef install Prompt ``` 在 WSL 里照 `## 开发` 的 `rakuish-dev` 流程用干净 PATH 启动最稳: ```bash ~/bin/rakuish-dev raku bin/rakuish ``` ## 运行脚本(0.11.0 起) 脚本模式和 REPL 共用同一个主循环,只是关掉全部交互装饰。 ```bash raku bin/rakuish run hello.rakuish # 显式 run raku bin/rakuish hello.rakuish # 裸路径也认 chmod +x hello.rakuish && ./hello.rakuish # shebang: #!/usr/bin/env rakuish ``` `hello.rakuish`: ```raku say 'hello'; say 1 + 1; # 3 —— 要输出就 say,裸表达式不打印 !ls -l # ! 开头交给 shell 引擎(默认真 bash) exit 7; # 退出码透传 ``` | | REPL | 脚本 | |---|---|---| | `In[n]` 回显 / `Out[n]` 值渲染 | 有 | 无 | | 求值失败后猜「这行像不像外部命令」 | 有 | **关**(打错变量名不会被当命令执行) | | 出错 | 美化后继续 | 写 stderr,**立即停止**,退出码 1 | | 参数 | —— | `@*ARGS` 可用 | 所以 `raku bin/rakuish run x.rakuish > out.txt` 里只有程序输出,错误全在 stderr。 设计与路线图见 [`docs/PLAN-C.md`](docs/PLAN-C.md):真 bash 留作兼容层、native 语法层冻结、 自研预算全部投到 Raku shell DSL。进度:**P0 脚本模式 ✅**(0.11.0)→ **P1 Result 对象化 ✅**(0.12.0)→ **P2 内建与会话状态归位 ✅**(0.13.0)→ **P3 补全与 DSL 糖 ✅**(0.14.0)→ **P4 脚本正确性 ✅**(0.15.0)→ **P5-1 整文件一次编译 ✅**(0.16.0)。 ## 脚本模式的正确性(0.15.0 起 / C 方案 P4) 三件事,都属于"错了还看不出来"的那类,所以都用真实子进程断言(`t/49-p4-cells.t`)。 1. **cell 切分不再被块内花括号破坏**。`q{...}` / `qq(...)` 现在按嵌套层数配对, `for 1..3 -> $i { say "v={$i}" }`、`%h{$i} = ...` 这类不再静默终止。 2. **跨行 `else` 能接上**。块尾 `}` 属于 `if` / `unless` / `with` / `without` 时, 脚本模式会先探一行:是 `else` / `elsif` / `catch` 就并进同一格,不是就**原样退回**给下一轮 (不丢行、行号记得住)。`for` / `while` 没有 `else` 分支,仍然立即执行,不拖一拍。 3. **不再静默终止**。文件读完而块/引号没闭合时,报 `文件:行号: 文件结束时输入还没写完` 并以 **2** 退出(对齐 bash 的语法错误),前面的输出照常保留。 4. **整文件一次编译执行(0.16.0 / P5-1)**。脚本模式不再「读一格跑一格、每格重跑整份缓冲」: 现在把文件读满、拼成一个编译单元一次 `EVAL`。副作用只发生一次,耗时从 O(n²) 回到 O(n), 顺带把 `use v6.d` / `class` / `sub MAIN` / heredoc / `END` 这些文件级语义全部拿回来。 代价是**编译期错误整份都不执行**(对齐 `raku foo.raku` 与 bash);运行期错误仍保留之前的输出。 证据见 `t/50-p5-script.t`。REPL 维持原有的重放式累积(跨格 `my` / 容器状态靠它活着),不受影响。 ## 会话内建归 REPL 宿主(0.13.0 起 / C 方案 P2) 原则:**谁拥有会话,谁实现内建**。`cd` / `pwd` / `export` / `unset` / `alias` / `unalias` / `type` / `exit` / `read` / `source`(`.`)/ `test`(含 `[`)现在由 REPL 进程自己实现,直接改宿主 `%*ENV` 与 `$*CWD`,不再甩给引擎进程。 ```rakuish export FOO=1 # 宿主 %*ENV 立刻可见 !printenv FOO # 引擎是子进程,天然继承 → 1 cd /tmp # 三种视角同步 pwd # /tmp !pwd # /tmp run('pwd').out # /tmp alias ll='ls -l' # 只对 REPL 简单命令生效 !ll # `!` 行归 bash,不受 alias 影响 ``` 引擎 → 宿主方向仍走 `--env-out` + `apply-env-delta`:`!export BAR=2` 之后 `%ENV` 也看得到。 顺带修掉 P0 遗留:脚本模式 `$*PROGRAM` 指向脚本本身;报错渲染带上 `文件:行号`(`.line` 经 `remap-line` 归算,`.file` 不用 MoarVM 的 `EVAL_0`)。 ## shell 结果对象化(0.12.0 起 / C 方案 P1) `!cmd` 把输出甩给终端、`` `cmd` `` 只给一个字符串——都是"看得见、拿不着"。 `run` 把命令结果变成**值**,于是 shell 输出能直接进 Raku 表达式: ```raku my $r = run 'ls -la'; # 立即执行,得到 Result $r.code; $r.out; $r.err; # 退出码 / stdout / stderr(互不污染) $r.ok; $r.Int; # code == 0 / 退出码 run('ls').grep(*.ends-with('.rakumod')).head; # 进程内过滤 run('git status --short').lines.elems; # 数行 run(q{printf '%s' '{"a":{"b":[1,2,3]}}'}).json[1]; # → 2 run('echo', '-n', 'a b').out; # 多参数 = 直接 exec,不过 shell run-pipe('ls', 'grep .raku', 'head -3'); # 真管道(大数据不进内存) ``` | 成员 | 说明 | |---|---| | `.out` / `.err` / `.code` | stdout(**不**含 stderr)/ stderr / 退出码 | | `.ok` / `.Bool` / `.Int` | `code == 0` / 同 / 退出码 | | `.Str` | bash `$(cmd)` 语义:剥掉尾部换行,内部换行保留 | | `.lines` / `.words` / `.elems` | 切行 / 分词 / 行数 | | `.head(n?)` / `.tail(n?)` / `.first(m)` | 取首尾(不传 `n` 返回单行) | | `.grep(m)` / `.map(&f)` / `.sort(&by?)` / `.join(sep)` | 在**行**上加工,返回 List | | `.json` | 解析 `.out`;装了 `JSON::Fast` 优先用它,否则走内置解析器 | | `.summary` / `.gist` / `.raku` | REPL 摘要 / 完整可读 / `Out[n]` 用的单行摘要 | 语义与 `` `cmd` `` 完全一致(都走引擎,`%shell` 切后端时一起变),差别只在 `run` 还带回了 `code` 与 `stderr`。 > ⚠ `run` 在 **EVAL 里遮蔽了 Raku 内建 `run`**(这是有意的:Rakuish 里 `run('ls')` > 就该拿到 `Result`)。要原生 `Proc`,显式写 `my &cr = &CORE::run;` 再用 `cr(...)`。 ## 补全与三颗小糖(0.14.0 起 / C 方案 P3) P0–P2 解决「能不能用」,P3 全是手感。 **补全** `!` 行首与 `run` 的首个参数,现在补的是「PATH 命令 + 内建 + 别名」合并去重后的结果 —— `!cd` / `!export` / `!ll` 敲一半按 TAB 就有反应了(`ll` 是你在 `.rakuishrc` 里 `alias ll='ls -l'` 定义的)。 `run` 的首参补出来**自带引号**: ``` rakuish> run 'gr run 'grep' ``` 因为 `run ls` 在 Raku 里是「调用例程 `ls`」,`run 'ls'` 才是「执行命令 ls」。 PATH 扫描结果按 PATH 串缓存(补全每敲一个字符都要算候选,几千个可执行文件逐个 `stat` 是主要卡顿源),`export PATH=…` 之后键变了自动失效,不会拿到过期候选。 WSL 特例:默认会把 Windows 的 PATH 追加进来,而 drvfs 上每个文件都报 `+x` —— 所以 `/mnt/` 下的目录改按扩展名认命令(`exe` / `bat` / `cmd` / `com` / 无扩展名), 不会把 `.dll` / `.json` 塞进候选表。 **糖** ```rakuish (run 'grep -c raku', :in("a\nraku\nraku\n")).out.trim; # :in 喂 stdin(shell 行形式)→ "2" (run 'wc', '-l', :in($text)).out.trim; # 同上,argv 形式(直接 exec) (run 'ls -1 *.rakumod').paths».basename; # .paths:输出当 IO::Path 列表 (run 'ls -1 *.rakumod').paths.grep(*.f); # 接着当文件用 run 'ls' # 裸 run:直接把 .out 展示成 Out[n] # Out[3]: # bin # docs # lib # t my $r = run 'ls' # 赋值 ⇒ 仍是 Result 摘要,不刷屏 # Out[4]: Result(code=0 · 12 行 · bin) ``` 判定在**源码**上(`bare-run-src`),所以只影响「这行就是这个命令」的写法;赋值、当实参、 `say run 'ls'` 都不受影响。脚本模式下不自动打印,与「脚本只输出程序自己 say 的东西」一致。 `stderr` 有内容单列一行,退出码非 0 标 `[退出码 N]`: ```rakuish run 'ls /nope' # Out[5]: (无输出) # stderr: ls: cannot access '/nope': No such file or directory # [退出码 2] ``` `:in` 只收字符串;要传句柄或拿原生 `Proc`,显式 `my &cr = &CORE::run;` 再用 `cr(...)`。 ## 安装(作为 Raku 模块) Rakuish 是一个标准 Raku 发行版(见 `META6.json`)。用 zef 安装后即可获得 `rakuish` 命令与 `Rakuish::*` 模块: ```bash # 方式一:从源码目录安装(读取 META6.json,自动拉取 Prompt / Commands 依赖) zef install . # 方式二:走项目自带脚本(等价于上面,并顺手给 bin/rakuish 加可执行位) bash tools/install.sh # 卸载 zef uninstall Rakuish # 或:bash tools/uninstall.sh ``` 安装后依赖 `Prompt`(行编辑器 / TAB 补全)与 `Commands`(magic 分发)会一并装上; `bin/rakuish` 会被装到 Raku 的 bin 目录,直接进入 PATH。 ⚠ **还有第二个可执行文件**:`bin/rakuish-sh` —— **shell 引擎** (REPL 把 shell 命令交给它跑,默认真 bash;见下面「架构」一节)。 它与 `bin/rakuish` 一起安装(`META6.json` 的 `bin` 字段里有两个)。 ⚠ `Rakuish::Shell::Engine::engine-path()` 会依次尝试:`RAKUISH_SH` 环境变量 → `<安装前缀>/bin/rakuish-sh`(从 `$*REPO` 的 prefix 推)→ **源码树**的 `/bin/rakuish-sh` → launcher 同目录 → `PATH`。 ⇒ **源码运行和 zef / raku-pm 安装后**都能找到引擎,不依赖 cwd ✓ (zef 把模块源码放在 `<前缀>/sources/`,不能按模块文件的父目录推算 —— 这是 0.9.3 修掉的真 bug。) > 注意:本项目**只支持 Linux**(LineEditor 依赖真实 pty,且 `%run` 走真 `sh`)。 ## 核心设计(摘要,详见设计文档) | | 快档 | 慢档 | |---|---|---| | 谁执行 | 自身进程(Raku `EVAL` / shell 内建 / 纯 Raku 函数) | 真 `sh` 子进程 | | 输入 | 无前缀 | `!cmd`、`%run s.sh` | | 输出 | Raku 值 → `Out[n]`(**不靠 `$?` 猜**) | 继承 fd1 直通:流式 + 原生 ANSI 颜色原样透传 | | 状态 | 活在 Workspace,跨 cell 保留 | 脚本自己的进程,跑完即销毁 | **关键原则:别用 Raku 模拟 shell 跑脚本。** 整个 `.sh` 原样 spawn 出去, `local` / `$$` / `IFS` / heredoc / 子 shell / 退出码全部由真 `sh` 负责, 一行都不用模拟。 ## 与前作的关系 ### 前作 `raku-shell`(Windows 版 POSIX shell) 已于 2026-10-09 **冻结归档**,在 Windows 侧保留作为参考实现。**不搬它的执行内核,按 Linux 重写** —— Linux 上 `run(:in)` 可用、参数原样传递、没有文本句柄 CRLF、信号投得出去, 要绕的坑根本不存在。详见设计文档第 0.1 节。 ### 曾经的 V 实现(`v/`) 2026-10-13 起曾用 V 重写一版 v0.2,想用自写的 lexer/parser/expand 拿到原生管道与 重定向语义。2026-10-30 **定案:主线回到 Raku 实现**,`v/` 停止维护。 定案依据(实测,不是推测): - **依赖漂移时 Raku 路线优雅降级、V 路线硬崩**。rakupp 5.3.0→5.5.0 只回归了 1 个 测试(正则跨后端语义,根因已定位、修复已验证);V 编译器一升级,**整个构建失败**, REPL 根本跑不起来。 - **V 重写把功能全丢了**:TAB 补全、历史、magic 命令、`Out[n]` 值渲染、报错美化、 会话回放、`~/.rakuishrc`、Ctrl-C 保活 —— Raku 版有的,V 版一个都没有。 - **V 路线要求 pty 常驻、ANSI 剥离、提示符边界识别、`%*ENV` 字符串桥**,而这套复杂度 在 Raku 里全部消失(`EVAL` 就在同一进程内)。设计文档第 0.1 节早就论证过。 `v/` 里的 **shell 层(lexer / parser / expand,537 行纯逻辑)质量不错**,保留下来 作为移植素材 —— 把它们移植进 Raku 实现,就能同时拿到原生管道/重定向语义和现有全部 交互打磨。坑位清单见 [`v/README.md`](v/README.md)。 ## 仓库与远端 远端:**Gitee** — ```bash git remote -v # origin → gitee git push origin main # 主线在 main ``` 本地仓库早期没有远端,历史只存在这一台机器上;2026-10-30 起已配好 Gitee 远端。 换机器前先 `git bundle` 备份仍然更保险。 ## 开发 项目在 WSL 原生文件系统(`~/Rakuish`)—— Linux-only 项目不该放在 `/mnt/c` 那种 DrvFs 上:行尾、权限、文件锁语义都不对,实测慢 10-15%。 ### 先过 `rakuish-dev` **WSL 默认继承 Windows 的 PATH**,里面混着 `/mnt/c/rakudo/bin` 与 `/mnt/c/rakudo/share/perl6/site`;而且 WSL 裸环境下 `raku` **根本不在 PATH 上** (Windows 那份 MoarVM 只在 `/mnt/c/rakudo`)。两个后果: - 直接 `raku` 可能跑到 Windows 那份 Rakudo 上,装的模块落错地方 - 忘了写绝对路径就`command not found`,而 `which -a raku` 是空的 —— 看不出原因 所以所有命令都套一层: ```bash ~/bin/rakuish-dev <命令> # 重建干净 PATH 后执行 ~/bin/rakuish-dev raku t/01-incomplete.t ``` 它做三件事:把 `RAKU_ROOT`(rakubrew 2026.09 的 install/bin)放到 PATH 首位、 清空 `RAKULIB`/`PERL5LIB`/`PERL6LIB`、删掉全部 `/mnt/c` 条目。 ### 两个 Raku 实现都用来验证 ```bash cd ~/Rakuish bash tools/run-all.sh # 全套件 × 两实现,汇总写 tmp/all.log ``` **任何改动都要在两个实现上各跑一遍**,这是 Platform 层隔离的验收标准: 纯逻辑部分两个实现必须逐条断言一致,差异只允许留在 Platform 里。 `run-all.sh` **同时报退出码和 `not ok` 计数** —— 本项目踩过:测试崩在中途 (后面的 subtest 根本没跑)时 `not ok` 计数是 0 但退出码非 0,只看计数会把 "测试崩了"当"全绿"。 实现差异清单见设计文档第 0.1 节,隔离方式见「Platform 层」一节。 ### 主循环的结构约定 **「读一行」被做成可注入的 Callable**(`reader-from` 返回一个按顺序给元素、 取完给 Nil 的闭包),主循环本体 `drive(&read-line)` 只依赖这个 Callable。 于是判定 / 分流 / 渲染全都能单元测试(18 组),真读终端那段由 `tools/repl-smoke.raku` 在 pty 里冒烟。 这个拆分不是洁癖:读一行是库的活,而"这一行该续行吗 / 该走哪档 / Out[n] 是几"才是本项目自己的逻辑 —— 后者才是值得测的部分。 分流规则(显式前缀,**不做自动判别**): | 输入 | 走向 | |---|---| | `1 + 1` | 快档 Raku 求值 | | `%time` / `%help` | magic 命令(`Commands` 承载,自带唯一前缀缩写) | | `!ls -l` | 慢档 shell —— **默认直通**,流式与原生 ANSI 都保住 | | `! --numbers cmd` | 慢档 + 捕获并给行号 `1 │ …`(加工必然失去流式) | | 没写完(如 `1 +`) | 续行,提示符变成 ` ....1> ` | | `%repl` / `%STORE{...}` | `%repl` 看/关/开/清空会话记录;`%STORE{'k'} = v` 是跨 cell 状态主 API(按 Raku 求值,不再误判 magic) | ### 会话持久化(`.repl` 重放) 每次启动会自动把"会改变 workspace 状态的输入"追加进 transcript 文件 (默认 `~/.rakuish/repl.raku`,可用环境变量 `RAKUISH_REPL` 覆盖),下次启动 重放它以恢复 `%STORE` 等跨 cell 状态——典型教学场景:"关机后恢复工作台"。 - **记什么**:普通 Raku 行、`%time`、`%load`(都最终走 EVAL,状态会留下)。 - **不记什么**:`%run`(脚本重跑有副作用)、`%quit`/`%exit`/`%help`(无意义)、 `%repl` 自身(自指的元命令)。失败的 Raku cell 也不记,避免把错误行永久固化。 - **`%repl`**:`%repl`(或 `%repl status`)看当前路径/条数/开关;`%repl off` 关记录 (本会话)、`%repl on` 开、`%repl clear` 清空文件。 - 重放时打印 `↻ 重放 N 条输入(%repl clear 清空 · %repl off 关闭记录)`。 文件格式就是一行条 REPL 输入;多行 cell 用多行存、回放时按同样规则重新拼。 **慢档为什么这样设计**:两个实现上实测确认 `run()` 不接返回值就**继承 fd1**, 于是输出流式直通、`\033[31m` 这类原生 ANSI 原样保留 —— 这在 Linux 上是 **免费**的。代价是拿不到输出内容,所以要加工(行号/汇总)必须显式要求。 `!` 选项写**前置**(`! --numbers cmd`),与 IPython 的 magic 同一约定: 末尾的 `--numbers` 分不清是选项还是传给程序的参数。 ### 交互层依赖已装齐 `Prompt` 0.0.11/ `Terminal::LineEditor` 0.0.23 / `Commands` 0.0.9装在 MoarVM 侧 (经 `zef`,源 `360.zef.pm`),**rakupp 侧三个模块也都能 `use`**。 装的时候踩到两件事,记下来免得重踩: 1. **`zef` 不在 Raku 的 site 目录里**,是 rakubrew 的本地源码仓库 (`~/.rakubrew/repos/zef`,v1.1.4)。用 wrapper 跑: `~/.local/bin/zef` → `raku -I/lib /bin/zef` 2. **`Terminal-API` 的测试需要有真实尺寸的 pty**。无 pty 时 `get-config` 直接抛错; `script -qec` 造出的 pty 尺寸是 0×0,于是「Terminal size is sane」失败。 正确做法:`script -q -c "stty rows 40 cols 120; zef install ..." /dev/null` ### `Prompt` 的一个 API 陷阱 `$p.history` 返回的是**你传给 `:history` 的那个 `IO::Path`**,不是历史条目数组: ```raku my $p = Prompt.new('LineEditor', :history($h)); say $p.history.^name; # IO::Path say $p.history; # /home/zby/.xxx ← 是路径,不是历史内容 ``` 于是 `$p.history.elems` 会返回**路径字符串的长度**,看起来像「历史只有 1 条」 (IO::Path 有 1 个分量)。要读历史内容得自己维护数组,别信`.history`。 ## 目标平台 Linux(x86_64 / aarch64)。需要 Raku —— CI 与本地都用 [rakudo-star](https://rakudo.org/) 的原生发行版。