tenseleyflow/bensch / 8636752

Browse files

extract portable builtin tests

32 shell-agnostic builtin test scripts using compare_output against
BASH_REF. Covers alias, arithmetic, arrays, cd/pwd, declare, echo,
eval, exec, export/unset, fc, flow control, getopts, hash, history,
jobs/fg/bg, kill/wait, local, printf, pushd/popd, read, readonly,
set/shopt, shift, source, stress, test, times, trap, type/command,
ulimit, umask, variable ops.

Source: fortsh/tests/builtins/
Authored by espadonne
SHA
8636752996eb4db7a0ea187e73d27b2a82506a49
Parents
8b63983
Tree
272c97a

32 changed files

StatusFile+-
A suites/builtins/portable/test_alias.sh 21 0
A suites/builtins/portable/test_arithmetic.sh 59 0
A suites/builtins/portable/test_arrays.sh 46 0
A suites/builtins/portable/test_cd_pwd.sh 30 0
A suites/builtins/portable/test_declare.sh 35 0
A suites/builtins/portable/test_echo.sh 34 0
A suites/builtins/portable/test_eval.sh 28 0
A suites/builtins/portable/test_exec.sh 29 0
A suites/builtins/portable/test_export_unset.sh 28 0
A suites/builtins/portable/test_fc.sh 27 0
A suites/builtins/portable/test_flow_control.sh 39 0
A suites/builtins/portable/test_getopts.sh 30 0
A suites/builtins/portable/test_hash.sh 21 0
A suites/builtins/portable/test_history.sh 30 0
A suites/builtins/portable/test_jobs_fg_bg.sh 42 0
A suites/builtins/portable/test_kill_wait.sh 32 0
A suites/builtins/portable/test_local.sh 24 0
A suites/builtins/portable/test_printf.sh 47 0
A suites/builtins/portable/test_pushd_popd_dirs.sh 39 0
A suites/builtins/portable/test_read.sh 39 0
A suites/builtins/portable/test_readonly.sh 21 0
A suites/builtins/portable/test_set_shopt.sh 30 0
A suites/builtins/portable/test_shift.sh 19 0
A suites/builtins/portable/test_source.sh 29 0
A suites/builtins/portable/test_stress.sh 863 0
A suites/builtins/portable/test_test.sh 68 0
A suites/builtins/portable/test_times.sh 9 0
A suites/builtins/portable/test_trap.sh 27 0
A suites/builtins/portable/test_type_command.sh 26 0
A suites/builtins/portable/test_ulimit.sh 41 0
A suites/builtins/portable/test_umask.sh 15 0
A suites/builtins/portable/test_variable_ops.sh 59 0
suites/builtins/portable/test_alias.shadded
@@ -0,0 +1,21 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[alias]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. alias basic"
6
+compare_output "alias define and use via eval" 'shopt -s expand_aliases 2>/dev/null; alias greet="echo hello"; eval greet'
7
+compare_exit "alias lists all without error" 'alias >/dev/null 2>&1'
8
+compare_exit "alias nonexistent fails" 'alias nonexistent_alias_xyz 2>/dev/null'
9
+compare_output "alias with arguments" 'shopt -s expand_aliases 2>/dev/null; alias say="echo"; eval "say hello"'
10
+
11
+section "2. unalias"
12
+compare_exit "unalias removes alias" 'alias greet="echo hello"; unalias greet; alias greet 2>/dev/null'
13
+compare_exit "unalias -a removes all" 'alias a1="echo 1"; alias a2="echo 2"; unalias -a; alias a1 2>/dev/null'
14
+compare_exit "unalias nonexistent fails" 'unalias nonexistent_alias_xyz 2>/dev/null'
15
+
16
+section "3. alias edge cases"
17
+compare_output "alias with equals in value" 'shopt -s expand_aliases 2>/dev/null; alias myvar="echo x=1"; eval myvar'
18
+compare_output "alias with semicolon" 'shopt -s expand_aliases 2>/dev/null; alias both="echo a; echo b"; eval both'
19
+compare_output "alias preserves original after unalias" 'alias echo="printf ALIAS"; unalias echo; echo hello'
20
+
21
+print_summary
suites/builtins/portable/test_arithmetic.shadded
@@ -0,0 +1,59 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[arithmetic]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. basic arithmetic"
6
+compare_output "addition" 'echo $((3 + 4))'
7
+compare_output "subtraction" 'echo $((10 - 3))'
8
+compare_output "multiplication" 'echo $((6 * 7))'
9
+compare_output "division" 'echo $((20 / 4))'
10
+compare_output "modulo" 'echo $((17 % 5))'
11
+compare_output "negative result" 'echo $((3 - 10))'
12
+compare_output "zero division guard" 'echo $((0 / 1))'
13
+
14
+section "2. operator precedence"
15
+compare_output "multiply before add" 'echo $((2 + 3 * 4))'
16
+compare_output "parentheses override" 'echo $(( (2 + 3) * 4 ))'
17
+compare_output "nested parentheses" 'echo $(( (2 + 3) * (4 - 1) ))'
18
+compare_output "complex expression" 'echo $(( 10 / 2 + 3 * 4 - 1 ))'
19
+
20
+section "3. comparison and ternary"
21
+compare_output "ternary true" 'echo $((5 > 3 ? 5 : 3))'
22
+compare_output "ternary false" 'echo $((2 > 3 ? 2 : 3))'
23
+compare_output "equality" 'echo $((5 == 5))'
24
+compare_output "inequality" 'echo $((5 != 3))'
25
+compare_output "less than" 'echo $((3 < 5))'
26
+compare_output "greater than" 'echo $((5 > 3))'
27
+compare_output "logical AND" 'echo $((1 && 1))'
28
+compare_output "logical OR" 'echo $((0 || 1))'
29
+compare_output "logical NOT" 'echo $((!0))'
30
+
31
+section "4. increment and decrement"
32
+compare_output "post-increment" 'x=5; echo $((x++)); echo $x'
33
+compare_output "pre-increment" 'x=5; echo $((++x)); echo $x'
34
+compare_output "post-decrement" 'x=5; echo $((x--)); echo $x'
35
+compare_output "pre-decrement" 'x=5; echo $((--x)); echo $x'
36
+
37
+section "5. bitwise operations"
38
+compare_output "bitwise AND" 'echo $((12 & 10))'
39
+compare_output "bitwise OR" 'echo $((12 | 10))'
40
+compare_output "bitwise XOR" 'echo $((12 ^ 10))'
41
+compare_output "bitwise NOT" 'echo $((~0))'
42
+compare_output "left shift" 'echo $((1 << 4))'
43
+compare_output "right shift" 'echo $((16 >> 2))'
44
+
45
+section "6. compound assignment"
46
+compare_output "+= assignment" 'x=10; echo $((x += 5))'
47
+compare_output "-= assignment" 'x=10; echo $((x -= 3))'
48
+compare_output "*= assignment" 'x=4; echo $((x *= 3))'
49
+compare_output "/= assignment" 'x=20; echo $((x /= 4))'
50
+compare_output "%= assignment" 'x=17; echo $((x %= 5))'
51
+
52
+section "7. variables in arithmetic"
53
+compare_output "variable reference" 'a=10; b=20; echo $((a + b))'
54
+compare_output "unset var is zero" 'unset V; echo $((V + 5))'
55
+compare_output "assignment in expression" 'echo $((x = 5 + 3)); echo $x'
56
+compare_output "chained assignment" 'echo $((x = y = 5)); echo $x $y'
57
+compare_output "comma operator" 'echo $((x=1, y=2, x+y))'
58
+
59
+print_summary
suites/builtins/portable/test_arrays.shadded
@@ -0,0 +1,46 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[arrays]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. indexed array basics"
6
+compare_output "array literal assignment" 'arr=(a b c); echo ${arr[0]}'
7
+compare_output "array second element" 'arr=(a b c); echo ${arr[1]}'
8
+compare_output "array third element" 'arr=(a b c); echo ${arr[2]}'
9
+compare_output "array all elements @" 'arr=(a b c); echo ${arr[@]}'
10
+compare_output "array all elements *" 'arr=(a b c); echo ${arr[*]}'
11
+compare_output "array length" 'arr=(a b c); echo ${#arr[@]}'
12
+compare_output "array single element assignment" 'arr[0]=hello; echo ${arr[0]}'
13
+
14
+section "2. indexed array operations"
15
+compare_output "array sparse assignment" 'arr=(); arr[0]=x; arr[5]=y; echo ${arr[5]}'
16
+compare_output "array indices with !" 'arr=(a b c); arr[5]=d; echo ${!arr[@]}'
17
+compare_output "array append +=" 'arr=(a b); arr+=(c d); echo ${arr[@]}'
18
+compare_output "array slice" 'arr=(a b c d e); echo ${arr[@]:1:2}'
19
+compare_output "array slice to end" 'arr=(a b c d e); echo ${arr[@]:2}'
20
+compare_output "array unset element" 'arr=(a b c); unset arr[1]; echo ${arr[@]}'
21
+compare_output "array unset preserves indices" 'arr=(a b c d); unset arr[1]; echo ${!arr[@]}'
22
+compare_output "array element length" 'arr=(hello world); echo ${#arr[0]}'
23
+
24
+section "3. indexed array advanced"
25
+compare_output "array in for loop" 'arr=(a b c); for x in "${arr[@]}"; do echo $x; done'
26
+compare_output "array with spaces in elements" 'arr=("hello world" "foo bar"); echo ${arr[0]}'
27
+compare_output "array reassignment" 'arr=(a b c); arr=(x y); echo ${arr[@]}'
28
+compare_output "array from command substitution" 'arr=($(echo a b c)); echo ${arr[1]}'
29
+compare_output "array element modification" 'arr=(a b c); arr[1]=B; echo ${arr[@]}'
30
+
31
+section "4. associative array basics"
32
+compare_output "assoc array set and get" 'declare -A m; m[name]=alice; echo ${m[name]}'
33
+compare_output "assoc array multiple keys" 'declare -A m; m[a]=1; m[b]=2; m[c]=3; echo ${m[a]} ${m[b]} ${m[c]}'
34
+compare_output "assoc array count" 'declare -A m; m[a]=1; m[b]=2; m[c]=3; echo ${#m[@]}'
35
+compare_output "assoc array overwrite key" 'declare -A m; m[k]=old; m[k]=new; echo ${m[k]}'
36
+compare_output "assoc array unset key" 'declare -A m; m[a]=1; m[b]=2; unset m[a]; echo ${#m[@]}'
37
+
38
+section "5. associative array advanced"
39
+compare_output "assoc array key list sorted" 'declare -A m; m[x]=1; m[y]=2; for k in "${!m[@]}"; do echo "$k"; done | sort'
40
+compare_output "assoc array value with spaces" 'declare -A m; m[key]="hello world"; echo ${m[key]}'
41
+compare_output "assoc array numeric keys" 'declare -A m; m[1]=one; m[2]=two; echo ${m[1]} ${m[2]}'
42
+compare_output "assoc array empty value" 'declare -A m; m[k]=""; echo ">${m[k]}<"'
43
+compare_output "assoc array in loop" 'declare -A m; m[a]=1; m[b]=2; for v in "${m[@]}"; do echo $v; done | sort'
44
+compare_output "assoc array quoted key" 'declare -A m; m["my key"]="value"; echo ${m["my key"]}'
45
+
46
+print_summary
suites/builtins/portable/test_cd_pwd.shadded
@@ -0,0 +1,30 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[cd-pwd]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. cd basic"
6
+compare_output "cd no args goes to HOME" 'cd && pwd | tail -1'
7
+compare_output "cd to absolute /tmp" 'cd /tmp && pwd'
8
+compare_output "cd to relative subdir" "mkdir -p $TEST_TMPDIR/sub && cd $TEST_TMPDIR && cd sub && pwd"
9
+compare_exit "cd to nonexistent dir fails" 'cd /nonexistent_dir_xyz_12345'
10
+compare_exit "cd to nonexistent dir exit code" 'cd /nonexistent_dir_xyz_12345 2>/dev/null'
11
+compare_output "cd to root" 'cd / && pwd'
12
+compare_output "cd with trailing slash" 'cd /tmp/ && pwd'
13
+
14
+section "2. cd special"
15
+compare_output "cd - returns to OLDPWD" 'cd /tmp && cd / && cd - 2>/dev/null && pwd'
16
+compare_output "OLDPWD is set after cd" 'cd /tmp && cd / && echo $OLDPWD'
17
+compare_output "cd .. parent directory" 'cd /tmp && cd .. && pwd'
18
+compare_output "cd ../.. grandparent" 'cd /tmp && cd ../.. 2>/dev/null && pwd'
19
+compare_output "cd . stays in same dir" 'cd /tmp && cd . && pwd'
20
+
21
+section "3. pwd"
22
+compare_output "pwd outputs current dir" 'pwd'
23
+compare_exit "pwd exits 0" 'pwd'
24
+compare_output "pwd -L logical path" 'pwd -L'
25
+compare_output "pwd -P physical path" 'pwd -P'
26
+
27
+section "4. cd with CDPATH"
28
+compare_output "cd with CDPATH" "mkdir -p $TEST_TMPDIR/base/target && CDPATH=$TEST_TMPDIR/base && cd target 2>/dev/null && pwd"
29
+
30
+print_summary
suites/builtins/portable/test_declare.shadded
@@ -0,0 +1,35 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[declare]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. declare basic flags"
6
+compare_output "declare -i integer arithmetic" 'declare -i num=5+3; echo $num'
7
+compare_output "declare -r is readonly" 'declare -r RO=hello; echo $RO'
8
+compare_exit "declare -r prevents modification" 'declare -r RO=hello; RO=world 2>/dev/null'
9
+compare_output "declare -x exports variable" 'declare -x MYEXP=exported; '"$BASH_REF"' -c "echo \$MYEXP"'
10
+compare_output "declare plain variable" 'declare V=hello; echo $V'
11
+
12
+section "2. declare arrays"
13
+compare_output "declare -a indexed array" 'declare -a arr=(a b c); echo ${arr[1]}'
14
+compare_output "declare -a empty array" 'declare -a arr; arr[0]=x; echo ${arr[0]}'
15
+compare_output "declare -A associative array" 'declare -A map; map[key]=val; echo ${map[key]}'
16
+compare_output "declare -A multiple keys" 'declare -A m; m[a]=1; m[b]=2; echo ${m[a]} ${m[b]}'
17
+compare_output "declare -A overwrite key" 'declare -A m; m[k]=old; m[k]=new; echo ${m[k]}'
18
+
19
+section "3. declare listing"
20
+check_exit "declare -p prints attributes" 'declare -p >/dev/null' "0"
21
+compare_output "declare without args succeeds" 'declare >/dev/null; echo $?'
22
+compare_output "declare -p specific var" 'declare -i NUM=42; declare -p NUM'
23
+
24
+section "4. declare integer behavior"
25
+compare_output "declare -i assignment evaluates arithmetic" 'declare -i x; x=2+3; echo $x'
26
+compare_output "declare -i multiplication" 'declare -i x=3*4; echo $x'
27
+compare_output "declare -i with variable reference" 'y=10; declare -i x=y+5; echo $x'
28
+compare_output "declare -i string assigns zero" 'declare -i x=notanumber; echo $x'
29
+
30
+section "5. declare combined flags"
31
+compare_output "declare -ix integer and export" 'declare -ix INTEXP=42; echo $INTEXP'
32
+compare_output "declare -ri readonly integer" 'declare -ri RINT=42; echo $RINT'
33
+compare_exit "declare -ri prevents modification" 'declare -ri RINT=42; RINT=99 2>/dev/null'
34
+
35
+print_summary
suites/builtins/portable/test_echo.shadded
@@ -0,0 +1,34 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[echo]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. echo basic"
6
+compare_output "echo basic string" 'echo hello world'
7
+compare_output "echo with no arguments" 'echo'
8
+compare_output "echo with multiple spaced args" 'echo a   b    c'
9
+compare_output "echo single arg" 'echo hello'
10
+compare_output "echo preserves quotes in args" 'echo "hello world"'
11
+compare_output "echo multiple quoted args" 'echo "hello" "world"'
12
+compare_output "echo with special characters" 'echo "hello*world"'
13
+compare_output "echo empty string" 'echo ""'
14
+
15
+section "2. echo flags"
16
+compare_output "echo -n suppresses newline" 'echo -n hello'
17
+compare_output "echo -n with multiple args" 'echo -n hello world'
18
+compare_output "echo -e interprets backslash-n" 'echo -e "hello\nworld"'
19
+compare_output "echo -e interprets backslash-t" 'echo -e "hello\tworld"'
20
+compare_output "echo -e interprets double backslash" 'echo -e "back\\\\slash"'
21
+compare_output "echo -e interprets backslash-a and backslash-b" 'echo -e "x\b\ay"'
22
+compare_output "echo -E disables escape interpretation" 'echo -E "hello\nworld"'
23
+compare_output "echo -en combines flags" 'echo -en "hello\nworld"'
24
+compare_output "echo -ne combines flags reversed" 'echo -ne "hello\tworld"'
25
+
26
+section "3. echo edge cases"
27
+compare_output "echo dash-dash is literal" 'echo -- hello'
28
+compare_output "echo treats unknown flag as text" 'echo -z hello'
29
+compare_output "echo -e with \\c truncates" 'echo -e "hello\cworld"'
30
+compare_output "echo -e with \\0NNN octal" 'echo -e "\0101"'
31
+compare_output "echo -e with \\xNN hex" 'echo -e "\x41"'
32
+compare_output "echo preserves trailing spaces in quotes" 'echo "hello   "'
33
+
34
+print_summary
suites/builtins/portable/test_eval.shadded
@@ -0,0 +1,28 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[eval]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. eval basic"
6
+compare_output "eval basic echo" "eval 'echo hello'"
7
+compare_output "eval with variable expansion" 'X=world; eval "echo hello $X"'
8
+compare_output "eval constructs command from parts" 'CMD="echo"; ARG="hello"; eval "$CMD $ARG"'
9
+compare_output "eval with multiple statements" 'eval "echo one; echo two"'
10
+
11
+section "2. eval exit codes"
12
+compare_exit "eval preserves exit code 0" "eval 'true'"
13
+compare_exit "eval preserves exit code 1" "eval 'false'"
14
+compare_exit "eval exit with specific code" "eval 'exit 42'"
15
+compare_output "eval captures $?" 'eval "true"; echo $?'
16
+
17
+section "3. eval compound commands"
18
+compare_output "eval for loop" "eval 'for i in a b c; do echo \$i; done'"
19
+compare_output "eval if statement" 'eval "if true; then echo yes; fi"'
20
+compare_output "eval pipeline" 'eval "echo hello | tr h H"'
21
+
22
+section "4. eval edge cases"
23
+compare_output "eval empty string" 'eval ""; echo $?'
24
+compare_output "eval variable indirection" 'name=VAR; VAR=hello; eval "echo \$$name"'
25
+compare_output "eval with quoting layers" "eval 'echo '\"'\"'hello'\"'\"''"
26
+compare_output "eval nested" 'eval eval "echo hello"'
27
+
28
+print_summary
suites/builtins/portable/test_exec.shadded
@@ -0,0 +1,29 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[exec]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. exec replaces shell"
6
+compare_output "exec replaces shell with command" 'exec echo replaced'
7
+compare_exit "exec replaces shell exit code true" 'exec true'
8
+compare_exit "exec replaces shell exit code false" 'exec false'
9
+compare_output "exec nothing after exec runs" 'exec echo hello; echo unreachable'
10
+compare_output "exec with arguments" 'exec echo one two three'
11
+compare_exit "exec nonexistent command fails" 'exec /nonexistent_cmd_xyz 2>/dev/null'
12
+
13
+section "2. exec with redirections only"
14
+compare_output "exec redirect fd to /dev/null" "exec 3>/dev/null; echo ok"
15
+compare_output "exec redirect fd to file" "exec 3>$TEST_TMPDIR/execout; echo hello >&3; exec 3>&-; cat $TEST_TMPDIR/execout"
16
+compare_output "exec input redirect from file" "echo data > $TEST_TMPDIR/execin; exec 3<$TEST_TMPDIR/execin; read line <&3; echo \$line"
17
+compare_output "exec close fd" "exec 3>$TEST_TMPDIR/execout; exec 3>&-; echo ok"
18
+compare_output "exec redirect stdout" "exec >$TEST_TMPDIR/execstdout; echo captured; exec >/dev/tty 2>/dev/null; cat $TEST_TMPDIR/execstdout"
19
+compare_output "exec redirect stderr" "exec 2>$TEST_TMPDIR/execstderr; echo err >&2; exec 2>/dev/tty 2>/dev/null; cat $TEST_TMPDIR/execstderr"
20
+
21
+section "3. exec in subshell"
22
+compare_output "exec in subshell does not affect parent" '(exec echo sub); echo parent'
23
+compare_exit "exec in subshell exit code" '(exec false)'
24
+
25
+section "4. exec preserves environment"
26
+compare_output "exec preserves exported vars" 'export MYVAR=hello; exec echo $MYVAR'
27
+compare_output "exec with PATH lookup" 'exec ls /dev/null'
28
+
29
+print_summary
suites/builtins/portable/test_export_unset.shadded
@@ -0,0 +1,28 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[export-unset]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. export"
6
+compare_output "export VAR=value" 'export MYVAR=hello; echo $MYVAR'
7
+compare_output "export preserves in subshell" 'export MYVAR=hello; '"$BASH_REF"' -c "echo \$MYVAR"'
8
+compare_output "export without value marks for export" 'MYVAR=test; export MYVAR; '"$BASH_REF"' -c "echo \$MYVAR"'
9
+compare_output "export multiple vars" 'export A=1 B=2 C=3; echo $A $B $C'
10
+compare_exit "export -p succeeds" 'export -p'
11
+compare_output "export overwrites existing" 'export V=old; export V=new; echo $V'
12
+compare_output "unexported var not in subshell" 'MYVAR=local; '"$BASH_REF"' -c "echo \${MYVAR:-empty}"'
13
+compare_output "export -n unexports variable" 'export MYVAR=hello; export -n MYVAR; '"$BASH_REF"' -c "echo \${MYVAR:-gone}"'
14
+
15
+section "2. unset"
16
+compare_output "unset removes variable" 'MYVAR=hello; unset MYVAR; echo ">${MYVAR}<"'
17
+compare_exit "unset nonexistent var succeeds" 'unset NONEXISTENT_VAR_XYZ_99'
18
+compare_output "unset -v removes variable" 'MYVAR=hello; unset -v MYVAR; echo ">${MYVAR}<"'
19
+compare_output "unset -f removes function" 'f() { echo hi; }; unset -f f; f 2>/dev/null; echo $?'
20
+compare_output "unset multiple vars" 'A=1; B=2; C=3; unset A C; echo ">${A}< ${B} >${C}<"'
21
+compare_output "unset array" 'arr=(a b c); unset arr; echo ">${arr[@]}<"'
22
+
23
+section "3. printenv"
24
+compare_output "printenv specific var" 'export MYVAR=hello; printenv MYVAR'
25
+compare_exit "printenv nonexistent var fails" 'printenv NONEXISTENT_VAR_XYZ_99'
26
+compare_exit "printenv with no args succeeds" 'printenv >/dev/null'
27
+
28
+print_summary
suites/builtins/portable/test_fc.shadded
@@ -0,0 +1,27 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[fc]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. fc listing"
6
+check_exit "fc -l lists history" 'fc -l' "0"
7
+check_exit "fc -l -n suppresses line numbers" 'fc -l -n' "0"
8
+check_exit "fc -l -r reverses order" 'fc -l -r' "0"
9
+skip "fc -l produces output" "requires interactive mode (no history in -c)"
10
+
11
+section "2. fc with range"
12
+check_exit "fc -l with negative offset" 'fc -l -5 2>/dev/null; true' "0"
13
+check_exit "fc -l first last range" 'fc -l 1 5 2>/dev/null; true' "0"
14
+check_exit "fc -l with prefix search" 'echo hello; fc -l echo 2>/dev/null; true' "0"
15
+
16
+section "3. fc flags combined"
17
+check_exit "fc -l -n combined" 'fc -l -n 2>/dev/null; true' "0"
18
+check_exit "fc -l -r -n all combined" 'fc -l -r -n 2>/dev/null; true' "0"
19
+
20
+section "4. fc substitution"
21
+compare_output "fc -s re-executes" 'echo testcmd123; fc -s echo 2>/dev/null || true'
22
+check_exit "fc -s with no history" 'fc -s nonexistent_xyz 2>/dev/null; true' "0"
23
+
24
+section "5. fc editor"
25
+check_exit "fc -e with EDITOR" 'FCEDIT=/bin/true; fc -e /bin/true 2>/dev/null; true' "0"
26
+
27
+print_summary
suites/builtins/portable/test_flow_control.shadded
@@ -0,0 +1,39 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[flow-control]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. exit"
6
+compare_exit "exit 0" 'exit 0'
7
+compare_exit "exit 1" 'exit 1'
8
+compare_exit "exit 42" 'exit 42'
9
+compare_exit "exit no arg uses last status" 'false; exit'
10
+compare_exit "exit 255 wraps" 'exit 255'
11
+
12
+section "2. return"
13
+compare_output "return from function" 'f() { echo before; return; echo after; }; f'
14
+compare_exit "return with code" 'f() { return 5; }; f'
15
+compare_output "return preserves code" 'f() { return 3; }; f; echo $?'
16
+compare_output "return only affects function" 'f() { return 0; }; f; echo after'
17
+compare_exit "return outside function fails" 'return 0 2>/dev/null'
18
+
19
+section "3. break"
20
+compare_output "break exits loop" 'for i in 1 2 3 4 5; do if [ $i -eq 3 ]; then break; fi; echo $i; done'
21
+compare_output "break in while loop" 'i=0; while true; do i=$((i+1)); if [ $i -eq 3 ]; then break; fi; echo $i; done'
22
+compare_output "break N exits nested loops" 'for i in 1 2; do for j in a b; do if [ "$j" = "b" ]; then break 2; fi; echo "$i$j"; done; done'
23
+compare_output "break 1 same as break" 'for i in 1 2 3; do if [ $i -eq 2 ]; then break 1; fi; echo $i; done'
24
+compare_output "break from inner loop only" 'for i in 1 2; do for j in a b c; do if [ "$j" = "b" ]; then break; fi; echo "$i$j"; done; echo "outer$i"; done'
25
+
26
+section "4. continue"
27
+compare_output "continue skips iteration" 'for i in 1 2 3 4 5; do if [ $i -eq 3 ]; then continue; fi; echo $i; done'
28
+compare_output "continue in while loop" 'i=0; while [ $i -lt 5 ]; do i=$((i+1)); if [ $i -eq 3 ]; then continue; fi; echo $i; done'
29
+compare_output "continue N in nested loops" 'for i in 1 2; do for j in a b c; do if [ "$j" = "b" ]; then continue 2; fi; echo "$i$j"; done; done'
30
+compare_output "continue 1 same as continue" 'for i in 1 2 3; do if [ $i -eq 2 ]; then continue 1; fi; echo $i; done'
31
+
32
+section "5. colon and true/false"
33
+compare_exit ": is no-op with exit 0" ':'
34
+compare_exit "true exits 0" 'true'
35
+compare_exit "false exits 1" 'false'
36
+compare_exit ": with arguments still exits 0" ': some args here'
37
+compare_output ": does not produce output" ': hello; echo $?'
38
+
39
+print_summary
suites/builtins/portable/test_getopts.shadded
@@ -0,0 +1,30 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[getopts]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. getopts basic"
6
+compare_output "getopts basic option parsing" 'f() { while getopts "ab:c" opt; do echo "$opt"; done; }; f -a -c'
7
+compare_output "getopts with option argument" 'f() { while getopts "a:b" opt; do echo "$opt=$OPTARG"; done; }; f -a value'
8
+compare_output "getopts OPTIND tracks position" 'f() { OPTIND=1; while getopts "ab" opt; do :; done; echo $OPTIND; }; f -a -b'
9
+compare_output "getopts single option" 'f() { while getopts "x" opt; do echo "$opt"; done; }; f -x'
10
+compare_output "getopts no options given" 'f() { getopts "ab" opt; echo $?; }; f'
11
+
12
+section "2. getopts combined and multiple"
13
+compare_output "getopts combined flags" 'f() { while getopts "abc" opt; do echo "$opt"; done; }; f -abc'
14
+compare_output "getopts with remaining args" 'f() { OPTIND=1; while getopts "a" opt; do echo "$opt"; done; shift $((OPTIND-1)); echo "$1"; }; f -a hello'
15
+compare_output "getopts OPTARG for colon option" 'f() { while getopts "a:" opt; do echo "$OPTARG"; done; }; f -a myval'
16
+compare_output "getopts multiple options with args" 'f() { while getopts "a:b:c" opt; do echo "$opt=$OPTARG"; done; }; f -a one -b two -c'
17
+compare_output "getopts option arg attached" 'f() { while getopts "n:" opt; do echo "$OPTARG"; done; }; f -n5'
18
+
19
+section "3. getopts error handling"
20
+compare_output "getopts unknown option" 'f() { while getopts "ab" opt 2>/dev/null; do echo "$opt"; done; }; f -c'
21
+compare_output "getopts silent mode unknown" 'f() { while getopts ":ab" opt; do echo "$opt"; done; }; f -c'
22
+compare_output "getopts silent mode missing arg" 'f() { while getopts ":a:" opt; do echo "$opt=$OPTARG"; done; }; f -a'
23
+compare_exit "getopts returns 1 when done" 'f() { set -- -a; getopts "a" opt; getopts "a" opt; echo $?; }; f'
24
+
25
+section "4. getopts OPTIND reset"
26
+compare_output "getopts OPTIND reset between calls" 'f() { OPTIND=1; while getopts "a" opt; do echo "$opt"; done; }; f -a; f -a'
27
+compare_output "getopts preserves non-option args" 'f() { OPTIND=1; while getopts "v" opt; do echo "$opt"; done; shift $((OPTIND-1)); echo "$@"; }; f -v arg1 arg2'
28
+compare_output "getopts double-dash stops parsing" 'f() { OPTIND=1; while getopts "a" opt; do echo "$opt"; done; shift $((OPTIND-1)); echo "$@"; }; f -a -- -b'
29
+
30
+print_summary
suites/builtins/portable/test_hash.shadded
@@ -0,0 +1,21 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[hash]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. hash basic"
6
+check_exit "hash with no args" 'hash' "0"
7
+check_exit "hash -r clears cache" 'hash -r' "0"
8
+compare_exit "hash specific command" 'hash ls'
9
+compare_exit "hash nonexistent command fails" 'hash nonexistent_cmd_xyz_999 2>/dev/null'
10
+compare_output "hash -r then lookup" 'hash -r; hash ls 2>/dev/null; echo $?'
11
+
12
+section "2. hash caching behavior"
13
+compare_output "hash caches after use" 'ls >/dev/null; hash -t ls 2>/dev/null || hash ls 2>/dev/null; echo $?'
14
+compare_output "hash -r clears then miss" 'ls >/dev/null; hash -r; hash -t ls 2>/dev/null; echo $?'
15
+compare_output "hash multiple commands" 'ls >/dev/null; cat /dev/null; hash ls cat 2>/dev/null; echo $?'
16
+
17
+section "3. hash error handling"
18
+compare_exit "hash nonexistent gives error" 'hash no_such_cmd_xyz 2>/dev/null'
19
+compare_output "hash after PATH change" 'hash ls 2>/dev/null; PATH=""; hash -t ls 2>/dev/null; echo $?'
20
+
21
+print_summary
suites/builtins/portable/test_history.shadded
@@ -0,0 +1,30 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[history]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. history display"
6
+check_exit "history runs without error" 'history' "0"
7
+check_exit "history N shows last N" 'history 5' "0"
8
+skip "history produces numbered output" "requires interactive mode (no history in -c)"
9
+
10
+section "2. history management"
11
+check_exit "history -c clears history" 'history -c' "0"
12
+compare_output "history -c then history shows empty" 'history -c; history | wc -l | tr -d " "'
13
+check_exit "history -d deletes entry" 'history -d 1 2>/dev/null' "0"
14
+
15
+section "3. history file operations"
16
+check_exit "history -w writes to file" "HISTFILE=$TEST_TMPDIR/hist_test; history -w" "0"
17
+check_exit "history -r reads from file" "HISTFILE=$TEST_TMPDIR/hist_test; history -r" "0"
18
+check_exit "history -a appends to file" "HISTFILE=$TEST_TMPDIR/hist_test; history -a" "0"
19
+check_output "history -w creates file" "HISTFILE=$TEST_TMPDIR/hist_w; history -w; test -f $TEST_TMPDIR/hist_w && echo yes" "yes"
20
+skip "history -w then -r round-trip" "requires interactive mode (no history in -c)"
21
+
22
+section "4. history HISTSIZE"
23
+skip "HISTSIZE limits history" "requires interactive mode (no history in -c)"
24
+
25
+section "5. history edge cases"
26
+check_exit "history with invalid flag" 'history --invalid 2>/dev/null; true' "0"
27
+check_exit "history 0 shows nothing" 'history 0' "0"
28
+check_exit "history negative number" 'history -1 2>/dev/null; true' "0"
29
+
30
+print_summary
suites/builtins/portable/test_jobs_fg_bg.shadded
@@ -0,0 +1,42 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[jobs-fg-bg]"
3
+
4
+# Job control tests require a PTY — skip in CI/non-interactive mode
5
+if [ ! -t 0 ] && [ -z "$FORCE_JOB_TESTS" ]; then
6
+    echo "Passed:  0"
7
+    echo "Failed:  0"
8
+    echo "Skipped: 0"
9
+    echo "Total:   0"
10
+    exit 0
11
+fi
12
+
13
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
14
+
15
+section "1. jobs display"
16
+check_exit "jobs with no background jobs" 'jobs' "0"
17
+compare_output "jobs shows background process" 'sleep 60 & jobs; kill %1 2>/dev/null; wait 2>/dev/null'
18
+check_exit "jobs -p shows PIDs" 'sleep 60 & jobs -p >/dev/null; kill %1 2>/dev/null; wait 2>/dev/null' "0"
19
+compare_output "jobs after completion" 'true & wait; jobs'
20
+compare_output "jobs shows multiple" 'sleep 60 & sleep 60 & jobs | wc -l | tr -d " "; kill %1 %2 2>/dev/null; wait 2>/dev/null'
21
+compare_output "jobs -p output is numeric" 'sleep 60 & jobs -p | grep -qE "^[0-9]+$" && echo yes; kill %1 2>/dev/null; wait 2>/dev/null'
22
+
23
+section "2. fg"
24
+compare_exit "fg with no jobs fails" 'fg 2>/dev/null'
25
+compare_output "fg brings job to foreground" 'sleep 0.1 & fg %1 >/dev/null 2>&1; echo $?'
26
+compare_exit "fg invalid job spec fails" 'fg %99 2>/dev/null'
27
+
28
+section "3. bg"
29
+compare_exit "bg with no stopped jobs fails" 'bg 2>/dev/null'
30
+compare_exit "bg invalid job spec fails" 'bg %99 2>/dev/null'
31
+
32
+section "4. job spec parsing"
33
+compare_output "kill by job spec %1" 'sleep 60 & kill %1 2>/dev/null; wait 2>/dev/null; echo done'
34
+compare_output "multiple background jobs" 'sleep 60 & sleep 60 & kill %1 %2 2>/dev/null; wait 2>/dev/null; echo done'
35
+compare_output "job numbering sequential" 'sleep 60 & sleep 60 & sleep 60 & kill %1 %2 %3 2>/dev/null; wait 2>/dev/null; echo done'
36
+
37
+section "5. background execution"
38
+compare_output "command runs in background" '(echo bg_done) & wait; echo fg_done'
39
+compare_output "background preserves exit" '(exit 42) & wait $!; echo $?'
40
+compare_output "dollar-bang tracks PID" 'sleep 0.1 & test -n "$!" && echo yes'
41
+
42
+print_summary
suites/builtins/portable/test_kill_wait.shadded
@@ -0,0 +1,32 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[kill-wait]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. kill signals"
6
+check_exit "kill -l lists signals" 'kill -l' "0"
7
+compare_exit "kill nonexistent PID fails" 'kill -0 999999 2>/dev/null'
8
+compare_output "kill -l output has TERM" 'kill -l | grep -q TERM && echo yes'
9
+compare_output "kill -0 checks process exists" 'sleep 60 & pid=$!; kill -0 $pid && echo alive; kill $pid; wait $pid 2>/dev/null'
10
+compare_output "kill sends TERM by default" 'sleep 60 & pid=$!; kill $pid; wait $pid 2>/dev/null; echo done'
11
+compare_output "kill -9 sends SIGKILL" 'sleep 60 & pid=$!; kill -9 $pid; wait $pid 2>/dev/null; echo done'
12
+compare_output "kill -s TERM sends TERM" 'sleep 60 & pid=$!; kill -s TERM $pid; wait $pid 2>/dev/null; echo done'
13
+compare_exit "kill with invalid signal" 'kill -INVALID 1 2>/dev/null'
14
+
15
+section "2. wait basic"
16
+compare_output "wait for background job" 'echo start; true & wait; echo done'
17
+compare_exit "wait returns bg exit status 0" 'true & wait $!'
18
+compare_exit "wait returns bg exit status 1" 'false & wait $!'
19
+compare_output "wait with no bg jobs succeeds" 'wait; echo $?'
20
+compare_exit "wait specific PID" 'sleep 0.1 & wait $!'
21
+
22
+section "3. wait multiple"
23
+compare_output "wait all background jobs" 'true & true & wait; echo done'
24
+compare_output "wait specific among multiple" 'sleep 0.1 & p1=$!; sleep 0.1 & p2=$!; wait $p1; echo "p1=$?"; wait $p2; echo "p2=$?"'
25
+compare_exit "wait for already-exited process" 'true & pid=$!; sleep 0.1; wait $pid'
26
+
27
+section "4. wait exit status"
28
+compare_output "wait captures exit 42" '(exit 42) & wait $!; echo $?'
29
+compare_output "wait captures exit 0" '(exit 0) & wait $!; echo $?'
30
+compare_output "dollar-bang is last bg PID" 'sleep 0.1 & echo $! | grep -qE "^[0-9]+$" && echo valid'
31
+
32
+print_summary
suites/builtins/portable/test_local.shadded
@@ -0,0 +1,24 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[local]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. local scope"
6
+compare_output "local restricts scope to function" 'f() { local x=inner; echo $x; }; f; echo ">${x}<"'
7
+compare_output "local with initial value" 'f() { local msg=hello; echo $msg; }; f'
8
+compare_output "local does not leak outside" 'f() { local secret=42; }; f; echo ">${secret}<"'
9
+compare_output "local shadows outer variable" 'x=outer; f() { local x=inner; echo $x; }; f; echo $x'
10
+compare_exit "local outside function fails" 'local x=5 2>/dev/null'
11
+
12
+section "2. local advanced"
13
+compare_output "local without value" 'f() { local x; echo ">${x}<"; }; f'
14
+compare_output "local multiple vars" 'f() { local a=1 b=2; echo $a $b; }; f'
15
+compare_output "local preserves outer after return" 'x=outer; f() { local x=inner; return; }; f; echo $x'
16
+compare_output "nested function locals" 'g() { local x=inner2; echo $x; }; f() { local x=inner1; g; echo $x; }; f'
17
+compare_output "local in nested calls" 'x=global; f() { local x=f_val; g; echo $x; }; g() { echo $x; }; f; echo $x'
18
+
19
+section "3. local with types"
20
+compare_output "local -i integer" 'f() { local -i n=5+3; echo $n; }; f'
21
+compare_output "local -a array" 'f() { local -a arr=(a b c); echo ${arr[@]}; }; f'
22
+compare_output "local -r readonly" 'f() { local -r x=fixed; echo $x; }; f'
23
+
24
+print_summary
suites/builtins/portable/test_printf.shadded
@@ -0,0 +1,47 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[printf]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. printf format specifiers"
6
+compare_output "printf %s string" 'printf "%s\n" hello'
7
+compare_output "printf %d decimal" 'printf "%d\n" 42'
8
+compare_output "printf %x hex lowercase" 'printf "%x\n" 255'
9
+compare_output "printf %X hex uppercase" 'printf "%X\n" 255'
10
+compare_output "printf %o octal" 'printf "%o\n" 8'
11
+compare_output "printf %c character" 'printf "%c\n" A'
12
+compare_output "printf %i integer" 'printf "%i\n" 42'
13
+
14
+section "2. printf width and precision"
15
+compare_output "printf left-aligned %-10s" 'printf "[%-10s]\n" hi'
16
+compare_output "printf right-aligned %10s" 'printf "[%10s]\n" hi'
17
+compare_output "printf precision truncate %.5s" 'printf "%.5s\n" "hello world"'
18
+compare_output "printf zero-padded %05d" 'printf "%05d\n" 42'
19
+compare_output "printf width with string %8s" 'printf "[%8s]\n" "abc"'
20
+compare_output "printf negative number" 'printf "%d\n" -5'
21
+
22
+section "3. printf escape sequences"
23
+compare_output "printf newline in format" 'printf "a\nb\n"'
24
+compare_output "printf tab in format" 'printf "a\tb\n"'
25
+compare_output "printf backslash in format" 'printf "a\\\\b\n"'
26
+compare_output "printf carriage return" 'printf "hello\rworld\n"'
27
+compare_output "printf literal percent" 'printf "100%%\n"'
28
+
29
+section "4. printf multiple args and %b"
30
+compare_output "printf recycles format for multiple args" 'printf "%s\n" a b c'
31
+compare_output "printf %b interprets escapes in arg" 'printf "%b\n" "hello\nworld"'
32
+compare_output "printf multiple %s in format" 'printf "%s=%s\n" key val'
33
+compare_output "printf mixed format" 'printf "%s is %d\n" age 25'
34
+
35
+section "5. printf error handling"
36
+compare_exit "printf missing format string" 'printf'
37
+compare_output "printf missing arg uses default" 'printf "%s %d\n"'
38
+compare_output "printf extra args recycle" 'printf "%s\n" a b c d'
39
+check_output "printf %d with non-numeric arg" 'printf "%d\n" abc 2>&1' "fortsh: printf: abc: invalid number
40
+0"
41
+
42
+section "6. printf special formats"
43
+compare_output "printf %q shell-quoted string" 'printf "%q\n" "hello world"'
44
+compare_output "printf octal escape in format" 'printf "\101\n"'
45
+compare_output "printf hex escape in format" 'printf "\x41\n"'
46
+
47
+print_summary
suites/builtins/portable/test_pushd_popd_dirs.shadded
@@ -0,0 +1,39 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[pushd-popd-dirs]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. pushd basic"
6
+compare_output "pushd changes directory" 'pushd /tmp >/dev/null && pwd'
7
+compare_output "pushd prints stack" 'pushd /tmp 2>/dev/null'
8
+compare_output "pushd to HOME with tilde" 'pushd /tmp >/dev/null; pushd ~ >/dev/null && pwd'
9
+compare_exit "pushd to nonexistent dir fails" 'pushd /nonexistent_xyz_12345 2>/dev/null'
10
+compare_output "pushd swaps top two with no arg" 'pushd /tmp >/dev/null; pushd /var >/dev/null; pushd >/dev/null; pwd'
11
+compare_output "pushd -n suppresses cd" 'cd /tmp; pushd -n /var 2>/dev/null; pwd'
12
+compare_output "pushd to root" 'pushd / >/dev/null && pwd'
13
+compare_output "pushd multiple dirs" 'pushd /tmp >/dev/null; pushd /var >/dev/null; pushd / >/dev/null; pwd'
14
+
15
+section "2. popd basic"
16
+compare_output "popd returns to previous dir" 'pushd /tmp >/dev/null; pushd /var >/dev/null; popd >/dev/null; pwd'
17
+compare_exit "popd on empty stack fails" 'popd 2>/dev/null'
18
+compare_output "popd -n suppresses cd" 'pushd /tmp >/dev/null; pushd /var >/dev/null; popd -n >/dev/null; pwd'
19
+compare_output "multiple pushd then popd" 'pushd /tmp >/dev/null; pushd /var >/dev/null; pushd / >/dev/null; popd >/dev/null; popd >/dev/null; pwd'
20
+compare_output "popd all the way back" 'ORIG=$(pwd); pushd /tmp >/dev/null; pushd /var >/dev/null; popd >/dev/null; popd >/dev/null; pwd'
21
+
22
+section "3. popd with index"
23
+compare_output "popd +0 removes top" 'pushd /tmp >/dev/null; pushd /var >/dev/null; popd +0 >/dev/null; pwd'
24
+compare_output "popd +1 removes second" 'pushd /tmp >/dev/null; pushd /var >/dev/null; popd +1 >/dev/null 2>/dev/null; pwd'
25
+
26
+section "4. dirs"
27
+compare_exit "dirs shows current dir" 'dirs'
28
+compare_output "dirs -c clears stack" 'pushd /tmp >/dev/null; dirs -c; dirs'
29
+compare_output "dirs -p one per line" 'pushd /tmp >/dev/null; dirs -p'
30
+compare_output "dirs -v numbered" 'pushd /tmp >/dev/null; dirs -v'
31
+compare_output "dirs after pushd shows stack" 'pushd /tmp >/dev/null; pushd /var >/dev/null; dirs'
32
+compare_output "dirs -l long format" 'pushd /tmp >/dev/null; dirs -l'
33
+compare_output "dirs with no stack shows cwd" 'dirs -c; dirs'
34
+
35
+section "5. directory stack round-trip"
36
+compare_output "pushd/popd preserves original" 'ORIG=$(pwd); pushd /tmp >/dev/null; pushd /var >/dev/null; popd >/dev/null; popd >/dev/null; test "$(pwd)" = "$ORIG" && echo yes'
37
+compare_output "deep stack round-trip" 'ORIG=$(pwd); pushd /tmp >/dev/null; pushd /var >/dev/null; pushd / >/dev/null; popd >/dev/null; popd >/dev/null; popd >/dev/null; test "$(pwd)" = "$ORIG" && echo yes'
38
+
39
+print_summary
suites/builtins/portable/test_read.shadded
@@ -0,0 +1,39 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[read]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. read basic"
6
+compare_output "read from herestring" 'read VAR <<< "hello"; echo $VAR'
7
+compare_output "read multiple vars" 'read A B C <<< "one two three"; echo "$A|$B|$C"'
8
+compare_output "read excess into last var" 'read A B <<< "one two three four"; echo "$A|$B"'
9
+compare_output "read single var gets all" 'read LINE <<< "hello world test"; echo "$LINE"'
10
+compare_output "read with empty input" 'read VAR <<< ""; echo ">${VAR}<"'
11
+
12
+section "2. read flags"
13
+compare_output "read -r preserves backslash" 'read -r VAR <<< "hello\\world"; echo "$VAR"'
14
+compare_output "read without -r interprets backslash" 'read VAR <<< "hello\\\\world"; echo "$VAR"'
15
+compare_output "read -a into array" 'read -a ARR <<< "a b c"; echo ${ARR[0]} ${ARR[1]} ${ARR[2]}'
16
+compare_output "read -a array length" 'read -a ARR <<< "x y z"; echo ${#ARR[@]}'
17
+
18
+section "3. read with IFS"
19
+compare_output "read with colon IFS" 'IFS=: read A B C <<< "x:y:z"; echo "$A|$B|$C"'
20
+compare_output "read with comma IFS" 'IFS=, read A B C <<< "a,b,c"; echo "$A|$B|$C"'
21
+compare_output "read with custom IFS excess" 'IFS=: read A B <<< "x:y:z"; echo "$A|$B"'
22
+compare_output "read with space IFS default" 'read A B <<< "  hello   world  "; echo ">$A<>$B<"'
23
+
24
+section "4. read from heredoc"
25
+compare_output "read from heredoc" 'read VAR << EOF
26
+hello
27
+EOF
28
+echo $VAR'
29
+compare_output "read loop from heredoc" 'while read line; do echo "got:$line"; done << EOF
30
+alpha
31
+beta
32
+EOF'
33
+
34
+section "5. read edge cases"
35
+compare_exit "read with no input returns 1" 'echo -n "" | read VAR'
36
+compare_output "read preserves whitespace with IFS empty" 'IFS= read line <<< "  spaces  "; echo ">$line<"'
37
+compare_output "read -r with trailing backslash" 'read -r VAR <<< "end\\"; echo "$VAR"'
38
+
39
+print_summary
suites/builtins/portable/test_readonly.shadded
@@ -0,0 +1,21 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[readonly]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. readonly basic"
6
+compare_output "readonly VAR=value preserves value" 'readonly MYRO=hello; echo $MYRO'
7
+compare_output "readonly existing var" 'MYVAR=test; readonly MYVAR; echo $MYVAR'
8
+compare_output "readonly with empty value" 'readonly MYRO=""; echo ">${MYRO}<"'
9
+compare_output "readonly multiple vars" 'readonly A=1 B=2; echo $A $B'
10
+
11
+section "2. readonly enforcement"
12
+compare_exit "modify readonly var fails" 'readonly MYRO=hello; MYRO=world 2>/dev/null'
13
+compare_exit "unset readonly var fails" 'readonly MYRO=hello; unset MYRO 2>/dev/null'
14
+compare_output "readonly var in subshell" 'readonly MYRO=hello; echo $MYRO'
15
+compare_exit "export readonly var succeeds" 'readonly MYRO=hello; export MYRO 2>/dev/null'
16
+
17
+section "3. readonly listing"
18
+check_exit "readonly -p lists vars" 'readonly -p' "0"
19
+compare_output "readonly -p shows declared vars" 'readonly TESTRO=abc; readonly -p | grep TESTRO'
20
+
21
+print_summary
suites/builtins/portable/test_set_shopt.shadded
@@ -0,0 +1,30 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[set-shopt]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. set options"
6
+compare_exit "set -e exits on error" 'set -e; false'
7
+compare_output "set -e does not exit on conditional" 'set -e; if false; then echo no; fi; echo ok'
8
+compare_exit "set -u errors on unset var" 'set -u; echo ${NONEXISTENT_XYZ_999} 2>/dev/null'
9
+compare_exit "set -u triggers failure for unset" 'set -u; echo $NONEXISTENT_XYZ_999 2>/dev/null'
10
+compare_exit "set -o pipefail catches pipe failure" 'set -o pipefail; false | true'
11
+compare_output "set -o pipefail success" 'set -o pipefail; true | true; echo $?'
12
+
13
+section "2. set positional parameters"
14
+compare_output "set -- sets positional params" 'set -- a b c; echo $1 $2 $3'
15
+compare_output "set -- count" 'set -- x y z; echo $#'
16
+compare_output "set -- overwrites previous" 'set -- a b; set -- x y z; echo $1 $#'
17
+compare_output "set -- empty clears params" 'set -- a b c; set --; echo $#'
18
+compare_output "set -- with special chars" 'set -- "hello world" foo; echo "$1"'
19
+compare_output "dollar-at expansion" 'set -- a b c; for x in "$@"; do echo $x; done'
20
+compare_output "dollar-star expansion" 'set -- a b c; echo "$*"'
21
+
22
+section "3. dollar-dash options string"
23
+compare_output "dollar-dash shows options" 'set -u; case $- in *u*) echo yes;; *) echo no;; esac'
24
+
25
+section "4. shopt"
26
+check_exit "shopt -s extglob" 'shopt -s extglob 2>/dev/null; echo done' "0"
27
+compare_exit "shopt -q queries option" 'shopt -q login_shell 2>/dev/null; true'
28
+compare_exit "shopt -u unsets option" 'shopt -s extglob 2>/dev/null; shopt -u extglob 2>/dev/null; true'
29
+
30
+print_summary
suites/builtins/portable/test_shift.shadded
@@ -0,0 +1,19 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[shift]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. shift basic"
6
+compare_output "shift removes first param" 'set -- a b c; shift; echo $1'
7
+compare_output "shift updates count" 'set -- a b c; shift; echo $#'
8
+compare_output "shift N removes N params" 'set -- a b c d e; shift 2; echo $1'
9
+compare_output "shift all params" 'set -- a b c; shift 3; echo $#'
10
+compare_output "shift preserves remaining" 'set -- a b c d; shift 2; echo "$@"'
11
+
12
+section "2. shift edge cases"
13
+compare_exit "shift past available params fails" 'set -- a; shift 5 2>/dev/null'
14
+compare_output "shift 0 is no-op" 'set -- a b c; shift 0; echo $1'
15
+compare_output "shift in loop" 'set -- a b c; while [ $# -gt 0 ]; do echo $1; shift; done'
16
+compare_output "shift with no params" 'set --; shift 2>/dev/null; echo $#'
17
+compare_output "multiple shifts" 'set -- a b c d e; shift; shift; echo $1 $#'
18
+
19
+print_summary
suites/builtins/portable/test_source.shadded
@@ -0,0 +1,29 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[source]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+# Create test files
6
+printf 'SOURCED_VAR=hello\n' > "$TEST_TMPDIR/source_var.sh"
7
+printf 'greet() { echo "hi $1"; }\n' > "$TEST_TMPDIR/source_func.sh"
8
+printf 'echo "arg1=$1 arg2=$2"\n' > "$TEST_TMPDIR/source_args.sh"
9
+printf 'X=10\nY=20\necho $((X + Y))\n' > "$TEST_TMPDIR/source_multi.sh"
10
+printf 'return 42\n' > "$TEST_TMPDIR/source_return.sh"
11
+
12
+section "1. source basic"
13
+compare_output "source file sets variable" "source $TEST_TMPDIR/source_var.sh; echo \$SOURCED_VAR"
14
+compare_output "source file defines function" "source $TEST_TMPDIR/source_func.sh; greet world"
15
+compare_exit "source nonexistent file fails" "source $TEST_TMPDIR/no_such_file.sh 2>/dev/null"
16
+compare_output "source with arguments" "source $TEST_TMPDIR/source_args.sh foo bar"
17
+compare_output "source multiple assignments" "source $TEST_TMPDIR/source_multi.sh"
18
+
19
+section "2. dot command"
20
+compare_output "dot command works like source" ". $TEST_TMPDIR/source_var.sh; echo \$SOURCED_VAR"
21
+compare_output "dot with function def" ". $TEST_TMPDIR/source_func.sh; greet user"
22
+compare_exit "dot nonexistent file fails" ". $TEST_TMPDIR/no_such_file.sh 2>/dev/null"
23
+
24
+section "3. source edge cases"
25
+compare_exit "source file with return" "source $TEST_TMPDIR/source_return.sh"
26
+compare_output "source preserves env" "A=before; printf 'A=after\n' > $TEST_TMPDIR/s.sh; source $TEST_TMPDIR/s.sh; echo \$A"
27
+compare_output "source in function" "f() { source $TEST_TMPDIR/source_var.sh; echo \$SOURCED_VAR; }; f"
28
+
29
+print_summary
suites/builtins/portable/test_stress.shadded
@@ -0,0 +1,863 @@
1
+#!/bin/sh
2
+# ============================================================
3
+# Stress tests for allocatable string / widened buffer changes
4
+# ============================================================
5
+# These tests verify that fortsh handles large values, deep
6
+# nesting, many variables, and boundary conditions correctly
7
+# after the MAX_VAR_VALUE_LEN 1024->4096 refactoring.
8
+#
9
+# Tests marked with skip() are known pre-existing issues not
10
+# related to the buffer refactoring.
11
+
12
+TEST_PREFIX="STRESS"
13
+SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
14
+. "$SCRIPT_DIR/test_harness.sh"
15
+
16
+# Platform-aware timeouts: ARM64 (flang-new) is ~300x slower on loops
17
+case "$(uname -m)" in
18
+  arm64|aarch64) TEST_TIMEOUT=90 ;;
19
+  *)             TEST_TIMEOUT=30 ;;
20
+esac
21
+
22
+# ==========================================
23
+# 1. Long variable values
24
+# ==========================================
25
+section "1 - Long variable values"
26
+
27
+compare_output "2000-char variable via printf %0*d" \
28
+  'x=$(printf "%0*d" 2000 0); echo ${#x}'
29
+
30
+compare_output "3000-char variable via printf %0*d" \
31
+  'x=$(printf "%0*d" 3000 0); echo ${#x}'
32
+
33
+compare_output "4000-char variable (near limit)" \
34
+  'x=$(printf "%0*d" 4000 0); echo ${#x}'
35
+
36
+compare_output "variable with 1000 'a' chars" \
37
+  'x=$(printf "a%.0s" $(seq 1 1000)); echo ${#x}'
38
+
39
+# printf "a%.0s" with many args hits command expansion buffer
40
+# use python or head /dev/zero instead for large single-char strings
41
+compare_output "2000-char variable via head" \
42
+  'x=$(head -c 2000 /dev/zero | tr "\0" "a"); echo ${#x}'
43
+
44
+compare_output "3000-char variable via head" \
45
+  'x=$(head -c 3000 /dev/zero | tr "\0" "a"); echo ${#x}'
46
+
47
+# BUG: printf "a%.0s" with many args truncates at ~1041 chars (#37)
48
+compare_output "2000-char via printf repeat" \
49
+  'x=$(printf "a%.0s" $(seq 1 2000)); echo ${#x}'
50
+
51
+compare_output "long variable substring" \
52
+  'x=$(printf "%0*d" 3000 0); echo ${#x}; y=${x:500:1000}; echo ${#y}'
53
+
54
+compare_output "long variable pattern removal" \
55
+  'x="aaa$(printf "%0*d" 2000 0)aaa"; y=${x##aaa}; echo ${#y}'
56
+
57
+# ==========================================
58
+# 2. Long command lines
59
+# ==========================================
60
+section "2 - Long command lines"
61
+
62
+compare_output "echo with 500 args" \
63
+  'echo $(seq 1 500) | wc -w'
64
+
65
+compare_output "echo with 1000 args" \
66
+  'echo $(seq 1 1000) | wc -w'
67
+
68
+compare_output "long pipe chain" \
69
+  'echo hello | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'
70
+
71
+compare_output "long command with semicolons" \
72
+  'a=1; b=2; c=3; d=4; e=5; f=6; g=7; h=8; i=9; j=10; echo $a $b $c $d $e $f $g $h $i $j'
73
+
74
+# ==========================================
75
+# 3. Array stress tests
76
+# ==========================================
77
+section "3 - Array stress tests"
78
+
79
+# Use explicit indices to avoid arr[$i]= parsing differences
80
+compare_output "array direct assignment 10 elements" \
81
+  'arr=(a b c d e f g h i j); echo ${#arr[@]}'
82
+
83
+compare_output "array with explicit long values" \
84
+  'x=$(printf "%0*d" 100 0); arr[0]=$x; arr[1]=$x; echo ${#arr[0]} ${#arr[1]}'
85
+
86
+# BUG: array element assignment truncates long values (#35)
87
+compare_output "array element from command substitution" \
88
+  'arr[0]=$(printf "%0*d" 1000 0); echo ${#arr[0]}'
89
+
90
+compare_output "array append" \
91
+  'arr=(); for i in $(seq 1 30); do arr+=("item$i"); done; echo ${#arr[@]}'
92
+
93
+compare_output "array slice" \
94
+  'arr=(a b c d e f g h i j); echo ${arr[@]:3:4}'
95
+
96
+compare_output "unset array elements" \
97
+  'arr=(1 2 3 4 5 6 7 8 9 10); unset arr[4]; unset arr[7]; echo ${#arr[@]}'
98
+
99
+# ==========================================
100
+# 4. Associative array stress tests
101
+# ==========================================
102
+section "4 - Associative array stress tests"
103
+
104
+compare_output "basic assoc array" \
105
+  'declare -A m; m[a]=1; m[b]=2; m[c]=3; echo ${#m[@]}'
106
+
107
+compare_output "assoc array with long value via var" \
108
+  'declare -A m; x=$(printf "%0*d" 1000 0); m[k]=$x; echo ${#m[k]}'
109
+
110
+# BUG: assoc element assignment truncates long values (#35)
111
+compare_output "assoc array element from command substitution" \
112
+  'declare -A m; m[x]=$(printf "%0*d" 1000 0); echo ${#m[x]}'
113
+
114
+compare_output "assoc array overwrite" \
115
+  'declare -A m; m[k]=old; m[k]=new; echo ${m[k]}'
116
+
117
+# ==========================================
118
+# 5. Deep nesting
119
+# ==========================================
120
+section "5 - Deep nesting"
121
+
122
+compare_output "nested if 10 levels" \
123
+  'x=1; if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then echo deep; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi'
124
+
125
+compare_output "nested loops 5 levels" \
126
+  'for a in 1 2; do for b in 1 2; do for c in 1 2; do for d in 1 2; do for e in 1 2; do echo "$a$b$c$d$e"; done; done; done; done; done | wc -l'
127
+
128
+compare_output "nested command substitution 5 levels" \
129
+  'echo $(echo $(echo $(echo $(echo hello))))'
130
+
131
+compare_output "nested case in loops" \
132
+  'for i in a b c; do case $i in a) echo A;; b) for j in 1 2; do case $j in 1) echo B1;; 2) echo B2;; esac; done;; c) echo C;; esac; done'
133
+
134
+# ==========================================
135
+# 6. Many variables
136
+# ==========================================
137
+section "6 - Many variables"
138
+
139
+compare_output "100 variables" \
140
+  'for i in $(seq 1 100); do eval "var_$i=$i"; done; echo $var_1 $var_50 $var_100'
141
+
142
+compare_output "export 50 variables" \
143
+  'for i in $(seq 1 50); do export "ev_$i=$i"; done; env | grep "^ev_" | wc -l'
144
+
145
+compare_output "rapid set/unset cycle" \
146
+  'for i in $(seq 1 50); do x=$i; unset x; done; echo ${x:-unset}'
147
+
148
+# ==========================================
149
+# 7. Function stress tests
150
+# ==========================================
151
+section "7 - Function stress tests"
152
+
153
+compare_output "function with many params" \
154
+  'f() { echo $# $1 $5 ${10}; }; f a b c d e f g h i j'
155
+
156
+compare_output "recursive function 10 levels" \
157
+  'f() { if [ $1 -le 0 ]; then echo done; else f $(($1 - 1)); fi; }; f 10'
158
+
159
+compare_output "recursive function 20 levels" \
160
+  'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 20'
161
+
162
+# BUG: recursive function 50 levels causes core dump (#36)
163
+compare_output "recursive function 50 levels" \
164
+  'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 50'
165
+
166
+compare_output "function redefine in loop" \
167
+  'for i in 1 2 3; do eval "f() { echo $i; }"; f; done'
168
+
169
+compare_output "function local var scoping" \
170
+  'f() { local x=inner; echo $x; }; x=outer; f; echo $x'
171
+
172
+compare_output "nested function calls 5 levels" \
173
+  'a() { echo a; b; }; b() { echo b; c; }; c() { echo c; d; }; d() { echo d; e; }; e() { echo e; }; a'
174
+
175
+# ==========================================
176
+# 8. Alias stress tests
177
+# ==========================================
178
+section "8 - Alias stress tests"
179
+
180
+check_output "alias with long command" \
181
+  'shopt -s expand_aliases; alias longcmd="echo hello_from_alias"; longcmd' \
182
+  'hello_from_alias'
183
+
184
+compare_output "many aliases" \
185
+  'shopt -s expand_aliases; for i in $(seq 1 20); do alias "a$i=echo $i"; done; alias | wc -l'
186
+
187
+# ==========================================
188
+# 9. Trap stress tests
189
+# ==========================================
190
+section "9 - Trap stress tests"
191
+
192
+compare_output "EXIT trap fires" \
193
+  'trap "echo trapped" EXIT; true'
194
+
195
+compare_output "multiple trap signals" \
196
+  'trap "echo exit" EXIT; trap "echo hup" HUP; trap "echo usr1" USR1; trap -p | grep -cE "(EXIT|HUP|USR1)"'
197
+
198
+compare_output "trap set/unset cycle" \
199
+  'for i in $(seq 1 10); do trap "echo $i" EXIT; done; trap -p EXIT'
200
+
201
+# ==========================================
202
+# 10. Heredoc stress tests
203
+# ==========================================
204
+section "10 - Heredoc stress tests"
205
+
206
+compare_output "heredoc basic" \
207
+  'cat <<EOF
208
+line1
209
+line2
210
+line3
211
+EOF'
212
+
213
+compare_output "heredoc with variable expansion" \
214
+  'x=world; cat <<EOF
215
+hello $x
216
+EOF'
217
+
218
+compare_output "heredoc with long delimiter" \
219
+  'cat <<VERYLONGDELIMITERTHATISTOTALLYUNNECESSARY
220
+hello
221
+VERYLONGDELIMITERTHATISTOTALLYUNNECESSARY'
222
+
223
+# ==========================================
224
+# 11. String operations on large values
225
+# ==========================================
226
+section "11 - String operations"
227
+
228
+compare_output "string length of 3000-char var" \
229
+  'x=$(head -c 3000 /dev/zero | tr "\0" "a"); echo ${#x}'
230
+
231
+compare_output "uppercase transform large string" \
232
+  'x=$(head -c 1000 /dev/zero | tr "\0" "a"); echo ${x^^} | wc -c'
233
+
234
+compare_output "string replacement large string" \
235
+  'x=$(head -c 1000 /dev/zero | tr "\0" "a"); echo ${x//a/b} | wc -c'
236
+
237
+compare_output "concatenation stress" \
238
+  'x=""; for i in $(seq 1 100); do x="${x}abc"; done; echo ${#x}'
239
+
240
+# ==========================================
241
+# 12. Parameter expansion stress
242
+# ==========================================
243
+section "12 - Parameter expansion stress"
244
+
245
+compare_output "default value expansion chain" \
246
+  'echo ${a:-${b:-${c:-${d:-${e:-deep}}}}}'
247
+
248
+compare_output "indirect expansion" \
249
+  'x=hello; ref=x; echo ${!ref}'
250
+
251
+compare_output "many positional params" \
252
+  'set -- $(seq 1 50); echo $# $1 ${25} ${50}'
253
+
254
+compare_output "shift through many params" \
255
+  'set -- $(seq 1 30); shift 15; echo $1 $#'
256
+
257
+compare_output "all positional params" \
258
+  'set -- $(seq 1 20); echo "$@" | wc -w'
259
+
260
+# ==========================================
261
+# 13. Pipeline stress
262
+# ==========================================
263
+section "13 - Pipeline stress"
264
+
265
+compare_output "10-stage pipeline" \
266
+  'seq 1 100 | sort -n | head -50 | tail -10 | wc -l'
267
+
268
+compare_output "pipeline with large data" \
269
+  'seq 1 10000 | wc -l'
270
+
271
+compare_output "pipeline with grep chain" \
272
+  'seq 1 1000 | grep 5 | grep -v 50 | wc -l'
273
+
274
+# ==========================================
275
+# 14. Brace expansion stress
276
+# ==========================================
277
+section "14 - Brace expansion stress"
278
+
279
+compare_output "large sequence" \
280
+  'echo {1..100} | wc -w'
281
+
282
+compare_output "nested braces" \
283
+  'echo {a,b}{1,2,3}{x,y} | wc -w'
284
+
285
+compare_output "alpha sequence" \
286
+  'echo {a..z} | wc -w'
287
+
288
+# ==========================================
289
+# 15. Boundary conditions
290
+# ==========================================
291
+section "15 - Boundary conditions"
292
+
293
+compare_output "empty variable operations" \
294
+  'x=""; echo "${#x}" "${x:-default}" "${x:+set}"'
295
+
296
+compare_output "single char variable" \
297
+  'x=a; echo ${#x} ${x}'
298
+
299
+compare_output "null byte handling" \
300
+  'printf "a\0b" | wc -c'
301
+
302
+compare_output "empty command substitution" \
303
+  'x=$(true); echo "[$x]"'
304
+
305
+compare_output "whitespace-only variable" \
306
+  'x="   "; echo "${#x}" "$x"'
307
+
308
+compare_output "long assignment value" \
309
+  'x=$(head -c 2000 /dev/zero | tr "\0" "z"); echo ${#x}'
310
+
311
+compare_output "variable with special chars" \
312
+  'x="hello'\''world\"test"; echo "$x"'
313
+
314
+# ==========================================
315
+# 16. Pipeline overflow (formerly 4096 ceiling)
316
+# ==========================================
317
+section "16 - Pipeline overflow (>4096 byte values)"
318
+
319
+compare_output "5000-char variable via head" \
320
+  'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${#x}'
321
+
322
+compare_output "10000-char variable via head" \
323
+  'x=$(head -c 10000 /dev/zero | tr "\0" "a"); echo ${#x}'
324
+
325
+compare_output "5000-char in echo" \
326
+  'x=$(head -c 5000 /dev/zero | tr "\0" "b"); echo ${#x}'
327
+
328
+compare_output "8000-char variable via python" \
329
+  'x=$(python3 -c "print(\"c\"*8000, end=\"\")"); echo ${#x}'
330
+
331
+compare_output "5000-char array assignment via word splitting" \
332
+  'x=$(head -c 5000 /dev/zero | tr "\0" "d"); arr=($x); echo ${#arr[0]}'
333
+
334
+compare_output "10000-char array element" \
335
+  'x=$(head -c 10000 /dev/zero | tr "\0" "e"); arr=($x); echo ${#arr[0]}'
336
+
337
+compare_output "multiple 5000-char array elements" \
338
+  'a=$(head -c 5000 /dev/zero | tr "\0" "f"); b=$(head -c 5000 /dev/zero | tr "\0" "g"); arr=($a $b); echo ${#arr[0]} ${#arr[1]}'
339
+
340
+compare_output "5000-char variable string length" \
341
+  'x=$(head -c 5000 /dev/zero | tr "\0" "h"); echo ${#x}'
342
+
343
+compare_output "5000-char variable substring" \
344
+  'x=$(head -c 5000 /dev/zero | tr "\0" "i"); y=${x:1000:2000}; echo ${#y}'
345
+
346
+compare_output "command substitution preserves 10000 bytes" \
347
+  'echo $(head -c 10000 /dev/zero | tr "\0" "j") | wc -c'
348
+
349
+compare_output "nested command sub with large output" \
350
+  'x=$(echo $(head -c 5000 /dev/zero | tr "\0" "k")); echo ${#x}'
351
+
352
+compare_output "export 5000-char variable" \
353
+  'export BIGVAR=$(head -c 5000 /dev/zero | tr "\0" "m"); echo ${#BIGVAR}'
354
+
355
+# ==========================================
356
+# 17. Indirect expansion edge cases
357
+# ==========================================
358
+section "17 - Indirect expansion edge cases"
359
+
360
+compare_output "indirect to unset variable" \
361
+  'ref=nosuchvar; echo "${!ref}"'
362
+
363
+compare_output "indirect with default value" \
364
+  'x=hello; ref=x; echo ${!ref:-fallback}'
365
+
366
+compare_output "indirect chain (not recursive)" \
367
+  'a=hello; b=a; echo ${!b}'
368
+
369
+compare_output "indirect to empty variable" \
370
+  'x=""; ref=x; echo "[${!ref}]"'
371
+
372
+compare_output "indirect to integer variable" \
373
+  'x=42; ref=x; echo ${!ref}'
374
+
375
+compare_output "indirect to PATH" \
376
+  'ref=PATH; val=${!ref}; echo "${val:0:1}"'
377
+
378
+compare_output "indirect with nounset on valid ref" \
379
+  'set -u; x=hello; ref=x; echo ${!ref}; set +u'
380
+
381
+# ==========================================
382
+# 18. Hardcoded limit boundary tests
383
+# ==========================================
384
+section "18 - Hardcoded limit boundaries"
385
+
386
+# Case statement patterns (grammar_parser patterns(10) limit)
387
+# NOTE: increasing case limits causes stack frame overflow in nested parsing.
388
+# Keeping limits at patterns(10)/items(20) to avoid stack corruption.
389
+# Issue tracked for future heap-allocation refactor.
390
+compare_output "case with 9 patterns" \
391
+  'x=9; case $x in 1) echo a;; 2) echo b;; 3) echo c;; 4) echo d;; 5) echo e;; 6) echo f;; 7) echo g;; 8) echo h;; 9) echo i;; esac'
392
+
393
+compare_output "case with 10 patterns" \
394
+  'x=10; case $x in 1) echo a;; 2) echo b;; 3) echo c;; 4) echo d;; 5) echo e;; 6) echo f;; 7) echo g;; 8) echo h;; 9) echo i;; 10) echo j;; esac'
395
+
396
+compare_output "case with 18 items" \
397
+  'x=18; case $x in 1) echo a;; 2) echo b;; 3) echo c;; 4) echo d;; 5) echo e;; 6) echo f;; 7) echo g;; 8) echo h;; 9) echo i;; 10) echo j;; 11) echo k;; 12) echo l;; 13) echo m;; 14) echo n;; 15) echo o;; 16) echo p;; 17) echo q;; 18) echo r;; esac'
398
+
399
+# Many function definitions (function_ast_cache(20) limit)
400
+compare_output "25 function definitions" \
401
+  'for i in $(seq 1 25); do eval "f_$i() { echo $i; }"; done; f_1; f_13; f_25'
402
+
403
+compare_output "30 function definitions" \
404
+  'for i in $(seq 1 30); do eval "f_$i() { echo $i; }"; done; f_1; f_15; f_30'
405
+
406
+# Many prefix assignments (saved_var_names(10) limit)
407
+compare_output "5 prefix assignments" \
408
+  'A=1 B=2 C=3 D=4 E=5 env | grep -c "^[ABCDE]="'
409
+
410
+compare_output "12 prefix assignments" \
411
+  'A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 I=9 J=10 K=11 L=12 env | grep -c "^[A-L]="'
412
+
413
+# Many shell variables (MAX_SHELL_VARS=512)
414
+compare_output "200 variables" \
415
+  'for i in $(seq 1 200); do eval "var_$i=$i"; done; echo $var_1 $var_100 $var_200'
416
+
417
+compare_output "400 variables" \
418
+  'i=1; while [ $i -le 400 ]; do eval "var_$i=$i"; i=$((i+1)); done; echo $var_1 $var_200 $var_400'
419
+
420
+# Control flow nesting depth (MAX_CONTROL_DEPTH=20)
421
+compare_output "nested if 15 levels" \
422
+  'x=1; if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then echo deep15; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi'
423
+
424
+# Long variable names (MAX_VAR_NAME_LEN=256)
425
+compare_output "100-char variable name" \
426
+  'eval "$(printf "a%.0s" $(seq 1 100))=hello"; eval "echo \$$(printf "a%.0s" $(seq 1 100))"'
427
+
428
+compare_output "200-char variable name" \
429
+  'eval "$(printf "a%.0s" $(seq 1 200))=world"; eval "echo \$$(printf "a%.0s" $(seq 1 200))"'
430
+
431
+# ==========================================
432
+# 19. Recursion & stack pressure
433
+# ==========================================
434
+section "19 - Recursion and stack pressure"
435
+
436
+compare_output "recursive function 75 levels" \
437
+  'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 75'
438
+
439
+compare_output "recursive function 100 levels" \
440
+  'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 100'
441
+
442
+compare_output "mutual recursion 20 levels" \
443
+  'a() { if [ $1 -le 0 ]; then echo done; return; fi; b $(($1-1)); }; b() { a "$1"; }; a 20'
444
+
445
+compare_output "nested subshells 10 levels" \
446
+  '(echo 1; (echo 2; (echo 3; (echo 4; (echo 5; (echo 6; (echo 7; (echo 8; (echo 9; (echo 10))))))))))'
447
+
448
+compare_output "nested command substitution 10 levels" \
449
+  'echo $(echo $(echo $(echo $(echo $(echo $(echo $(echo $(echo $(echo hello)))))))))'
450
+
451
+compare_output "nested arithmetic 20 levels" \
452
+  'echo $(( ((((((((((((((((((((1+1)))))))))))))))))))) ))'
453
+
454
+# ==========================================
455
+# 20. Memory & buffer boundary tests
456
+# ==========================================
457
+section "20 - Buffer boundaries"
458
+
459
+# Command substitution at exact power-of-2 boundaries
460
+compare_output "cmd sub exactly 4096 bytes" \
461
+  'x=$(head -c 4096 /dev/zero | tr "\0" "a"); echo ${#x}'
462
+
463
+compare_output "cmd sub exactly 4097 bytes" \
464
+  'x=$(head -c 4097 /dev/zero | tr "\0" "a"); echo ${#x}'
465
+
466
+compare_output "cmd sub exactly 8192 bytes" \
467
+  'x=$(head -c 8192 /dev/zero | tr "\0" "a"); echo ${#x}'
468
+
469
+compare_output "cmd sub exactly 8193 bytes" \
470
+  'x=$(head -c 8193 /dev/zero | tr "\0" "a"); echo ${#x}'
471
+
472
+compare_output "cmd sub exactly 16384 bytes" \
473
+  'x=$(head -c 16384 /dev/zero | tr "\0" "a"); echo ${#x}'
474
+
475
+compare_output "cmd sub exactly 65536 bytes" \
476
+  'x=$(head -c 65536 /dev/zero | tr "\0" "a"); echo ${#x}'
477
+
478
+# Pattern matching on large strings
479
+compare_output "pattern removal on 5000-char string" \
480
+  'x=$(head -c 5000 /dev/zero | tr "\0" "a"); y=${x#aaa}; echo ${#y}'
481
+
482
+compare_output "pattern replace on 5000-char string" \
483
+  'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${x//a/b} | wc -c'
484
+
485
+# Sparse array
486
+compare_output "sparse array index 10000" \
487
+  'arr[10000]=hello; echo ${arr[10000]}'
488
+
489
+compare_output "sparse array index 99999" \
490
+  'arr[99999]=world; echo ${arr[99999]}'
491
+
492
+# Arithmetic integer boundaries
493
+compare_output "arithmetic max int" \
494
+  'echo $((9223372036854775807))'
495
+
496
+compare_output "arithmetic large multiplication" \
497
+  'echo $((1000000 * 1000000))'
498
+
499
+compare_output "arithmetic negative" \
500
+  'echo $((-2147483648))'
501
+
502
+# Substring at boundary offsets
503
+compare_output "substring negative offset" \
504
+  'x=hello_world; echo ${x: -5}'
505
+
506
+compare_output "substring offset beyond length" \
507
+  'x=hi; echo "[${x:100:5}]"'
508
+
509
+compare_output "substring zero length" \
510
+  'x=hello; echo "[${x:2:0}]"'
511
+
512
+# ==========================================
513
+# 21. Tokenization limits
514
+# ==========================================
515
+section "21 - Tokenization limits"
516
+
517
+compare_output "echo with 200 arguments" \
518
+  'echo $(seq 1 200) | wc -w'
519
+
520
+compare_output "echo with 400 arguments" \
521
+  'echo $(seq 1 400) | wc -w'
522
+
523
+compare_output "many semicolons 30 commands" \
524
+  'a=0; a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); echo $a'
525
+
526
+# ==========================================
527
+# 22. FD & redirection stress
528
+# ==========================================
529
+section "22 - FD and redirection stress"
530
+
531
+compare_output "rapid redirect cycling 100 iterations" \
532
+  'i=1; while [ $i -le 100 ]; do echo $i > /tmp/fortsh_test_fd_$$; i=$((i+1)); done; cat /tmp/fortsh_test_fd_$$; rm -f /tmp/fortsh_test_fd_$$'
533
+
534
+compare_output "append redirect 200 lines" \
535
+  'rm -f /tmp/fortsh_test_append_$$; i=1; while [ $i -le 200 ]; do echo $i >> /tmp/fortsh_test_append_$$; i=$((i+1)); done; wc -l < /tmp/fortsh_test_append_$$; rm -f /tmp/fortsh_test_append_$$'
536
+
537
+compare_output "stderr redirect in loop" \
538
+  'for i in 1 2 3 4 5; do echo "err$i" >&2; done 2>&1 | wc -l'
539
+
540
+compare_output "output and error merge" \
541
+  '{ echo stdout; echo stderr >&2; } 2>&1 | sort'
542
+
543
+compare_output "dev null redirect in loop" \
544
+  'i=1; while [ $i -le 100 ]; do echo $i > /dev/null; i=$((i+1)); done; echo done'
545
+
546
+# ==========================================
547
+# 23. Pipeline & fork stress
548
+# ==========================================
549
+section "23 - Pipeline and fork stress"
550
+
551
+compare_output "15-stage pipeline" \
552
+  'echo hello | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'
553
+
554
+compare_output "20-stage pipeline" \
555
+  'echo hello | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'
556
+
557
+compare_output "pipeline with 100000 lines" \
558
+  'seq 1 100000 | wc -l'
559
+
560
+compare_output "pipeline with sort 10000 lines" \
561
+  'seq 1 10000 | sort -rn 2>/dev/null | head -1'
562
+
563
+compare_output "background job wait cycling" \
564
+  'i=1; while [ $i -le 50 ]; do true & i=$((i+1)); done; wait; echo done'
565
+
566
+compare_output "100 background jobs then wait" \
567
+  'i=1; while [ $i -le 100 ]; do true & i=$((i+1)); done; wait; echo done'
568
+
569
+compare_output "subshell pipeline isolation" \
570
+  'x=outer; echo $x | (read y; echo $y) | cat'
571
+
572
+# ==========================================
573
+# 24. Array scaling
574
+# ==========================================
575
+section "24 - Array scaling"
576
+
577
+# BUG: arr[$i]=$i doesn't expand $i in subscript — use eval workaround
578
+compare_output "indexed array 100 elements" \
579
+  'i=0; while [ $i -lt 100 ]; do eval "arr[$i]=$i"; i=$((i+1)); done; echo ${#arr[@]} ${arr[0]} ${arr[50]} ${arr[99]}'
580
+
581
+compare_output "indexed array 500 elements" \
582
+  'i=0; while [ $i -lt 500 ]; do eval "arr[$i]=$i"; i=$((i+1)); done; echo ${#arr[@]} ${arr[0]} ${arr[250]} ${arr[499]}'
583
+
584
+# BUG: arr=($(cmd)) doesn't word-split command substitution into elements
585
+compare_output "indexed array via init list 20" \
586
+  'arr=(a b c d e f g h i j k l m n o p q r s t); echo ${#arr[@]} ${arr[0]} ${arr[19]}'
587
+
588
+compare_output "array append 500 times" \
589
+  'arr=(); i=0; while [ $i -lt 500 ]; do arr+=("$i"); i=$((i+1)); done; echo ${#arr[@]}'
590
+
591
+# BUG: m["key$i"] doesn't expand $i in subscript — use eval workaround
592
+compare_output "assoc array 100 keys" \
593
+  'declare -A m; i=1; while [ $i -le 100 ]; do eval "m[key$i]=$i"; i=$((i+1)); done; echo ${#m[@]} ${m[key1]} ${m[key50]} ${m[key100]}'
594
+
595
+compare_output "assoc array 200 keys" \
596
+  'declare -A m; i=1; while [ $i -le 200 ]; do eval "m[key$i]=$i"; i=$((i+1)); done; echo ${#m[@]}'
597
+
598
+compare_output "assoc array overwrite cycle" \
599
+  'declare -A m; i=1; while [ $i -le 100 ]; do m[k]=$i; i=$((i+1)); done; echo ${m[k]}'
600
+
601
+compare_output "array element 10000-char value" \
602
+  'x=$(head -c 10000 /dev/zero | tr "\0" "a"); arr[0]=$x; echo ${#arr[0]}'
603
+
604
+# ==========================================
605
+# 25. Control flow edge cases
606
+# ==========================================
607
+section "25 - Control flow edge cases"
608
+
609
+compare_output "mixed loop nesting for-while-until" \
610
+  'for i in 1 2; do j=0; while [ $j -lt 2 ]; do k=5; until [ $k -le 3 ]; do k=$((k-1)); done; echo "$i $j $k"; j=$((j+1)); done; done'
611
+
612
+compare_output "break 2 from inner loop" \
613
+  'for i in 1 2 3; do for j in a b c; do if [ "$j" = "b" ]; then break 2; fi; echo "$i$j"; done; done; echo end'
614
+
615
+compare_output "break 3 from triple nested" \
616
+  'for i in 1 2; do for j in a b; do for k in x y; do if [ "$k" = "y" ]; then break 3; fi; echo "$i$j$k"; done; done; done; echo end'
617
+
618
+compare_output "continue 2 from inner loop" \
619
+  'for i in 1 2 3; do for j in a b c; do if [ "$j" = "b" ]; then continue 2; fi; echo "$i$j"; done; done'
620
+
621
+compare_output "loop 5000 iterations" \
622
+  'x=0; i=0; while [ $i -lt 5000 ]; do x=$((x+1)); i=$((i+1)); done; echo $x'
623
+
624
+# 10000 iterations takes ~72s on ARM64, ~1s on x86_64
625
+case "$(uname -m)" in
626
+  arm64|aarch64) TEST_TIMEOUT=180 ;;
627
+  *)             TEST_TIMEOUT=60 ;;
628
+esac
629
+compare_output "loop 10000 iterations" \
630
+  'x=0; i=0; while [ $i -lt 10000 ]; do x=$((x+1)); i=$((i+1)); done; echo $x'
631
+case "$(uname -m)" in
632
+  arm64|aarch64) TEST_TIMEOUT=90 ;;
633
+  *)             TEST_TIMEOUT=30 ;;
634
+esac
635
+
636
+compare_output "while with complex condition" \
637
+  'i=0; while [ $i -lt 10 ] && [ $((i % 2)) -eq 0 -o $i -lt 5 ]; do echo $i; i=$((i+1)); done'
638
+
639
+compare_output "nested case in loop" \
640
+  'for i in $(seq 1 20); do case $((i % 4)) in 0) echo "a$i";; 1) echo "b$i";; 2) echo "c$i";; 3) echo "d$i";; esac; done | wc -l'
641
+
642
+# ==========================================
643
+# 26. Trap & signal stress
644
+# ==========================================
645
+section "26 - Trap and signal stress"
646
+
647
+compare_output "set traps on 10 signals" \
648
+  'trap "echo 1" HUP; trap "echo 2" INT; trap "echo 3" QUIT; trap "echo 4" TERM; trap "echo 5" USR1; trap "echo 6" USR2; trap "echo 7" PIPE; trap "echo 8" ALRM; trap "echo 9" CONT; trap "echo exit" EXIT; trap -p | wc -l'
649
+
650
+compare_output "trap reset cycling 50 times" \
651
+  'i=1; while [ $i -le 50 ]; do trap "echo $i" EXIT; i=$((i+1)); done; trap -p EXIT'
652
+
653
+compare_output "trap with long handler" \
654
+  'handler=""; i=1; while [ $i -le 100 ]; do handler="${handler}echo $i;"; i=$((i+1)); done; trap "$handler" EXIT; echo before'
655
+
656
+compare_output "modify trap inside trap" \
657
+  'trap '\''trap "echo inner" EXIT; echo outer'\'' EXIT; exit 0'
658
+
659
+compare_output "trap unset then reset" \
660
+  'trap "echo first" EXIT; trap - EXIT; trap "echo second" EXIT; exit 0'
661
+
662
+# ==========================================
663
+# 27. String & expansion stress
664
+# ==========================================
665
+section "27 - String and expansion stress"
666
+
667
+compare_output "uppercase 5000-char variable" \
668
+  'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${x^^} | wc -c'
669
+
670
+compare_output "lowercase 5000-char variable" \
671
+  'x=$(head -c 5000 /dev/zero | tr "\0" "A"); echo ${x,,} | wc -c'
672
+
673
+compare_output "global replace 5000-char variable" \
674
+  'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${x//a/b} | wc -c'
675
+
676
+compare_output "concatenation loop 5000 chars" \
677
+  'x=""; for i in $(seq 1 1000); do x="${x}abcde"; done; echo ${#x}'
678
+
679
+compare_output "prefix removal on 10000-char string" \
680
+  'x=$(head -c 10000 /dev/zero | tr "\0" "a"); y=${x#aaaa}; echo ${#y}'
681
+
682
+compare_output "suffix removal on 10000-char string" \
683
+  'x=$(head -c 10000 /dev/zero | tr "\0" "a"); y=${x%aaaa}; echo ${#y}'
684
+
685
+compare_output "default expansion chain 8 deep" \
686
+  'echo ${a:-${b:-${c:-${d:-${e:-${f:-${g:-${h:-deep8}}}}}}}}'
687
+
688
+compare_output "assign-default expansion" \
689
+  'unset x; echo ${x:=hello}; echo $x'
690
+
691
+compare_output "error-if-unset expansion" \
692
+  'x=set; echo ${x:?should not error}'
693
+
694
+# ==========================================
695
+# 28. Heredoc scaling
696
+# ==========================================
697
+section "28 - Heredoc scaling"
698
+
699
+compare_output "heredoc 50 lines" \
700
+  'cat <<EOF | wc -l
701
+$(seq 1 50)
702
+EOF'
703
+
704
+compare_output "heredoc with 20 variable expansions" \
705
+  'a=1; b=2; c=3; d=4; e=5; f=6; g=7; h=8; i=9; j=10; cat <<EOF
706
+$a $b $c $d $e $f $g $h $i $j $a $b $c $d $e $f $g $h $i $j
707
+EOF'
708
+
709
+compare_output "heredoc with command substitution" \
710
+  'cat <<EOF
711
+$(echo hello) $(echo world) $(echo from) $(echo heredoc)
712
+EOF'
713
+
714
+compare_output "heredoc quoted delimiter no expansion" \
715
+  'x=should_not_expand; cat <<'\''EOF'\''
716
+$x $(echo nope)
717
+EOF'
718
+
719
+compare_output "multiple heredocs sequential" \
720
+  'cat <<A; cat <<B; cat <<C
721
+line_a
722
+A
723
+line_b
724
+B
725
+line_c
726
+C'
727
+
728
+# ==========================================
729
+# 29. Glob expansion stress
730
+# ==========================================
731
+section "29 - Glob expansion"
732
+
733
+compare_output "glob with many matches" \
734
+  'mkdir -p /tmp/fortsh_glob_$$; i=1; while [ $i -le 200 ]; do touch /tmp/fortsh_glob_$$/f$i.txt; i=$((i+1)); done; ls /tmp/fortsh_glob_$$/*.txt | wc -l; rm -rf /tmp/fortsh_glob_$$'
735
+
736
+# Use check_output for glob no-match since $$ differs between bash and fortsh
737
+check_output "glob with no matches nullglob off" \
738
+  'echo /tmp/no_such_dir_xyz/*.abc' \
739
+  '/tmp/no_such_dir_xyz/*.abc'
740
+
741
+compare_output "glob bracket expression" \
742
+  'mkdir -p /tmp/fortsh_glob2_$$; touch /tmp/fortsh_glob2_$$/a1 /tmp/fortsh_glob2_$$/b2 /tmp/fortsh_glob2_$$/c3; ls /tmp/fortsh_glob2_$$/[abc][0-9] | wc -l; rm -rf /tmp/fortsh_glob2_$$'
743
+
744
+compare_output "glob question mark" \
745
+  'mkdir -p /tmp/fortsh_glob3_$$; touch /tmp/fortsh_glob3_$$/ax /tmp/fortsh_glob3_$$/bx /tmp/fortsh_glob3_$$/cx; echo /tmp/fortsh_glob3_$$/?x | wc -w; rm -rf /tmp/fortsh_glob3_$$'
746
+
747
+# ==========================================
748
+# 30. Eval & dynamic execution stress
749
+# ==========================================
750
+section "30 - Eval and dynamic execution"
751
+
752
+compare_output "eval with nested quoting" \
753
+  'x="echo hello"; eval "$x"'
754
+
755
+compare_output "eval defining 50 variables" \
756
+  'for i in $(seq 1 50); do eval "v$i=$((i*2))"; done; echo $v1 $v25 $v50'
757
+
758
+compare_output "eval redefining function 20 times" \
759
+  'for i in $(seq 1 20); do eval "f() { echo $i; }"; done; f'
760
+
761
+compare_output "eval with command substitution" \
762
+  'x="echo \$(echo nested)"; eval "$x"'
763
+
764
+compare_output "eval chain 5 deep" \
765
+  'eval "eval \"eval \\\"eval \\\\\\\"echo deep\\\\\\\"\\\"\""'
766
+
767
+# ==========================================
768
+# 31. Rapid set/unset & variable churn
769
+# ==========================================
770
+section "31 - Variable churn"
771
+
772
+compare_output "set/unset 500 cycle" \
773
+  'i=0; while [ $i -lt 500 ]; do x=$i; unset x; i=$((i+1)); done; echo ${x:-done}'
774
+
775
+compare_output "rapid export/unexport" \
776
+  'i=0; while [ $i -lt 100 ]; do export v=$i; unset v; i=$((i+1)); done; echo ${v:-done}'
777
+
778
+compare_output "overwrite same variable 1000 times" \
779
+  'i=0; while [ $i -lt 1000 ]; do x=$i; i=$((i+1)); done; echo $x'
780
+
781
+compare_output "local variable scoping 10 deep" \
782
+  'f() { local x=$1; if [ $1 -le 0 ]; then echo $x; return; fi; f $(($1-1)); echo $x; }; f 10 | head -1'
783
+
784
+compare_output "local var does not leak" \
785
+  'f() { local inner=secret; }; f; echo "${inner:-not_leaked}"'
786
+
787
+# ==========================================
788
+# 32. Quoting & escaping edge cases
789
+# ==========================================
790
+section "32 - Quoting edge cases"
791
+
792
+compare_output "single quotes preserve specials" \
793
+  'echo '\''$HOME $(echo no) `echo no`'\'''
794
+
795
+compare_output "double quotes allow expansion" \
796
+  'x=works; echo "it $x"'
797
+
798
+compare_output "mixed quote concatenation" \
799
+  "echo 'hello'\"world\"'more'"
800
+
801
+compare_output "backslash in double quotes" \
802
+  'echo "a\\b" "c\"d" "e\$f"'
803
+
804
+# dollar-single-quote ($'...') ANSI-C quoting
805
+compare_output "dollar-single-quote escapes" \
806
+  "echo \$'hello\\tworld'"
807
+
808
+compare_output "empty string arguments preserved" \
809
+  'f() { echo $#; }; f "" "" ""'
810
+
811
+compare_output "IFS splitting edge case" \
812
+  'IFS=:; x="a:b::d"; for w in $x; do echo "[$w]"; done'
813
+
814
+# ==========================================
815
+# 33. Process substitution & subshell stress
816
+# ==========================================
817
+section "33 - Subshell isolation"
818
+
819
+compare_output "subshell variable isolation" \
820
+  'x=outer; (x=inner; echo $x); echo $x'
821
+
822
+compare_output "subshell exit code" \
823
+  '(exit 42); echo $?'
824
+
825
+compare_output "nested subshell variable" \
826
+  'x=1; (x=2; (x=3; echo $x); echo $x); echo $x'
827
+
828
+compare_output "subshell with loop" \
829
+  '(for i in 1 2 3; do echo $i; done) | wc -l'
830
+
831
+compare_output "subshell does not affect parent vars" \
832
+  'x=before; (x=after; export x); echo $x'
833
+
834
+# ==========================================
835
+# 34. Compound command stress
836
+# ==========================================
837
+section "34 - Compound commands"
838
+
839
+compare_output "brace group with many commands" \
840
+  '{ echo a; echo b; echo c; echo d; echo e; echo f; echo g; echo h; echo i; echo j; } | wc -l'
841
+
842
+compare_output "brace group pipeline" \
843
+  '{ echo hello; echo world; } | sort'
844
+
845
+compare_output "brace group with redirect" \
846
+  '{ echo line1; echo line2; echo line3; } > /tmp/fortsh_brace_$$; wc -l < /tmp/fortsh_brace_$$; rm -f /tmp/fortsh_brace_$$'
847
+
848
+compare_output "nested brace groups" \
849
+  '{ { { echo deep; }; }; }'
850
+
851
+compare_output "command list with mixed operators" \
852
+  'true && echo a; false || echo b; true && false || echo c'
853
+
854
+compare_output "long AND chain" \
855
+  'true && true && true && true && true && true && true && true && true && true && echo all_true'
856
+
857
+compare_output "long OR chain" \
858
+  'false || false || false || false || false || false || false || false || false || false || echo found'
859
+
860
+compare_output "mixed AND/OR chain" \
861
+  'true && false || true && echo result'
862
+
863
+print_summary
suites/builtins/portable/test_test.shadded
@@ -0,0 +1,68 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[test]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. test string operators"
6
+compare_exit "test -z empty string" 'test -z ""'
7
+compare_exit "test -z nonempty fails" 'test -z "hello"'
8
+compare_exit "test -n nonempty string" 'test -n "hello"'
9
+compare_exit "test -n empty fails" 'test -n ""'
10
+compare_exit "test string equality" 'test "abc" = "abc"'
11
+compare_exit "test string inequality" 'test "abc" != "def"'
12
+compare_exit "test equal strings =" 'test "same" = "same"'
13
+compare_exit "test unequal strings !=" 'test "a" != "b"'
14
+
15
+section "2. test numeric operators"
16
+compare_exit "test -eq equal" 'test 5 -eq 5'
17
+compare_exit "test -ne not equal" 'test 5 -ne 3'
18
+compare_exit "test -lt less than" 'test 3 -lt 5'
19
+compare_exit "test -gt greater than" 'test 5 -gt 3'
20
+compare_exit "test -le less or equal" 'test 5 -le 5'
21
+compare_exit "test -le strictly less" 'test 4 -le 5'
22
+compare_exit "test -ge greater or equal" 'test 5 -ge 5'
23
+compare_exit "test -ge strictly greater" 'test 6 -ge 5'
24
+compare_exit "test -eq fail" 'test 5 -eq 3'
25
+compare_exit "test -lt fail" 'test 5 -lt 3'
26
+
27
+section "3. test file operators"
28
+compare_exit "test -f regular file" "touch $TEST_TMPDIR/testfile; test -f $TEST_TMPDIR/testfile"
29
+compare_exit "test -f nonexistent" "test -f $TEST_TMPDIR/no_such_file"
30
+compare_exit "test -d directory" "test -d $TEST_TMPDIR"
31
+compare_exit "test -d on file fails" "touch $TEST_TMPDIR/testfile; test -d $TEST_TMPDIR/testfile"
32
+compare_exit "test -e file exists" "touch $TEST_TMPDIR/testfile; test -e $TEST_TMPDIR/testfile"
33
+compare_exit "test -e nonexistent" "test -e $TEST_TMPDIR/no_such_file"
34
+compare_exit "test -s nonempty file" "echo data > $TEST_TMPDIR/nonempty; test -s $TEST_TMPDIR/nonempty"
35
+compare_exit "test -s empty file" "touch $TEST_TMPDIR/emptyfile; test -s $TEST_TMPDIR/emptyfile"
36
+compare_exit "test -r readable file" "touch $TEST_TMPDIR/testfile; test -r $TEST_TMPDIR/testfile"
37
+compare_exit "test -w writable file" "touch $TEST_TMPDIR/testfile; test -w $TEST_TMPDIR/testfile"
38
+compare_exit "test -x not executable" "touch $TEST_TMPDIR/testfile; test -x $TEST_TMPDIR/testfile"
39
+compare_exit "test -x executable" "touch $TEST_TMPDIR/exefile; chmod +x $TEST_TMPDIR/exefile; test -x $TEST_TMPDIR/exefile"
40
+
41
+section "4. test logical operators"
42
+compare_exit "test logical NOT true" 'test ! -z "hello"'
43
+compare_exit "test logical NOT false" 'test ! -z ""'
44
+compare_exit "test logical AND -a both true" 'test 1 -eq 1 -a 2 -eq 2'
45
+compare_exit "test logical AND -a one false" 'test 1 -eq 1 -a 2 -eq 3'
46
+compare_exit "test logical OR -o both false" 'test 1 -eq 2 -o 3 -eq 4'
47
+compare_exit "test logical OR -o one true" 'test 1 -eq 2 -o 2 -eq 2'
48
+
49
+section "5. [ bracket form"
50
+compare_exit "[ string equality ]" '[ "abc" = "abc" ]'
51
+compare_exit "[ numeric test ]" '[ 5 -gt 3 ]'
52
+compare_exit "[ -z empty ]" '[ -z "" ]'
53
+compare_exit "[ file exists ]" "touch $TEST_TMPDIR/testfile; [ -f $TEST_TMPDIR/testfile ]"
54
+
55
+section "6. [[ extended test"
56
+compare_exit "[[ pattern match glob ]]" '[[ "hello" == h* ]]'
57
+compare_exit "[[ pattern no match ]]" '[[ "hello" == x* ]]'
58
+compare_exit "[[ regex match =~ ]]" '[[ "hello123" =~ [0-9]+ ]]'
59
+compare_exit "[[ regex no match ]]" '[[ "hello" =~ ^[0-9]+$ ]]'
60
+compare_exit "[[ logical AND && ]]" '[[ 1 -eq 1 && 2 -eq 2 ]]'
61
+compare_exit "[[ logical OR || ]]" '[[ 1 -eq 2 || 2 -eq 2 ]]'
62
+compare_exit "[[ negation ! ]]" '[[ ! "hello" == "world" ]]'
63
+compare_exit "[[ string comparison < ]]" '[[ "abc" < "def" ]]'
64
+compare_exit "[[ string comparison > ]]" '[[ "def" > "abc" ]]'
65
+compare_exit "[[ -z in extended ]]" '[[ -z "" ]]'
66
+compare_exit "[[ -n in extended ]]" '[[ -n "hello" ]]'
67
+
68
+print_summary
suites/builtins/portable/test_times.shadded
@@ -0,0 +1,9 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[times]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. times output"
6
+check_exit "times exits successfully" 'times' "0"
7
+check_exit "times produces output" 'times >/dev/null' "0"
8
+
9
+print_summary
suites/builtins/portable/test_trap.shadded
@@ -0,0 +1,27 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[trap]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. trap EXIT"
6
+compare_output "trap EXIT runs on exit" 'trap "echo bye" EXIT; echo hello'
7
+compare_output "trap EXIT runs after commands" 'trap "echo last" EXIT; echo first; echo second'
8
+compare_output "trap EXIT with explicit exit" 'trap "echo cleanup" EXIT; echo running; exit 0'
9
+compare_output "trap EXIT order" 'trap "echo bye" EXIT; echo a; echo b'
10
+
11
+section "2. trap management"
12
+compare_output "trap reset with dash" 'trap "echo caught" EXIT; trap - EXIT; echo done'
13
+compare_output "trap empty string ignores" 'trap "" INT; echo ok'
14
+check_exit "trap -l lists signals" 'trap -l' "0"
15
+check_exit "trap -p prints current traps" 'trap -p' "0"
16
+compare_output "trap -p shows set trap" 'trap "echo bye" EXIT; trap -p EXIT'
17
+
18
+section "3. trap with signals"
19
+compare_output "trap ERR on command failure" 'trap "echo error" ERR; false; true'
20
+compare_output "trap multiple signals" 'trap "echo sig" EXIT; echo running'
21
+compare_output "trap replaces previous" 'trap "echo first" EXIT; trap "echo second" EXIT; true'
22
+
23
+section "4. trap in subshell"
24
+compare_output "trap not inherited in subshell" 'trap "echo parent" EXIT; (echo child); echo back'
25
+compare_output "trap in subshell independent" '(trap "echo sub_exit" EXIT; echo in_sub)'
26
+
27
+print_summary
suites/builtins/portable/test_type_command.shadded
@@ -0,0 +1,26 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[type-command]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. type builtin"
6
+compare_exit "type recognizes builtin" 'type echo >/dev/null 2>&1'
7
+compare_exit "type recognizes external command" 'type ls >/dev/null 2>&1'
8
+compare_exit "type nonexistent command fails" 'type nonexistent_cmd_xyz_999 2>/dev/null'
9
+compare_output "type -t builtin" 'type -t echo'
10
+compare_output "type -t external" 'type -t ls'
11
+compare_output "type -t function" 'f() { :; }; type -t f'
12
+compare_output "type -t alias" 'alias myalias="echo hi" 2>/dev/null; type -t myalias'
13
+compare_output "type -t keyword" 'type -t if'
14
+
15
+section "2. command builtin"
16
+compare_exit "command -v finds builtin" 'command -v echo >/dev/null'
17
+compare_exit "command -v finds external" 'command -v ls >/dev/null'
18
+compare_exit "command -v nonexistent fails" 'command -v nonexistent_cmd_xyz_999'
19
+compare_output "command bypasses function" 'echo() { printf "FUNC\n"; }; command echo hello'
20
+compare_exit "command -V describes command" 'command -V echo >/dev/null'
21
+
22
+section "3. which"
23
+compare_exit "which finds external command" 'which ls >/dev/null 2>&1'
24
+compare_exit "which nonexistent fails" 'which nonexistent_cmd_xyz_999 2>/dev/null'
25
+
26
+print_summary
suites/builtins/portable/test_ulimit.shadded
@@ -0,0 +1,41 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[ulimit]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. ulimit display defaults"
6
+compare_output "ulimit default shows file size" 'ulimit'
7
+compare_output "ulimit -f same as default" 'ulimit -f'
8
+check_exit "ulimit -a shows all limits" 'ulimit -a' "0"
9
+check_output "ulimit -a produces output" 'ulimit -a | head -1 | grep -q . && echo yes' "yes"
10
+
11
+section "2. ulimit resource types"
12
+compare_output "ulimit -n open files" 'ulimit -n'
13
+compare_output "ulimit -s stack size" 'ulimit -s'
14
+compare_output "ulimit -u max processes" 'ulimit -u'
15
+compare_output "ulimit -t cpu time" 'ulimit -t'
16
+compare_output "ulimit -c core size" 'ulimit -c'
17
+compare_output "ulimit -d data size" 'ulimit -d'
18
+compare_output "ulimit -v virtual memory" 'ulimit -v'
19
+compare_output "ulimit -l locked memory" 'ulimit -l'
20
+compare_output "ulimit -m RSS" 'ulimit -m'
21
+
22
+section "3. ulimit soft vs hard"
23
+compare_output "ulimit -Sn soft open files" 'ulimit -Sn'
24
+compare_output "ulimit -Hn hard open files" 'ulimit -Hn'
25
+compare_output "ulimit -Ss soft stack" 'ulimit -Ss'
26
+compare_output "ulimit -Hs hard stack" 'ulimit -Hs'
27
+compare_output "ulimit -Su soft processes" 'ulimit -Su'
28
+compare_output "ulimit -Hu hard processes" 'ulimit -Hu'
29
+
30
+section "4. ulimit set and query"
31
+compare_output "ulimit -n set and get" 'ulimit -n 512; ulimit -n'
32
+compare_output "ulimit -s set and get" 'ulimit -s 8192; ulimit -s'
33
+compare_output "ulimit -c set to 0" 'ulimit -c 0; ulimit -c'
34
+compare_output "ulimit -c set to unlimited" 'ulimit -c unlimited; ulimit -c'
35
+
36
+section "5. ulimit error handling"
37
+compare_exit "ulimit invalid flag" 'ulimit -Z 2>/dev/null'
38
+compare_exit "ulimit set above hard limit" 'ulimit -n 999999999 2>/dev/null'
39
+compare_output "ulimit set then verify" 'OLD=$(ulimit -n); ulimit -n 256; echo $(ulimit -n); ulimit -n $OLD 2>/dev/null'
40
+
41
+print_summary
suites/builtins/portable/test_umask.shadded
@@ -0,0 +1,15 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[umask]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. umask display"
6
+compare_output "umask shows current mask" 'umask'
7
+compare_output "umask -S symbolic form" 'umask -S'
8
+
9
+section "2. umask set"
10
+compare_output "umask set 0022 and verify" 'umask 0022; umask'
11
+compare_output "umask set 0077 and symbolic" 'umask 0077; umask -S'
12
+compare_output "umask set then restore" 'OLD=$(umask); umask 0077; umask $OLD; umask'
13
+compare_exit "umask invalid value" 'umask 9999 2>/dev/null'
14
+
15
+print_summary
suites/builtins/portable/test_variable_ops.shadded
@@ -0,0 +1,59 @@
1
+#!/bin/sh
2
+TEST_PREFIX="[var-ops]"
3
+. "$(cd "$(dirname "$0")" && pwd)/test_harness.sh"
4
+
5
+section "1. default value operators"
6
+compare_output '${var:-default} with unset var' 'unset V; echo ${V:-fallback}'
7
+compare_output '${var:-default} with set var' 'V=real; echo ${V:-fallback}'
8
+compare_output '${var:-default} with empty var' 'V=""; echo ${V:-fallback}'
9
+compare_output '${var-default} with unset (no colon)' 'unset V; echo ${V-fallback}'
10
+compare_output '${var-default} with empty (no colon)' 'V=""; echo "${V-fallback}"'
11
+compare_output '${var:=default} assigns default' 'unset V; echo ${V:=assigned}; echo $V'
12
+compare_output '${var:+alternate} with set var' 'V=yes; echo ${V:+alt}'
13
+compare_output '${var:+alternate} with unset var' 'unset V; echo ${V:+alt}'
14
+compare_output '${var:+alternate} with empty var' 'V=""; echo ${V:+alt}'
15
+compare_exit '${var:?error} with unset var fails' 'unset V; : ${V:?oops} 2>/dev/null'
16
+compare_output '${var:?error} with set var' 'V=ok; echo ${V:?oops}'
17
+
18
+section "2. string length"
19
+compare_output '${#var} string length' 'V=hello; echo ${#V}'
20
+compare_output '${#var} empty string' 'V=""; echo ${#V}'
21
+compare_output '${#var} with spaces' 'V="hello world"; echo ${#V}'
22
+compare_output '${#var} unset is zero' 'unset V; echo ${#V}'
23
+
24
+section "3. prefix removal"
25
+compare_output '${var#pattern} shortest prefix' 'V="hello.world.txt"; echo ${V#*.}'
26
+compare_output '${var##pattern} longest prefix' 'V="hello.world.txt"; echo ${V##*.}'
27
+compare_output '${var#prefix} literal prefix' 'V="/usr/local/bin"; echo ${V#/usr}'
28
+compare_output '${var##*/} basename equivalent' 'V="/path/to/file.txt"; echo ${V##*/}'
29
+
30
+section "4. suffix removal"
31
+compare_output '${var%pattern} shortest suffix' 'V="hello.world.txt"; echo ${V%.*}'
32
+compare_output '${var%%pattern} longest suffix' 'V="hello.world.txt"; echo ${V%%.*}'
33
+compare_output '${var%suffix} literal suffix' 'V="file.tar.gz"; echo ${V%.gz}'
34
+compare_output '${var%%/*} dirname-like' 'V="path/to/file"; echo ${V%%/*}'
35
+
36
+section "5. replacement"
37
+compare_output '${var/pattern/repl} first match' 'V="hello world hello"; echo ${V/hello/hi}'
38
+compare_output '${var//pattern/repl} all matches' 'V="aabaa"; echo ${V//a/X}'
39
+compare_output '${var/pattern/} deletion' 'V="hello world"; echo ${V/world/}'
40
+compare_output '${var/#pattern/repl} anchor start' 'V="hello world"; echo ${V/#hello/hi}'
41
+compare_output '${var/%pattern/repl} anchor end' 'V="hello world"; echo ${V/%world/earth}'
42
+compare_output '${var//pattern/} delete all' 'V="a1b2c3"; echo ${V//[0-9]/}'
43
+
44
+section "6. case conversion"
45
+compare_output '${var^} capitalize first' 'V="hello"; echo ${V^}'
46
+compare_output '${var^^} uppercase all' 'V="hello"; echo ${V^^}'
47
+compare_output '${var,} lowercase first' 'V="HELLO"; echo ${V,}'
48
+compare_output '${var,,} lowercase all' 'V="HELLO"; echo ${V,,}'
49
+compare_output '${var^^} mixed case' 'V="hElLo"; echo ${V^^}'
50
+compare_output '${var,,} mixed case' 'V="HeLLo"; echo ${V,,}'
51
+
52
+section "7. substring"
53
+compare_output '${var:offset:length}' 'V="hello world"; echo ${V:6:5}'
54
+compare_output '${var:offset} to end' 'V="hello world"; echo ${V:6}'
55
+compare_output '${var:0:5} from start' 'V="hello world"; echo ${V:0:5}'
56
+compare_output '${var: -3} negative offset' 'V="hello"; echo ${V: -3}'
57
+compare_output '${var:0:0} empty substring' 'V="hello"; echo "${V:0:0}"'
58
+
59
+print_summary