Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a3b89ed
feat(cp): add -a archive mode preserving attrs and symlinks
Charliechen114514 Jun 28, 2026
967e185
docs(plan): record cp archive batch done
Charliechen114514 Jun 28, 2026
0f9b3cb
feat(test): POSIX three-state exit codes and recursive-descent parser
Charliechen114514 Jun 28, 2026
bbac892
docs(plan): record test POSIX batch done
Charliechen114514 Jun 28, 2026
6dfe329
feat(ls): add -R recursive listing and --color, fix perm format
Charliechen114514 Jun 28, 2026
69f88e6
docs(plan): record ls recursive+color batch done
Charliechen114514 Jun 28, 2026
b6920c3
feat(grep): add -A/-B/-C context lines with group separators
Charliechen114514 Jun 28, 2026
80c7bd4
docs(plan): record grep context batch done
Charliechen114514 Jun 28, 2026
3e29feb
feat(find): boolean expressions with recursive-descent parser
Charliechen114514 Jun 28, 2026
ee7c588
docs(plan): record find boolean batch done
Charliechen114514 Jun 28, 2026
40d48dc
feat(sh): arithmetic expansion and assignment RHS expansion
Charliechen114514 Jun 28, 2026
181eaa9
feat(sh): case statement with glob pattern matching
Charliechen114514 Jun 28, 2026
bfcc71e
feat(sh): user-defined functions with return and local scopes
Charliechen114514 Jun 28, 2026
876eb1a
docs(plan): record sh deepen P1 (arithmetic/case/functions) progress
Charliechen114514 Jun 28, 2026
e07de18
feat(sh): here-doc redirections (<< and <<-) with body expansion
Charliechen114514 Jun 28, 2026
f23c2f1
feat(sh): advanced parameter expansion ${...}
Charliechen114514 Jun 28, 2026
4e5814f
feat(sh): break/continue (incl. break N) and fix AndOr merge across ';'
Charliechen114514 Jun 28, 2026
33bc74f
feat(sh): read -p prompt and -r raw, fix last-variable IFS trimming
Charliechen114514 Jun 28, 2026
bf87627
docs(plan): record sh deepen mid (here-doc/advanced param/break/read)
Charliechen114514 Jun 28, 2026
46c3657
feat(sh): trap for EXIT and signals (INT/TERM/HUP/QUIT)
Charliechen114514 Jun 28, 2026
c1750e2
docs(plan): mark Phase 2 complete (sh trap finishes sh full-set)
Charliechen114514 Jun 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions document/ai/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> Tier 3(批级,易变)。单一事实源(批级)。全树见 [ROADMAP.md](ROADMAP.md),铁律见 [DIRECTIVES.md](DIRECTIVES.md)。
> **Phase 1.5 代码质量审查 ✅ 完成**(体积 -14%、消 iostream/stoi、统一错误宏、fs 封装扩展,379 测试全绿)。
> **v0.3.0 已发布**:L2 rootfs 启动骨架(init/mount/mdev/umount/swapoff/reboot/poweroff,117→123 applet)+ tail -f —— cfbox 在 i.MX6ULL 上作为 PID 1 替代 BusyBox。基线 399 测试 / 418 KB / 123 applet。
> 焦点 → Phase 2 批2 `cp -a`(归档模式:保权限/属主/时间戳/symlink/递归)
> ✅ **Phase 2 全部完成**(cp/test/ls/grep/find + sh 全收 8 项)。下一站:Phase 3 网络最小闭环
> 状态:✅ DONE / 🔄 NEXT / ⏳ PENDING / ⛔ BLOCKED。每批≈一 commit,完成门 `cmake --build build -j$(nproc) && ctest --test-dir build --output-on-failure` 全绿 + `bash tests/integration/run_all.sh`。

## ✅ Phase 1.5(代码质量审查)已完成 — 2026-05-26
Expand All @@ -20,10 +20,12 @@
| 批 | 范围 | 状态 | Commit | 测试 |
|----|------|------|--------|------|
| 批1 | `tail -f/-F`(fd-based follow:fstat 轮询 + 64KiB quantum + -F drain-switch + SIGINT 退出 0) | ✅ | bff34e9 | 381/0 |
| 批2 | `cp -a`(归档模式:保权限/属主/时间戳/symlink/递归) | 🔄 NEXT | — | — |
| 批3 | `test` POSIX 子集(文件测试/字符串/整数/复合表达式,退出码语义) | ⏳ | — | — |
| 批4 | `ls -R` 递归 + `--color`(LS_COLORS 感知、递归缩进) | ⏳ | — | — |
| 批5+ | grep -A/-B/-C、find 布尔表达式、sh 深化(按运维频率排) | ⏳ | — | — |
| 批2 | `cp -a`(归档模式:保权限/属主/时间戳/symlink/递归) | ✅ | a3b89ed | 406/0 |
| 批3 | `test` POSIX 子集(文件测试/字符串/整数/复合表达式,退出码语义) | ✅ | 0f9b3cb | 417/0 |
| 批4 | `ls -R` 递归 + `--color`(LS_COLORS 感知、递归缩进) | ✅ | 6dfe329 | 424/0 |
| 批5a | `grep -A/-B/-C` 上下文(ring 向前 + after_pending 向后 + 组间 `--`) | ✅ | b6920c3 | 431/0 |
| 批5b | `find` 布尔表达式(AST + -a/-o/-not/!/括号 + 递归下降) | ✅ | 3e29feb | 436/0 |
| 批5c | `sh` 深化(算术/case/函数+return+local/here-doc/高级`${}`/break N/read/trap 全收) | ✅ | 46c3657 | 436/57 |

> 各批细节(触及文件、Result 签名草案、完成门、gotcha)由 `/next <批>` 现场产出脚手架,确认后写入本表 commit/测试列。

Expand Down
7 changes: 3 additions & 4 deletions document/ai/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| Phase 0 | ✅(lite,并行收尾) | [phase-0a 基线盘点](../todo/phases/phase-0a-baseline-inventory.md) | 文档漂移修复、differential test 骨架、编译零 warning;与 Phase 1 并行 |
| Phase 1 | ✅ | [核心系统](../todo/phases/phase-1-core-system.md) | P0 系统命令(chmod/chown/dd/mount/stty 等 24 个新 applet) |
| Phase 1.5 | ✅ | [代码质量审查](../todo/phases/phase-1.5-code-quality-review.md) | 错误处理一致性、风格、测试覆盖、体积检查(A-G 扫描全过) |
| **Phase 2** | 🔄 **当前焦点** | [核心深化(同 Phase 1 文档 Part 3)](../todo/phases/phase-1-core-system.md) | tail -f、cp -a、test POSIX、ls -R/--color、grep -A/-B/-C、find 布尔、sh 深化 |
| **Phase 2** | | [核心深化(同 Phase 1 文档 Part 3)](../todo/phases/phase-1-core-system.md) | tail -f、cp -a、test POSIX、ls -R/--color、grep -A/-B/-C、find 布尔、sh 深化(全完成) |
| Phase 3 | ⏳ | [网络最小闭环](../todo/phases/phase-2-network.md) | 基础网络配置、诊断、下载、连接调试 |
| Phase 4 | ⏳ | [生产质量门禁深化](../todo/phases/phase-3-quality.md) | fuzzing、benchmark、POSIX 子集、release 工程 |
| Phase 5 | ⏳ | [多用户与嵌入式运行时](../todo/phases/phase-4-multiuser.md) | login/getty/syslog/mdev/storage |
Expand All @@ -22,9 +22,8 @@
- 兼容性裁决见 [compatibility-policy.md](../todo/compatibility-policy.md);v1.0 验收边界见 [v1-production-criteria.md](../todo/v1-production-criteria.md)。

## 当前焦点
**Phase 2 核心命令深化** 🔄(批级进度见 [PLAN.md](PLAN.md))。基线 399 GTest + 54 集成脚本,123 applet,418 KB(size-opt)。
> **v0.3.0 已发布**:L2 rootfs 启动骨架(`init` askfirst / `mount` / `mdev` / `umount` / `swapoff` / `reboot`·`poweroff`,117→123 applet)+ `tail -f` —— cfbox 在 i.MX6ULL 上作为 PID 1 替代 BusyBox,端到端实测。详见 [changelogs/v0.3.0.md](../../changelogs/v0.3.0.md)。
> 焦点回到 Phase 2 核心深化:批2 `cp -a`(归档模式)→ `test` POSIX → `ls -R`/`--color`。
**Phase 2 核心命令深化 ✅ 全部完成**(2026-06-28)。批2-5c:`cp -a`、`test` POSIX、`ls -R`/`--color`、`grep -A/-B/-C`、`find` 布尔、`sh` 全收(算术/case/函数/here-doc/高级`${}`/break N/read/trap)。基线 **436 GTest + 57 集成(sh)/ 438 KB** size-opt(v0.3.0 基线 399/418)。批级记录见 [PLAN.md](PLAN.md) 与 [notes/](../notes/)。
> **下一站**:Phase 3 网络最小闭环(基础网络配置/诊断/下载)。

## 当前焦点之后下一个可启动的
**Phase 3 网络最小闭环**(基础网络配置/诊断/下载)——Phase 2 核心命令深度到位后启动。更远:Phase 4 质量门禁(fuzzing/release 工程)→ Phase 5 多用户 → Phase 6 长尾。
22 changes: 22 additions & 0 deletions document/notes/2026-06-28-cp-archive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 2026-06-28 — cp -a 归档模式(Phase 2 批2)

## 背景
coreutils `cp -a`(archive)= `-rd --preserve=all`:递归 + 保 mode/owner/time + 复制 symlink 本身(不跟随)。顺带修复既有 `-p` 名实不符(实现只保 mode,help 却声称保 mode/owner/time)。

## 设计决策
- **lstat 驱动遍历,绝不跟随 symlink**:弃 `std::filesystem::copy`(默认跟随 symlink + 不保属性),自写 `copy_one_archive` 按 `S_ISLNK/S_ISDIR` 分发。这是 TOCTOU/symlink 高危面(PLAN GOTCHA #4)。
- **symlink 复制**:`read_symlink` + `create_symlink` 复制链接本身;owner 用 `lchown`,time 用 `utimensat(AT_SYMLINK_NOFOLLOW)`——`utime(2)` 不支持 symlink。
- **目录属性回填顺序**:先 `create_directory`(默认可写权限)→ 递归子项 → **最后** `preserve_attrs` 设 mode/owner/time。否则子项创建会刷新父目录 mtime,导致 -a 保时间戳失败(只在含子项目录暴露,单测易漏)。
- **属性失败 non-fatal**:owner 在非 root 下 EPERM 常见,coreutils 也是警告不致命;仅内容复制失败计 rc=1。
- **新增 fs 原语**:`fs::lstat`(link-aware)、`fs::set_times(path, timespec[2], no_follow)`。
- **潜伏 bug 顺手修**:`fs_util.hpp` 用 `std::vector` 却从未 include `<vector>`(靠 `<filesystem>` 传递包含侥幸编译),gcc-16 收紧传递 include 后暴露。显式补 `<vector>`。

## 验证
- GTest +7(ArchiveCopyPreservesMode/Timestamp/SymlinkAsLink/BrokenSymlink/Tree/DirectoryTimestamp + PreserveKeepsTimestamp),全量 **406/0** 绿。
- 集成 test_cp.sh +`cp -a` 场景(结构/mode/symlink/broken),54 脚本全绿。
- size-opt **422 KB**(基线 418 KB,+4 KB),≤550 KB 红线。

## 陷阱(留给后续批/维护者)
- `-r`(非 `-a`)仍走 `copy_recursive`(跟随 symlink)——coreutils `-r` 本就如此,向后兼容未改;安全归档一律用 `-a`。
- `st_atim/st_mtim` 是 POSIX 2008 字段,glibc/musl 可用;BSD 系 `st_atimespec` 需条件编译——cfbox Linux-only 暂无忧,交叉编译靠 CI cross 阶段兜底。
- commit: `a3b89ed`
24 changes: 24 additions & 0 deletions document/notes/2026-06-28-find-boolean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 2026-06-28 — find 布尔表达式(Phase 2 批5b)

## 背景
find 是扁平 AND 谓词链(`Predicate` enum + `matches_predicates` 全真折叠),无 -o/!/()/-not,未知谓词静默跳过。改为表达式树 + 递归下降。

## 设计决策
- **AST**(`Node{And,Or,Not,Name,Type,Exec,True}`):And/Or 持 N 子;Not 持 1 子;Name/Type/Exec/True 叶子。`Node(Kind)` 构造函数(非聚合)规避 designated-init 在 -Werror 下的诊断差异。
- **递归下降**(`Parser` + token 游标,仿 expr.cpp):`parse_or → parse_and`(隐式 AND:相邻 primary 自动 AND)`→ parse_not → parse_primary`。优先级 AND 高于 OR(`-name a -o -name b -type f` = `a OR (b AND f)`)。
- **-maxdepth 提为 global option**:parse 遇 `-maxdepth N` 设 `parser.maxdepth`,返回 `True` 占位(不进求值树,作用于遍历 `disable_recursion_pending`)。
- **-exec 作 action 叶子**:eval 时执行 + 返回 true;短路求值下 `-name x -o -exec ...` 的 exec 在 OR 左真时不执行(符合 GNU)。
- **PATH vs 表达式消歧**:argv[1] 若以 `-`/`(`/`)` 开头或为 `!`,则是表达式(PATH 默认 `.`),否则是 PATH。
- **错误处理**:未知谓词/缺操作数/括号不匹配 → `failed=true` → exit 1(GNU find 语义)。

## 验证
- GTest +5(FindByOr/FindByNotType/ExplicitAnd/ParensGrouping/UnknownPredicateExits1),全量 **436/0**。
- 集成 test_find.sh +5(-o、! -type f、\( \)、显式 -a、未知谓词 exit 1),16 passed。
- size-opt **422 KB**(持平——AST 用现有 vector,无新模板膨胀)。

## 陷阱
- 隐式 AND:`-name x -type f` 无 `-a` 也按 AND(GNU 行为);`parse_and` 循环 `peek != "-o" && peek != ")"` 触发。
- `-exec` 与默认 `-print`:表达式含 Exec 节点时抑制默认打印(GNU);混合 `-exec ... -print` 显式 print 未实现(后置)。
- 括号在 shell 需转义 `\(` `\)`,经 argv 传入为 `(` `)`(解析器认两种)。
- 未补 -iname/-path/-perm/-mtime/-newer/-prune/-delete(POSIX 子集外,后置)。
- commit: `3e29feb`
23 changes: 23 additions & 0 deletions document/notes/2026-06-28-grep-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 2026-06-28 — grep -A/-B/-C 上下文窗口(Phase 2 批5a)

## 背景
grep 仅即时打印匹配行,无上下文。补 GNU `-A/-B/-C`,核心难点是流式(for_each_line 逐行 fgetc,不可回卷)下的"向前看"与组间分隔。

## 设计决策
- **向前上下文(-B)**:`before_buf` 为容量 B 的 ring(`vector<pair<line_num,content>>`,超容量 erase front)。非匹配、非 after 的行入缓冲;匹配时先 flush 缓冲作为前导上下文。
- **向后上下文(-A)**:匹配后置 `after_pending=A`,后续非匹配行只要 after_pending>0 就 emit 并递减;连续匹配重置 after_pending(合并块)。
- **组间 `--`**:`need_separator`(一块 after 耗尽时置位)+ `last_printed`(连续检测)。emit 前若 `need_separator && ln != last_printed+1` 打 `--`。正确处理"连续块合并不打 --"与"尾部不打 --"。
- **前缀一致性**:`emit` lambda 统一 `path:` + `line_num:` + content,上下文行与匹配行同格式。
- **互斥语义**:`-q/-l/-c` 下上下文不打印(`printing = !quiet && !files_with_matches && !count_only`),但 match_count 仍计数。
- **零回归**:默认(无 -A/-B/-C)after=before=0,不进缓冲逻辑,process_line 走原匹配即打印路径,**字节级不变**。

## 验证
- GTest +7(ContextAfter/Before/Both/Separator/Zero/WithLineNumbers/InvalidNumber),全量 **431/0**。
- 集成 test_grep.sh +4(-A 含 `--`、-B、-C、invalid -A 退 2),16 passed。
- size-opt **422 KB**(持平——vector<pair> 复用既有模板,无新膨胀)。

## 陷阱
- `-C` 覆盖 `-A/-B`(后指定优先,符合 GNU)。
- ring 用 vector erase front 是 O(n),但 B 通常小;未用 deque 避免模板膨胀。
- 多文件上下文:每个文件独立 grep_file,`--` 不跨文件(GNU 跨文件也用 `--`,但本实现 per-file;多文件 -A 场景罕见,可接受)。
- commit: `b6920c3`
22 changes: 22 additions & 0 deletions document/notes/2026-06-28-ls-recursive-color.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 2026-06-28 — ls -R 递归 + --color(Phase 2 批4)

## 背景
ls 仅支持 -a/-l/-h,无递归、无着色(全仓零 ANSI)。顺带修一个潜伏格式 bug。

## 设计决策
- **-R 递归 DFS**(`list_recursive`):GNU 风格分块——每个目录 `path:` 头 + 内容,块间空行。**不跟随 symlink 目录**(symlink_status 判定,规避环形 fs 无限递归)。
- **`--color[=always|auto|never]`**:args.hpp **不注册** --color(避免 has_value 贪婪吃下一个 positional 当值),手动用 `has_long`/`get_long` 解析;bare `--color`=always。auto 由 `isatty(STDOUT)` 控制。
- **固定 type→色映射**(dir=01;34 蓝 / exec=01;32 绿 / symlink=01;36 青 / fifo / socket / 设备=黄系)。LS_COLORS glob 解析**延后**(体积红线 + 收益低)。
- **提取 `print_entry`/`collect_visible`**:消除原 list_directory 与 list_path 单文件的长格式重复(DRY)。
- **修潜伏 bug**:`format_permissions` 返回 10 字符(含前导占位 `-`),调用方又 insert type_char → `ls -l` 输出 `d-rwxr-xr-x`(多一 `-`)。改为返回 9 字符、type_char 由调用方前缀 → `drwxr-xr-x`(coreutils 兼容)。

## 验证
- GTest +7(RecursiveListsNestedDirs/DoesNotFollowSymlinkDir/PlusLong、ColorAlways/AutoOff/Never/InvalidValue),全量 **424/0**。
- 集成 test_ls.sh +3(-R 递归、--color=always 出 ANSI、--color=auto 管道无 ANSI),10 passed。
- size-opt **422 KB**(持平)。

## 陷阱
- `capture_stdout`(test_capture.hpp)用 dup2 重定向到文件 → isatty=0 → auto 自动关色,既有 6 GTest 零回归;着色用例一律 `--color=always`。
- --color 不注册到 args specs 是刻意为之;若将来 args.hpp 支持长选项可选值,可改回注册。
- LS_COLORS 解析未做,--color 仅 type-based 固定色;用户自定义 `*.ext` 着色暂不支持(后置)。
- commit: `6dfe329`
38 changes: 38 additions & 0 deletions document/notes/2026-06-28-sh-deepen-mid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 2026-06-28 — sh 深化中频(here-doc/高级${}/break N/read)(Phase 2 批5c 第 4-7 子项)

承接 [sh 深化 P1](2026-06-28-sh-deepen-p1.md)。本批收 here-doc + 高级参数展开 + break N + read 增强。

## 子项与设计决策

### 4. here-doc `<<` / `<<-`(e07de18)
- lexer 读 `<<DELIM`/`<<-DELIM` 时**一次性收集 body**(直到 DELIM 行,`<<-` 剥前导 tab),body 随 DLess/DLessDash token 携带。
- 约束:`<<DELIM` 在命令行末(常见用法)。多 here-doc 同行 / `<<DELIM arg` 后置不支持(罕见)。
- parser → Redir::HereDoc;executor 写 body 到 mkstemp 临时文件 + dup2 重定向 stdin。
- unquoted here-doc body 经 `expand_noglob` 展开 param/arith/command。

### 5. 高级 `${}` 参数展开(f23c2f1)
- `${#VAR}` 长度;`${VAR#pat}`/`${VAR##pat}` 去头(最短/最长前缀 glob);`${VAR%pat}`/`${VAR%%pat}` 去尾;`${VAR:-def}` 默认;`${VAR:+alt}` 替代。
- glob 前缀/后缀匹配用 fnmatch 递增长度扫描(`glob_prefix_len`/`glob_suffix_len`)。
- `${P##*/}` 经典 basename 技巧可用。
- 未做 `${VAR:=def}`(需修改 const state)与 `${VAR:?err}`(需错误传播),后置。

### 6. break N / continue(4e5814f)
- ShellState `break_loop` bool → `break_depth` int(跨 N 层)。
- execute(AndOr) 检测 break_depth/continue_loop 中断 body(否则 `break` 后 body 剩余命令仍执行)。
- **修两个既存 bug**:① break/continue **完全没有 builtin**(循环标志从无人设,break/continue 静默无效);② `parse_compound_list` merge 时把 result 最后 entry 的 op 强改 Semi,破坏 `cmd1 && cmd2; cmd3` 的 `&&`(`false && echo M; echo X` 错跑 echo M)。改 merge 为直接拼接。

### 7. read 增强(33bc74f)
- `read -p PROMPT`(stderr 提示)、`read -r`(raw,保留 `\`;当前本就保留,-r 为 no-op 标志)。
- **修既存 bug**:最后变量收前导 IFS(`echo a b c | read x y z` 的 z 是 `" c"`,现 `"c"`)。
- 未做 `-s`/`-n N`/`-t`(termios 静默/字符数/超时,后置)。

## 验证
- 集成 test_sh.sh:41 → 55(+3 here-doc +5 高级${} +3 break +3 read),全绿。
- ctest 436/436(sh 仍零 GTest)。
- size-opt **438 KB**(基线 418,+20 KB 累计 sh:fnmatch + mkstemp + AST 扩展)。

## 陷阱 / 留给后续
- **trap** 唯一剩余项:信号处理(SIGINT/SIGTERM/EXIT)。需 signal handler + 全局 trap 表 + executor 检查执行,与 fork/exec + multi-call binary 交互复杂,留下批 fresh context。
- **既存 keyword 误判**(调试 break 时发现):`echo done` 的 `done` 被 lexer 标 keyword、parser 当 terminator,导致 `echo done` 输出空。POSIX keyword 应只在命令起始位置识别。待修(影响 `echo fi/done/then` 等参数)。
- sh 仍零 GTest:本批靠集成验证(55 用例);test_sh.cpp 待建。
- commits: `e07de18`/`f23c2f1`/`4e5814f`/`33bc74f`
40 changes: 40 additions & 0 deletions document/notes/2026-06-28-sh-deepen-p1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 2026-06-28 — sh 深化 P1(算术/case/函数)(Phase 2 批5c 第 1-3 子项)

## 背景
sh 是最大 applet(8 文件 ~1927 行,零 GTest)。P1 高频深化三项:`$((expr))` 算术、`case` 语句、函数+return+local。打通运维脚本最关键闭环(init 脚本大量用这些)。

## 子项与设计决策

### 1. 算术 `$((expr))`(40d48dc)
- **递归下降求值器** `ArithEvaluator`:`+ - * / %`、比较、`&& || !`、括号、整数字面量、变量(裸名或 `$VAR`;空/非数字 → 0)。除零 → error → "0"。
- **赋值 RHS 展开修复**(既存 bug):`X=$((i+1))` 之前 set_var 存 raw 字符串,现 RHS 经 `expand_word` 展开后 join 回单值(赋值不 field-split)。
- process_dollar 识别 `$((` 与 `$(`(命令替换)区分。

### 2. case 语句(181eaa9)
- **CaseClause AST**(CaseBranch: patterns + body)+ Command variant 增项。
- **lexer 加 DSemi token**(`;;`)——既存 lexer 把 `;;` 产两个 Semi,parse_compound_list 无法区分分支结束。DSemi 让 case body 正确终止。
- **模式匹配用 fnmatch**;新增 `expand_noglob`(展开 param/arith/command 但**不** field-split/glob)给 case word/pattern——否则 `*` 会被 filename glob 展开成当前目录文件,破坏模式语义。
- parser `parse_case`:`case WORD in PAT(|PAT)) body;; ... esac`。

### 3. 函数 + return + local(bfcc71e)
- **FuncDef AST** + Command variant;`name() { body; }` 经 parse_func 注册。
- **ShellState 扩展**:函数表 `functions_`、local 作用域栈 `local_scopes_`、`return_pending`/`return_status`。
- **scope-aware get_var/set_var**:local 栈顶优先;set_var 若 name 是 active local 则写 scope,否则写全局 vars_。
- **函数调用**(execute_simple,builtin 后 external 前):保存 positional、设新 positional(`$1..`)、push_scope、execute body、消费 return_pending、pop_scope、恢复 positional。
- **return 传播**:execute(AndOr) + while/for body 检测 `return_pending` 中断,回到调用点消费。
- builtin `return N`(设 pending)+ `local NAME=VAL`(仅函数内,set_local)。

## 验证
- 集成 test_sh.sh:28 → 41(+5 算术 +4 case +4 函数),全绿。
- ctest 436/436(sh 仍零 GTest,待后续建 test_sh.cpp)。
- size-opt **434 KB**(基线 418,+16 KB:ArithEvaluator + CaseClause/FuncDef AST + ShellState 函数表/作用域 + fnmatch)。

## 陷阱 / 留给后续
- **here-doc**(`<<`/`<<-`)未做:lexer 需跨行收集 body 状态机(pending heredoc 队列 + 换行后注入 body token),复杂度高,留下批。
- **赋值 RHS 用 expand_word + join** 是近似(`VAR=*.txt` 会 glob,POSIX 赋值不 glob);罕见,接受。
- **break N / continue N** 未做:当前 break_loop 是 bool(单层),多层 break N 待 ShellState 改 break_depth。
- **高级 `${}`**(`${#VAR}`/`${VAR%pat}`/`${VAR#pat}`/`${VAR:=def}`/`${VAR:?err}`):expand_param 仅 :- / :+,其余留下批。
- **read 增强**(-p/-r/-s/-n/-t)、**trap**:留下批。
- **sh 零 GTest**:本批靠集成验证;test_sh.cpp(capture_stdout + sh_main -c)待建。
- 函数体执行 move body 到 ShellState(FuncDef 二次执行会定义空函数;罕见,接受)。
- commits: `40d48dc`(算术)/ `181eaa9`(case)/ `bfcc71e`(函数)
Loading
Loading