391ef1aebd
* feat(shell): GNU control-flow/set-option/builtin semantics + 89 integ cases set -u/-x/-f enforcement, set -e GNU list semantics, pipeline negation, break/continue levels, readonly fatal assignment, declare/typeset scoping, GNU select, base#value arithmetic, subshell background jobs (private job table), per-line stdin buffer reset (read no longer poisoned after the first line), GNU read IFS-whitespace trimming. Both languages + mirrored unit tests. 89 docker-pinned integ cases across bash/control.json, setopt.json, builtin.json, jobs.json, read.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(integ): run the new control/setopt/builtin/jobs/read cases on the box target Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(shell): complete test/[/[[ condition evaluator + return/fg fixes, 58 integ cases The old evaluator silently returned false for every operator outside -z/-n/-f/-d, string =/!=, and the six numeric comparisons: [ -e f ], [ -s f ], [[ x == pat* ]], [[ x =~ re ]], [[ a && b ]], and -a/-o combinators all evaluated false with no error, and [ -f dir ] was true. Same silently-wrong class as brace expansion — core agent idioms steering control flow the wrong way. Both languages, GNU-pinned via a 107-probe docker battery (bash 5.2, 106/107 identical) plus 58 exact-command pins: - test/[: bash arity rules (1-4 args) + recursive-descent -o/-a/!/() parser beyond four; file ops -e -f -d -s -r -w -x -L -h via a stat->readdir kind probe and the namespace symlink table; GNU diagnostics ("integer expression expected", "binary operator expected", "too many arguments") at exit 2; unquoted expansions word-split; -1 negative numbers reassembled from the parser's unary split; ERROR-recovery nodes flattened or truncated at ;. - [[ ]]: structured tree evaluator — glob ==/!= with quoted-RHS literal semantics, =~ ERE with BASH_REMATCH capture groups, && || ! ( ), string < >, arithmetic coercion for -eq family ([[ n -eq 3 ]], [[ 1+1 -eq 2 ]]), no word splitting; a bad operator is a parse error killing the whole line (exit 2) like bash. - Operators mirage cannot answer (-p -S -b -c -g -k -u -O -G -N -t, -nt -ot -ef) fail loudly as capability errors instead of silent false. - return: outside function/source now errors-and-continues ($?=2, GNU message) instead of leaking a stray signal; bare return propagates $? (function bodies track last_exit_code per statement); return inside a sourced file stops the source with its status. - fg: split from wait — "fg: current: no such job" exit 1 with no jobs; with a job prints the resumed command and adopts its exit. Coverage: 58 new integ cases (test.json +46, builtin.json +8 incl. disown/complete/compgen pins and return semantics, setopt.json +2 set +u / bare set, jobs.json +2 fg) across all 19 targets; +89 py and +82 TS mirrored unit tests. Harness 1364/1364 on python-ram, python-disk, and typescript-ram with zero curation. Documented divergences: fg follows interactive bash (non-interactive GNU always fails "no job control"); [[ error text is "mirage: conditional binary operator expected"; error messages drop bash's "line N:" prefix (existing convention). New parser limitations: [ a \< b ] is an ERROR node (use [[ a < b ]]); a malformed [ a = ] swallows trailing statements into the test node. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: retrigger after transient GitHub API outage in paths-filter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(shell): test -e/-s truth on prefix stores and size-unknown backends CI on the cloud-backend targets exposed two condition-evaluator bugs: - The readdir existence probe treated an empty listing as an existing directory, but prefix stores (s3, gridfs, hf, nextcloud) list a missing path as [] instead of raising, so -e/-d/-s answered true for nonexistent paths. The probe now demands a non-empty listing; those stores cannot hold an empty directory, so nothing real is lost. - -s counted a size-unknown stat as non-empty, but dropbox/gdrive/box stat freshly written empty files as size-unknown, so [ -s empty ] answered true (also breaking the pre-existing redirect_* cases that assert via test ! -s). A size-unknown regular file is now read to answer exactly; the prefetch TTL cache keeps repeat tests cheap. Adds stub-dispatch regression tests for both backend shapes in Python and TypeScript. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(shell): -d stub in builtins.test lists an entry (empty listing now means missing) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(shell): split the condition evaluator into a package (py+ts) condition.py / condition.ts had grown to ~500 lines mixing node types, operator tables, two evaluators, and the builtin wrapper. Both languages now mirror the same package layout: - types: CondNode dataclasses/union, CondError, CondContext - constants: operator classification sets + INT_COMPARATORS table (operator.eq/... in Python, bigint lambdas in TS) replacing the -eq/-ne/-lt if-chains in both evaluators - operators: operand scoping, path-kind probe, unary/binary application - flat: test/[ arity rules + the >4-arg recursive-descent parser - tree: the [[ ]] expression-tree evaluator - handle: the handle_test/handleTest builtin entry point Public surface is unchanged: the package __init__ (TS index.ts) re-exports what importers used before, so call sites are untouched apart from the TS relative-path bump. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: bytecii <bytecii@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
51 lines
1.8 KiB
Python
51 lines
1.8 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
|
|
def test_break_exits_inner_loop_only(shell):
|
|
out = shell.mirage(
|
|
"for i in 1 2; do for j in a b; do echo $i$j; break; done; done")
|
|
assert out == "1a\n2a\n"
|
|
|
|
|
|
def test_break_two_exits_both_loops(shell):
|
|
out = shell.mirage(
|
|
"for i in 1 2; do for j in a b; do echo $i$j; break 2; done; done")
|
|
assert out == "1a\n"
|
|
|
|
|
|
def test_break_level_beyond_depth_breaks_all(shell):
|
|
out = shell.mirage(
|
|
"for i in 1 2; do for j in a b; do echo $i$j; break 9; done; done")
|
|
assert out == "1a\n"
|
|
|
|
|
|
def test_continue_two_continues_outer_loop(shell):
|
|
out = shell.mirage("for i in 1 2; do for j in a b; do "
|
|
"if [ $j = a ]; then continue 2; fi; echo $i$j; done; "
|
|
"echo inner:$i; done")
|
|
assert out == ""
|
|
|
|
|
|
def test_continue_plain_skips_iteration(shell):
|
|
out = shell.mirage("for i in 1 2 3; do "
|
|
"if [ $i = 2 ]; then continue; fi; echo $i; done")
|
|
assert out == "1\n3\n"
|
|
|
|
|
|
def test_break_two_in_while_inside_for(shell):
|
|
out = shell.mirage("for i in 1 2; do n=0; while [ $n -lt 3 ]; do "
|
|
"n=$((n+1)); echo $i:$n; break 2; done; done")
|
|
assert out == "1:1\n"
|