Commit 1dc9973
Claude/advance project y32 it (#17)
* feat(rust-cli): fix function control-flow and return propagation
Two long-standing correctness holes in shell-function support:
1. Function bodies containing control structures (`if/fi`, `for/done`,
`while/done`, `case/esac`) were fragmented. `parse_function_def`
split the body naively on `;` and `\n`, so `f() { if x; then y; fi; }`
became `["if x", "then y", "fi"]` — three strings that failed to
parse individually. The body is now stored as a raw string between
the outermost braces, with proper brace-depth tracking that respects
single/double quotes. At call time `execute_function_call` uses the
existing control-structure-aware `split_on_semicolons` so a function
body parses the same as any other script fragment.
2. `return` inside nested control structures did nothing. The function
executor detected returns with `cmd_str.starts_with("return")`,
which never matched when `return` was buried inside an `if`, `for`,
or `while`. Introduces `ExecutionResult::Return { exit_code }` — a
sentinel that propagates through every control-structure handler
(`Command::If`, `WhileLoop`, `ForLoop`, `LogicalOp`, `Source`,
`execute_block`) until it reaches `execute_function_call`, which
converts it to a regular exit-code result. The fragile string-match
detection is gone.
Also:
- `tests/function_control_flow_tests.rs`: 8 regression tests covering
if/for/case in function bodies, `return` from nested `if`/`for`, and
a brace-in-quoted-string edge case.
- `tests/security_tests.rs`: `security_no_privilege_escalation` now
skips cleanly when running as root (with an optional
`VSH_ALLOW_ROOT_TESTS=1` override) instead of panicking. The test was
a meta-check about the test environment, not correctness, and was
making the suite non-portable to containerised CI.
Test results: 703 passing, 0 failing, 14 ignored (up from 602).
https://claude.ai/code/session_01EMHrh5Jq32pb98KXoSKLA4
* feat(rust-cli): support multi-line control structures in scripts and source
Previously `execute_script_content` and `Command::Source` iterated
`content.lines()` and parsed each line in isolation, so the canonical
POSIX shape
if true
then
mkdir d
fi
shredded into three lines that failed to parse individually. The same
went for `for/do/done`, `while/do/done`, `case/esac`, and multi-line
function bodies.
Changes:
- `parser.rs`: extract `split_on_top_level` from `split_on_semicolons`
and add `split_on_statement_separators`, which additionally treats
top-level `\n` as a statement boundary. It also tracks brace depth
(scoped to the statement-separator variant) so a `;` inside a
function body — `foo() { a; b; }` — does not end the statement.
Normal `${VAR}` expansions balance themselves and do not affect the
depth.
- `parser.rs::parse_command_block`: use the new splitter so control
structure BODIES can span multiple lines too.
- `main.rs::execute_script_content`: strip whole-line comments, then
feed the entire content through the new splitter.
- `executable.rs::Command::Source`: same fix for sourced files.
- `state.rs::next_fifo_id`: switch to a process-wide atomic counter.
The per-state counter started at 0 for every `ShellState`, so
parallel tests with the same PID collided on
`/tmp/vsh-fifo-<pid>-0`, causing `test_fifo_creation` /
`test_fifo_path_unique` to race intermittently.
- `tests/multiline_script_tests.rs`: 9 regression tests — 4 unit tests
for the splitter (newline, quoted newline, multi-line if/fi, function
defs) and 5 end-to-end tests for sourced scripts (multi-line if,
multi-line for, multi-line function defs, comment-only lines, and
mixed single-/multi-line statements).
Test results: 712 passing, 0 failing, 14 ignored (up from 703).
https://claude.ai/code/session_01EMHrh5Jq32pb98KXoSKLA4
* feat(rust-cli): implement POSIX §2.6.1 tilde expansion globally
Tilde expansion was previously hard-coded only inside the `cd`
command. It now runs as the first step of `expand_variables`, so every
command that uses variable expansion — `mkdir`, `touch`, `echo`,
`cp`, `mv`, external commands, etc. — gets tilde expansion for free.
Supported forms (per POSIX §2.6.1):
~ → $HOME
~/path → $HOME/path
~+ → $PWD
~+/path → $PWD/path
~- → $OLDPWD
~-/path → $OLDPWD/path
Not expanded inside single or double quotes ('~' / "~" stay literal).
This is enforced by `quoted_word_to_string` escaping `~` → `\~` in
quoted contexts; `expand_tilde` recognises the escape and emits a
literal `~`.
~user (getpwnam lookup) is not yet implemented; the tilde is left
literal in that case.
The `cd`-specific `~/` handling is removed since it's now redundant.
Test results: 721 passing, 0 failing, 14 ignored (up from 712).
https://claude.ai/code/session_01EMHrh5Jq32pb98KXoSKLA4
* feat(rust-cli): wire IFS word splitting into for-loops and read builtin
Two key POSIX §2.6.5 integration points for $IFS word splitting:
1. **for-loop word list** — `for x in $var; do ... done` now expands
each word in the `in` clause, then applies `ifs_split` using the
current `$IFS` (defaulting to space/tab/newline). This means
`ITEMS="a b c"; for x in $ITEMS; do mkdir $x; done` creates three
directories instead of one named "a b c".
2. **read builtin** — `read a b c` now splits stdin input by `$IFS`
and assigns fields to named variables. The last variable receives
the remainder (POSIX behaviour). The parser now collects multiple
variable names instead of just one. `read` with no args still
defaults to `$REPLY`.
The existing `ifs_split()` function in posix_builtins.rs (which had
full POSIX semantics — whitespace/non-whitespace IFS handling, empty
IFS preventing splitting, leading/trailing delimiter handling) was
already tested but not wired into any execution path. Now it is.
Tests: 8 new tests covering default IFS, custom IFS (comma-separated),
empty IFS, newline splitting, literal word lists, and multi-variable
read parsing.
Test results: 729 passing, 0 failing, 14 ignored (up from 721).
https://claude.ai/code/session_01EMHrh5Jq32pb98KXoSKLA4
* feat(rust-cli): wire SIGINT trap handler into REPL loops
The `trap 'handler' INT` mechanism was registering handlers in the
TrapTable but never firing them. Now:
- Both REPL variants (basic and enhanced/reedline) check for pending
SIGINT after each command cycle and execute the registered INT trap
handler if one exists.
- The enhanced REPL also fires INT traps on Ctrl-C (reedline returns
Signal::CtrlC directly, bypassing the ctrlc crate's async flag).
- A new public helper `run_pending_traps(state)` in executable.rs
encapsulates the check-and-fire logic. It checks the SIGINT flag,
clears it, looks up the INT trap handler, parses and executes it.
Returns true if the handler produced an Exit result.
- EXIT trap was already firing at shell exit (main.rs). No change.
Tests: 7 new tests covering trap registration, trap reset, INT trap
firing via run_pending_traps (with and without handler), no-signal
noop, and EXIT trap manual firing.
Test results: 736 passing, 0 failing, 14 ignored (up from 729).
https://claude.ai/code/session_01EMHrh5Jq32pb98KXoSKLA4
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3ff4a3e commit 1dc9973
6 files changed
Lines changed: 395 additions & 19 deletions
File tree
- impl/rust-cli
- src
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| |||
312 | 312 | | |
313 | 313 | | |
314 | 314 | | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
315 | 326 | | |
316 | 327 | | |
317 | 328 | | |
| |||
342 | 353 | | |
343 | 354 | | |
344 | 355 | | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
345 | 360 | | |
346 | 361 | | |
347 | 362 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
675 | 675 | | |
676 | 676 | | |
677 | 677 | | |
678 | | - | |
679 | | - | |
680 | | - | |
| 678 | + | |
681 | 679 | | |
682 | 680 | | |
683 | 681 | | |
| |||
692 | 690 | | |
693 | 691 | | |
694 | 692 | | |
695 | | - | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
696 | 728 | | |
697 | 729 | | |
698 | 730 | | |
| |||
892 | 924 | | |
893 | 925 | | |
894 | 926 | | |
895 | | - | |
896 | | - | |
897 | | - | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
898 | 943 | | |
899 | | - | |
| 944 | + | |
900 | 945 | | |
901 | 946 | | |
902 | 947 | | |
| |||
1283 | 1328 | | |
1284 | 1329 | | |
1285 | 1330 | | |
1286 | | - | |
| 1331 | + | |
1287 | 1332 | | |
1288 | 1333 | | |
1289 | 1334 | | |
| |||
1531 | 1576 | | |
1532 | 1577 | | |
1533 | 1578 | | |
| 1579 | + | |
| 1580 | + | |
| 1581 | + | |
| 1582 | + | |
| 1583 | + | |
| 1584 | + | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
| 1593 | + | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
| 1601 | + | |
| 1602 | + | |
| 1603 | + | |
| 1604 | + | |
| 1605 | + | |
| 1606 | + | |
| 1607 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
294 | 294 | | |
295 | 295 | | |
296 | 296 | | |
297 | | - | |
| 297 | + | |
298 | 298 | | |
299 | | - | |
| 299 | + | |
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
| |||
3483 | 3483 | | |
3484 | 3484 | | |
3485 | 3485 | | |
3486 | | - | |
3487 | | - | |
| 3486 | + | |
| 3487 | + | |
3488 | 3488 | | |
3489 | 3489 | | |
3490 | 3490 | | |
3491 | 3491 | | |
3492 | 3492 | | |
3493 | 3493 | | |
3494 | 3494 | | |
3495 | | - | |
| 3495 | + | |
3496 | 3496 | | |
3497 | 3497 | | |
3498 | 3498 | | |
3499 | | - | |
3500 | | - | |
| 3499 | + | |
| 3500 | + | |
3501 | 3501 | | |
3502 | | - | |
| 3502 | + | |
3503 | 3503 | | |
3504 | 3504 | | |
3505 | 3505 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
94 | 100 | | |
95 | 101 | | |
96 | 102 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
0 commit comments