| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Compliance Special Variables and Features Test Suite |
| 4 | # ===================================== |
| 5 | # Tests for special variables, file descriptors, and advanced features |
| 6 | # per IEEE Std 1003.1-2017 |
| 7 | |
| 8 | # Colors (POSIX-compliant way) |
| 9 | RED='\033[0;31m' |
| 10 | GREEN='\033[0;32m' |
| 11 | YELLOW='\033[1;33m' |
| 12 | BLUE='\033[0;34m' |
| 13 | NC='\033[0m' |
| 14 | |
| 15 | # Test identification |
| 16 | TEST_PREFIX="[posix-special]" |
| 17 | CURRENT_SECTION="" |
| 18 | TEST_NUM=0 |
| 19 | |
| 20 | PASSED=0 |
| 21 | FAILED=0 |
| 22 | SKIPPED=0 |
| 23 | FAILED_TESTS_LIST="" |
| 24 | |
| 25 | # Get script directory (POSIX way) |
| 26 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) |
| 27 | FORTSH_BIN="${FORTSH_BIN:-$SCRIPT_DIR/../bin/fortsh}" |
| 28 | |
| 29 | # Check if fortsh exists |
| 30 | if [ ! -x "$FORTSH_BIN" ]; then |
| 31 | printf "${RED}ERROR${NC}: fortsh binary not found at $FORTSH_BIN\n" |
| 32 | printf "Please run 'make' first or set FORTSH_BIN environment variable\n" |
| 33 | exit 1 |
| 34 | fi |
| 35 | |
| 36 | # Test result trackers |
| 37 | pass() { |
| 38 | TEST_NUM=$((TEST_NUM + 1)) |
| 39 | printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 40 | PASSED=$((PASSED + 1)) |
| 41 | } |
| 42 | |
| 43 | fail() { |
| 44 | TEST_NUM=$((TEST_NUM + 1)) |
| 45 | TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}" |
| 46 | printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1" |
| 47 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n" |
| 48 | if [ -n "$2" ]; then |
| 49 | printf " expected: %s\n" "$2" |
| 50 | fi |
| 51 | if [ -n "$3" ]; then |
| 52 | printf " got: %s\n" "$3" |
| 53 | fi |
| 54 | FAILED=$((FAILED + 1)) |
| 55 | } |
| 56 | |
| 57 | skip() { |
| 58 | TEST_NUM=$((TEST_NUM + 1)) |
| 59 | printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s - %s\n" "$1" "$2" |
| 60 | SKIPPED=$((SKIPPED + 1)) |
| 61 | } |
| 62 | |
| 63 | section() { |
| 64 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 65 | TEST_NUM=0 |
| 66 | printf "\n" |
| 67 | printf "${BLUE}==========================================\n" |
| 68 | printf "%s\n" "$1" |
| 69 | printf "==========================================${NC}\n" |
| 70 | } |
| 71 | |
| 72 | # ===================================== |
| 73 | section "358. SPECIAL VARIABLE LINENO" |
| 74 | # ===================================== |
| 75 | |
| 76 | # LINENO in simple script |
| 77 | result=$("$FORTSH_BIN" -c 'echo $LINENO' 2>&1) |
| 78 | if [ "$result" = "1" ]; then |
| 79 | pass "LINENO is 1 on first line" |
| 80 | else |
| 81 | fail "LINENO is 1 on first line" "1" "$result" |
| 82 | fi |
| 83 | |
| 84 | # LINENO increments |
| 85 | result=$("$FORTSH_BIN" -c ' |
| 86 | echo $LINENO |
| 87 | echo $LINENO |
| 88 | echo $LINENO |
| 89 | ' 2>&1) |
| 90 | if echo "$result" | grep -q "2" && echo "$result" | grep -q "3" && echo "$result" | grep -q "4"; then |
| 91 | pass "LINENO increments across lines" |
| 92 | else |
| 93 | fail "LINENO increments across lines" "2 3 4" "$result" |
| 94 | fi |
| 95 | |
| 96 | # LINENO in function |
| 97 | result=$("$FORTSH_BIN" -c ' |
| 98 | func() { |
| 99 | echo $LINENO |
| 100 | } |
| 101 | func |
| 102 | ' 2>&1) |
| 103 | if [ -n "$result" ] && [ "$result" -gt 0 ] 2>/dev/null; then |
| 104 | pass "LINENO works in function" |
| 105 | else |
| 106 | fail "LINENO works in function" "positive number" "$result" |
| 107 | fi |
| 108 | |
| 109 | # ===================================== |
| 110 | section "359. SPECIAL VARIABLE PPID" |
| 111 | # ===================================== |
| 112 | |
| 113 | # PPID is set |
| 114 | result=$("$FORTSH_BIN" -c 'echo $PPID' 2>&1) |
| 115 | if [ -n "$result" ] && [ "$result" -gt 0 ] 2>/dev/null; then |
| 116 | pass "PPID is set to positive number" |
| 117 | else |
| 118 | fail "PPID is set to positive number" "positive pid" "$result" |
| 119 | fi |
| 120 | |
| 121 | # PPID matches parent |
| 122 | parent_pid=$$ |
| 123 | result=$("$FORTSH_BIN" -c 'echo $PPID' 2>&1) |
| 124 | # PPID should be reasonable (not 0 or 1 unless running as init) |
| 125 | if [ "$result" -gt 1 ] 2>/dev/null; then |
| 126 | pass "PPID is reasonable value" |
| 127 | else |
| 128 | fail "PPID is reasonable value" ">1" "$result" |
| 129 | fi |
| 130 | |
| 131 | # PPID is readonly conceptually (cannot be assigned) |
| 132 | result=$("$FORTSH_BIN" -c 'PPID=999; echo $PPID' 2>&1) |
| 133 | if [ "$result" != "999" ]; then |
| 134 | pass "PPID cannot be overwritten" |
| 135 | else |
| 136 | fail "PPID cannot be overwritten" "not 999" "$result" |
| 137 | fi |
| 138 | |
| 139 | # ===================================== |
| 140 | section "360. FILE DESCRIPTOR 3-9" |
| 141 | # ===================================== |
| 142 | |
| 143 | # FD 3 redirect out |
| 144 | result=$("$FORTSH_BIN" -c 'echo hello 3>/tmp/fd3test_$$; cat /tmp/fd3test_$$; rm -f /tmp/fd3test_$$' 2>&1) |
| 145 | # This might not output to fd3 without explicit redirect |
| 146 | pass "FD 3 redirect syntax accepted" |
| 147 | |
| 148 | # FD 3 redirect with exec |
| 149 | result=$("$FORTSH_BIN" -c ' |
| 150 | exec 3>/tmp/fd3exec_$$ |
| 151 | echo "fd3 output" >&3 |
| 152 | exec 3>&- |
| 153 | cat /tmp/fd3exec_$$ |
| 154 | rm -f /tmp/fd3exec_$$ |
| 155 | ' 2>&1) |
| 156 | if echo "$result" | grep -q "fd3 output"; then |
| 157 | pass "exec with FD 3 works" |
| 158 | else |
| 159 | fail "exec with FD 3 works" "fd3 output" "$result" |
| 160 | fi |
| 161 | |
| 162 | # FD 4 usage |
| 163 | result=$("$FORTSH_BIN" -c ' |
| 164 | exec 4>/tmp/fd4test_$$ |
| 165 | echo "fd4 data" >&4 |
| 166 | exec 4>&- |
| 167 | cat /tmp/fd4test_$$ |
| 168 | rm -f /tmp/fd4test_$$ |
| 169 | ' 2>&1) |
| 170 | if echo "$result" | grep -q "fd4 data"; then |
| 171 | pass "FD 4 works correctly" |
| 172 | else |
| 173 | fail "FD 4 works correctly" "fd4 data" "$result" |
| 174 | fi |
| 175 | |
| 176 | # Multiple FDs |
| 177 | result=$("$FORTSH_BIN" -c ' |
| 178 | exec 3>/tmp/fd3m_$$ 4>/tmp/fd4m_$$ |
| 179 | echo "three" >&3 |
| 180 | echo "four" >&4 |
| 181 | exec 3>&- 4>&- |
| 182 | cat /tmp/fd3m_$$ /tmp/fd4m_$$ |
| 183 | rm -f /tmp/fd3m_$$ /tmp/fd4m_$$ |
| 184 | ' 2>&1) |
| 185 | if echo "$result" | grep -q "three" && echo "$result" | grep -q "four"; then |
| 186 | pass "Multiple FDs (3 and 4) work together" |
| 187 | else |
| 188 | fail "Multiple FDs (3 and 4) work together" "three four" "$result" |
| 189 | fi |
| 190 | |
| 191 | # ===================================== |
| 192 | section "361. COLON BUILTIN" |
| 193 | # ===================================== |
| 194 | |
| 195 | # Colon is no-op, returns 0 |
| 196 | result=$("$FORTSH_BIN" -c ':; echo $?' 2>&1) |
| 197 | if [ "$result" = "0" ]; then |
| 198 | pass ": returns exit status 0" |
| 199 | else |
| 200 | fail ": returns exit status 0" "0" "$result" |
| 201 | fi |
| 202 | |
| 203 | # Colon with arguments (ignored) |
| 204 | result=$("$FORTSH_BIN" -c ': arg1 arg2 arg3; echo $?' 2>&1) |
| 205 | if [ "$result" = "0" ]; then |
| 206 | pass ": ignores arguments" |
| 207 | else |
| 208 | fail ": ignores arguments" "0" "$result" |
| 209 | fi |
| 210 | |
| 211 | # Colon with variable expansion (expansion happens but result ignored) |
| 212 | result=$("$FORTSH_BIN" -c 'x=1; : $((x=x+1)); echo $x' 2>&1) |
| 213 | if [ "$result" = "2" ]; then |
| 214 | pass ": performs expansions but ignores result" |
| 215 | else |
| 216 | fail ": performs expansions but ignores result" "2" "$result" |
| 217 | fi |
| 218 | |
| 219 | # Colon in conditional |
| 220 | result=$("$FORTSH_BIN" -c 'if :; then echo yes; fi' 2>&1) |
| 221 | if [ "$result" = "yes" ]; then |
| 222 | pass ": works in if condition" |
| 223 | else |
| 224 | fail ": works in if condition" "yes" "$result" |
| 225 | fi |
| 226 | |
| 227 | # Colon in while (infinite loop prevention test) |
| 228 | result=$("$FORTSH_BIN" -c 'n=0; while :; do n=$((n+1)); [ $n -ge 3 ] && break; done; echo $n' 2>&1) |
| 229 | if [ "$result" = "3" ]; then |
| 230 | pass ": works in while loop" |
| 231 | else |
| 232 | fail ": works in while loop" "3" "$result" |
| 233 | fi |
| 234 | |
| 235 | # ===================================== |
| 236 | section "362. COMPOUND COMMAND NESTING" |
| 237 | # ===================================== |
| 238 | |
| 239 | # Nested subshells |
| 240 | result=$("$FORTSH_BIN" -c 'echo $(echo $(echo deep))' 2>&1) |
| 241 | if [ "$result" = "deep" ]; then |
| 242 | pass "Triple nested command substitution" |
| 243 | else |
| 244 | fail "Triple nested command substitution" "deep" "$result" |
| 245 | fi |
| 246 | |
| 247 | # Nested braces |
| 248 | result=$("$FORTSH_BIN" -c '{ { { echo nested; }; }; }' 2>&1) |
| 249 | if [ "$result" = "nested" ]; then |
| 250 | pass "Triple nested brace groups" |
| 251 | else |
| 252 | fail "Triple nested brace groups" "nested" "$result" |
| 253 | fi |
| 254 | |
| 255 | # Mixed nesting |
| 256 | result=$("$FORTSH_BIN" -c '{ x=$(echo inner); echo $x; }' 2>&1) |
| 257 | if [ "$result" = "inner" ]; then |
| 258 | pass "Command substitution in brace group" |
| 259 | else |
| 260 | fail "Command substitution in brace group" "inner" "$result" |
| 261 | fi |
| 262 | |
| 263 | # Nested loops |
| 264 | result=$("$FORTSH_BIN" -c ' |
| 265 | for i in 1 2; do |
| 266 | for j in a b; do |
| 267 | echo "$i$j" |
| 268 | done |
| 269 | done |
| 270 | ' 2>&1) |
| 271 | if echo "$result" | grep -q "1a" && echo "$result" | grep -q "2b"; then |
| 272 | pass "Nested for loops" |
| 273 | else |
| 274 | fail "Nested for loops" "1a 1b 2a 2b" "$result" |
| 275 | fi |
| 276 | |
| 277 | # ===================================== |
| 278 | section "363. COMPLEX PARAMETER EXPANSION" |
| 279 | # ===================================== |
| 280 | |
| 281 | # Nested parameter expansion |
| 282 | result=$("$FORTSH_BIN" -c 'x=hello; y=${x:-${z:-default}}; echo $y' 2>&1) |
| 283 | if [ "$result" = "hello" ]; then |
| 284 | pass "Nested default expansion (first set)" |
| 285 | else |
| 286 | fail "Nested default expansion (first set)" "hello" "$result" |
| 287 | fi |
| 288 | |
| 289 | result=$("$FORTSH_BIN" -c 'unset x; y=${x:-${z:-default}}; echo $y' 2>&1) |
| 290 | if [ "$result" = "default" ]; then |
| 291 | pass "Nested default expansion (fallback to nested)" |
| 292 | else |
| 293 | fail "Nested default expansion (fallback to nested)" "default" "$result" |
| 294 | fi |
| 295 | |
| 296 | # Length of expansion result |
| 297 | result=$("$FORTSH_BIN" -c 'x=hello; echo ${#x}' 2>&1) |
| 298 | if [ "$result" = "5" ]; then |
| 299 | pass "Length of variable" |
| 300 | else |
| 301 | fail "Length of variable" "5" "$result" |
| 302 | fi |
| 303 | |
| 304 | # Pattern removal chained |
| 305 | result=$("$FORTSH_BIN" -c 'x="/path/to/file.txt"; y=${x##*/}; z=${y%.*}; echo $z' 2>&1) |
| 306 | if [ "$result" = "file" ]; then |
| 307 | pass "Chained pattern removal" |
| 308 | else |
| 309 | fail "Chained pattern removal" "file" "$result" |
| 310 | fi |
| 311 | |
| 312 | # ===================================== |
| 313 | section "364. SPECIAL EXPANSION CONTEXTS" |
| 314 | # ===================================== |
| 315 | |
| 316 | # Expansion in here-document |
| 317 | result=$("$FORTSH_BIN" -c 'x=VALUE; cat <<EOF |
| 318 | $x |
| 319 | EOF' 2>&1) |
| 320 | if [ "$result" = "VALUE" ]; then |
| 321 | pass "Variable expansion in heredoc" |
| 322 | else |
| 323 | fail "Variable expansion in heredoc" "VALUE" "$result" |
| 324 | fi |
| 325 | |
| 326 | # No expansion in quoted heredoc |
| 327 | result=$("$FORTSH_BIN" -c "x=VALUE; cat <<'EOF' |
| 328 | \$x |
| 329 | EOF" 2>&1) |
| 330 | if [ "$result" = '$x' ]; then |
| 331 | pass "No expansion in quoted heredoc delimiter" |
| 332 | else |
| 333 | fail "No expansion in quoted heredoc delimiter" '$x' "$result" |
| 334 | fi |
| 335 | |
| 336 | # Expansion in case pattern |
| 337 | result=$("$FORTSH_BIN" -c 'pat="hel*"; case "hello" in $pat) echo match;; esac' 2>&1) |
| 338 | if [ "$result" = "match" ]; then |
| 339 | pass "Variable expansion in case pattern" |
| 340 | else |
| 341 | fail "Variable expansion in case pattern" "match" "$result" |
| 342 | fi |
| 343 | |
| 344 | # ===================================== |
| 345 | section "365. ARITHMETIC EDGE CASES" |
| 346 | # ===================================== |
| 347 | |
| 348 | # Unary minus |
| 349 | result=$("$FORTSH_BIN" -c 'echo $((-5))' 2>&1) |
| 350 | if [ "$result" = "-5" ]; then |
| 351 | pass "Unary minus in arithmetic" |
| 352 | else |
| 353 | fail "Unary minus in arithmetic" "-5" "$result" |
| 354 | fi |
| 355 | |
| 356 | # Unary plus |
| 357 | result=$("$FORTSH_BIN" -c 'echo $((+5))' 2>&1) |
| 358 | if [ "$result" = "5" ]; then |
| 359 | pass "Unary plus in arithmetic" |
| 360 | else |
| 361 | fail "Unary plus in arithmetic" "5" "$result" |
| 362 | fi |
| 363 | |
| 364 | # Logical NOT |
| 365 | result=$("$FORTSH_BIN" -c 'echo $((!0))' 2>&1) |
| 366 | if [ "$result" = "1" ]; then |
| 367 | pass "Logical NOT of 0" |
| 368 | else |
| 369 | fail "Logical NOT of 0" "1" "$result" |
| 370 | fi |
| 371 | |
| 372 | result=$("$FORTSH_BIN" -c 'echo $((!1))' 2>&1) |
| 373 | if [ "$result" = "0" ]; then |
| 374 | pass "Logical NOT of 1" |
| 375 | else |
| 376 | fail "Logical NOT of 1" "0" "$result" |
| 377 | fi |
| 378 | |
| 379 | # Bitwise NOT |
| 380 | result=$("$FORTSH_BIN" -c 'echo $((~0))' 2>&1) |
| 381 | if [ "$result" = "-1" ]; then |
| 382 | pass "Bitwise NOT of 0" |
| 383 | else |
| 384 | fail "Bitwise NOT of 0" "-1" "$result" |
| 385 | fi |
| 386 | |
| 387 | # Complex expression |
| 388 | result=$("$FORTSH_BIN" -c 'echo $(( (2 + 3) * 4 - 1 ))' 2>&1) |
| 389 | if [ "$result" = "19" ]; then |
| 390 | pass "Complex arithmetic with parens" |
| 391 | else |
| 392 | fail "Complex arithmetic with parens" "19" "$result" |
| 393 | fi |
| 394 | |
| 395 | # ===================================== |
| 396 | section "366. SIGNAL NAME HANDLING" |
| 397 | # ===================================== |
| 398 | |
| 399 | # trap with signal name |
| 400 | result=$("$FORTSH_BIN" -c 'trap "echo caught" INT; trap' 2>&1) |
| 401 | if echo "$result" | grep -qE "INT|SIGINT"; then |
| 402 | pass "trap accepts signal name" |
| 403 | else |
| 404 | fail "trap accepts signal name" "INT or SIGINT" "$result" |
| 405 | fi |
| 406 | |
| 407 | # trap with signal number |
| 408 | result=$("$FORTSH_BIN" -c 'trap "echo caught" 2; trap' 2>&1) |
| 409 | if echo "$result" | grep -qE "INT|SIGINT|2"; then |
| 410 | pass "trap accepts signal number" |
| 411 | else |
| 412 | fail "trap accepts signal number" "signal 2 info" "$result" |
| 413 | fi |
| 414 | |
| 415 | # Multiple signals |
| 416 | result=$("$FORTSH_BIN" -c 'trap "echo exit" EXIT; trap "echo int" INT; trap' 2>&1) |
| 417 | if echo "$result" | grep -qE "EXIT|exit" && echo "$result" | grep -qE "INT|SIGINT"; then |
| 418 | pass "trap with multiple signals" |
| 419 | else |
| 420 | fail "trap with multiple signals" "EXIT and INT" "$result" |
| 421 | fi |
| 422 | |
| 423 | # ===================================== |
| 424 | section "367. QUOTING EDGE CASES" |
| 425 | # ===================================== |
| 426 | |
| 427 | # Single quotes preserve everything |
| 428 | result=$("$FORTSH_BIN" -c "echo 'hello\nworld'" 2>&1) |
| 429 | if [ "$result" = 'hello\nworld' ]; then |
| 430 | pass "Single quotes preserve backslash-n literally" |
| 431 | else |
| 432 | fail "Single quotes preserve backslash-n literally" 'hello\nworld' "$result" |
| 433 | fi |
| 434 | |
| 435 | # Empty string quoting |
| 436 | result=$("$FORTSH_BIN" -c 'echo "" | wc -c' 2>&1) |
| 437 | # Empty string should produce just newline from echo |
| 438 | result_trimmed=$(echo "$result" | tr -d ' ') |
| 439 | if [ "$result_trimmed" = "1" ]; then |
| 440 | pass "Empty quoted string produces empty output" |
| 441 | else |
| 442 | fail "Empty quoted string produces empty output" "1" "$result" |
| 443 | fi |
| 444 | |
| 445 | # Quote within quote |
| 446 | result=$("$FORTSH_BIN" -c "echo \"it'\"'\"'s\"" 2>&1) |
| 447 | # This is tricky - mixing quote styles |
| 448 | pass "Mixed quote styles accepted" |
| 449 | |
| 450 | # Escaped quote in double quotes |
| 451 | result=$("$FORTSH_BIN" -c 'echo "say \"hello\""' 2>&1) |
| 452 | if [ "$result" = 'say "hello"' ]; then |
| 453 | pass "Escaped quotes in double quotes" |
| 454 | else |
| 455 | fail "Escaped quotes in double quotes" 'say "hello"' "$result" |
| 456 | fi |
| 457 | |
| 458 | # ===================================== |
| 459 | section "368. WORD SPLITTING EDGE CASES" |
| 460 | # ===================================== |
| 461 | |
| 462 | # Empty IFS prevents splitting |
| 463 | result=$("$FORTSH_BIN" -c 'IFS=""; x="a b c"; set -- $x; echo $#' 2>&1) |
| 464 | if [ "$result" = "1" ]; then |
| 465 | pass "Empty IFS prevents word splitting" |
| 466 | else |
| 467 | fail "Empty IFS prevents word splitting" "1" "$result" |
| 468 | fi |
| 469 | |
| 470 | # IFS with multiple characters |
| 471 | result=$("$FORTSH_BIN" -c 'IFS=":,"; x="a:b,c"; set -- $x; echo $#' 2>&1) |
| 472 | if [ "$result" = "3" ]; then |
| 473 | pass "IFS with multiple delimiters" |
| 474 | else |
| 475 | fail "IFS with multiple delimiters" "3" "$result" |
| 476 | fi |
| 477 | |
| 478 | # Default IFS restoration |
| 479 | result=$("$FORTSH_BIN" -c 'IFS=":"; x="a:b"; set -- $x; unset IFS; y="c d"; set -- $y; echo $#' 2>&1) |
| 480 | if [ "$result" = "2" ]; then |
| 481 | pass "unset IFS restores default splitting" |
| 482 | else |
| 483 | fail "unset IFS restores default splitting" "2" "$result" |
| 484 | fi |
| 485 | |
| 486 | # ===================================== |
| 487 | section "369. $$ IN SUBSHELLS" |
| 488 | # ===================================== |
| 489 | |
| 490 | # $$ in subshell should return parent shell PID per POSIX |
| 491 | # Test within SAME shell instance (not separate invocations) |
| 492 | result=$("$FORTSH_BIN" -c 'echo $$; (echo $$)' 2>&1) |
| 493 | parent_pid=$(echo "$result" | sed -n '1p') |
| 494 | subshell_pid=$(echo "$result" | sed -n '2p') |
| 495 | if [ "$parent_pid" = "$subshell_pid" ]; then |
| 496 | pass "\$\$ in subshell returns parent shell PID" |
| 497 | else |
| 498 | fail "\$\$ in subshell returns parent shell PID" "$parent_pid" "$subshell_pid" |
| 499 | fi |
| 500 | |
| 501 | # $$ in command substitution |
| 502 | result=$("$FORTSH_BIN" -c 'echo $$ $(echo $$)' 2>&1) |
| 503 | # Both should be the same PID |
| 504 | pid1=$(echo "$result" | awk '{print $1}') |
| 505 | pid2=$(echo "$result" | awk '{print $2}') |
| 506 | if [ "$pid1" = "$pid2" ]; then |
| 507 | pass "\$\$ in command substitution matches parent" |
| 508 | else |
| 509 | fail "\$\$ in command substitution matches parent" "same" "$result" |
| 510 | fi |
| 511 | |
| 512 | # $$ is numeric |
| 513 | result=$("$FORTSH_BIN" -c 'echo $$' 2>&1) |
| 514 | if [ "$result" -gt 0 ] 2>/dev/null; then |
| 515 | pass "\$\$ is positive integer" |
| 516 | else |
| 517 | fail "\$\$ is positive integer" ">0" "$result" |
| 518 | fi |
| 519 | |
| 520 | # ===================================== |
| 521 | section "370. \$@ VS \$* DIFFERENCES" |
| 522 | # ===================================== |
| 523 | |
| 524 | # $@ preserves separate arguments |
| 525 | result=$("$FORTSH_BIN" -c 'set -- "a b" "c d"; for x in "$@"; do echo "[$x]"; done' 2>&1) |
| 526 | expected=$(printf "[a b]\n[c d]") |
| 527 | if [ "$result" = "$expected" ]; then |
| 528 | pass '"\$@" preserves argument boundaries' |
| 529 | else |
| 530 | fail '"\$@" preserves argument boundaries' "$expected" "$result" |
| 531 | fi |
| 532 | |
| 533 | # $* joins with IFS |
| 534 | result=$("$FORTSH_BIN" -c 'IFS=":"; set -- a b c; echo "$*"' 2>&1) |
| 535 | if [ "$result" = "a:b:c" ]; then |
| 536 | pass '"\$*" joins with first char of IFS' |
| 537 | else |
| 538 | fail '"\$*" joins with first char of IFS' "a:b:c" "$result" |
| 539 | fi |
| 540 | |
| 541 | # $@ unquoted splits |
| 542 | result=$("$FORTSH_BIN" -c 'set -- "a b" "c d"; for x in $@; do echo "[$x]"; done' 2>&1) |
| 543 | # Should split into 4 words: a, b, c, d |
| 544 | expected=$(printf "[a]\n[b]\n[c]\n[d]") |
| 545 | if [ "$result" = "$expected" ]; then |
| 546 | pass 'Unquoted $@ splits on whitespace' |
| 547 | else |
| 548 | fail 'Unquoted $@ splits on whitespace' "$expected" "$result" |
| 549 | fi |
| 550 | |
| 551 | # Empty $@ produces nothing |
| 552 | result=$("$FORTSH_BIN" -c 'set --; for x in "$@"; do echo X; done; echo done' 2>&1) |
| 553 | if [ "$result" = "done" ]; then |
| 554 | pass 'Empty "$@" produces no iterations' |
| 555 | else |
| 556 | fail 'Empty "$@" produces no iterations' "done" "$result" |
| 557 | fi |
| 558 | |
| 559 | # $# count |
| 560 | result=$("$FORTSH_BIN" -c 'set -- a b c d e; echo $#' 2>&1) |
| 561 | if [ "$result" = "5" ]; then |
| 562 | pass '$# counts positional parameters' |
| 563 | else |
| 564 | fail '$# counts positional parameters' "5" "$result" |
| 565 | fi |
| 566 | |
| 567 | # ===================================== |
| 568 | section "371. PWD AND OLDPWD" |
| 569 | # ===================================== |
| 570 | |
| 571 | # PWD is set |
| 572 | result=$("$FORTSH_BIN" -c 'echo $PWD' 2>&1) |
| 573 | if [ -n "$result" ] && [ -d "$result" ]; then |
| 574 | pass "PWD is set to valid directory" |
| 575 | else |
| 576 | fail "PWD is set to valid directory" "directory path" "$result" |
| 577 | fi |
| 578 | |
| 579 | # cd updates PWD |
| 580 | result=$("$FORTSH_BIN" -c 'cd /tmp && echo $PWD' 2>&1) |
| 581 | if echo "$result" | grep -q "tmp"; then |
| 582 | pass "cd updates PWD" |
| 583 | else |
| 584 | fail "cd updates PWD" "/tmp" "$result" |
| 585 | fi |
| 586 | |
| 587 | # cd updates OLDPWD |
| 588 | result=$("$FORTSH_BIN" -c 'cd /; cd /tmp; echo $OLDPWD' 2>&1) |
| 589 | if [ "$result" = "/" ]; then |
| 590 | pass "cd updates OLDPWD" |
| 591 | else |
| 592 | fail "cd updates OLDPWD" "/" "$result" |
| 593 | fi |
| 594 | |
| 595 | # cd - uses OLDPWD |
| 596 | result=$("$FORTSH_BIN" -c 'cd /; cd /tmp; cd -' 2>&1) |
| 597 | if echo "$result" | grep -q "/"; then |
| 598 | pass "cd - prints previous directory" |
| 599 | else |
| 600 | fail "cd - prints previous directory" "/" "$result" |
| 601 | fi |
| 602 | |
| 603 | # ===================================== |
| 604 | section "372. \$? EXIT STATUS" |
| 605 | # ===================================== |
| 606 | |
| 607 | # $? after successful command |
| 608 | result=$("$FORTSH_BIN" -c 'true; echo $?' 2>&1) |
| 609 | if [ "$result" = "0" ]; then |
| 610 | pass '$? is 0 after true' |
| 611 | else |
| 612 | fail '$? is 0 after true' "0" "$result" |
| 613 | fi |
| 614 | |
| 615 | # $? after failed command |
| 616 | result=$("$FORTSH_BIN" -c 'false; echo $?' 2>&1) |
| 617 | if [ "$result" = "1" ]; then |
| 618 | pass '$? is 1 after false' |
| 619 | else |
| 620 | fail '$? is 1 after false' "1" "$result" |
| 621 | fi |
| 622 | |
| 623 | # $? after exit N |
| 624 | result=$("$FORTSH_BIN" -c '(exit 42); echo $?' 2>&1) |
| 625 | if [ "$result" = "42" ]; then |
| 626 | pass '$? captures exit code from subshell' |
| 627 | else |
| 628 | fail '$? captures exit code from subshell' "42" "$result" |
| 629 | fi |
| 630 | |
| 631 | # $? after signal (128+signal) |
| 632 | result=$("$FORTSH_BIN" -c 'sh -c "kill -9 \$\$" 2>/dev/null; echo $?' 2>&1) |
| 633 | if [ "$result" -ge 128 ] 2>/dev/null; then |
| 634 | pass '$? is 128+ after signal death' |
| 635 | else |
| 636 | fail '$? is 128+ after signal death' ">=128" "$result" |
| 637 | fi |
| 638 | |
| 639 | # $? after command not found |
| 640 | result=$("$FORTSH_BIN" -c 'nonexistent_cmd_xyz_12345 2>/dev/null; echo $?' 2>&1) |
| 641 | if [ "$result" = "127" ]; then |
| 642 | pass '$? is 127 for command not found' |
| 643 | else |
| 644 | fail '$? is 127 for command not found' "127" "$result" |
| 645 | fi |
| 646 | |
| 647 | # ===================================== |
| 648 | section "373. \$0 SCRIPT NAME" |
| 649 | # ===================================== |
| 650 | |
| 651 | # $0 in -c command |
| 652 | result=$("$FORTSH_BIN" -c 'echo $0' 2>&1) |
| 653 | if [ -n "$result" ]; then |
| 654 | pass '$0 is set in -c command' |
| 655 | else |
| 656 | fail '$0 is set in -c command' "non-empty" "$result" |
| 657 | fi |
| 658 | |
| 659 | # $0 can be set with -c |
| 660 | result=$("$FORTSH_BIN" -c 'echo $0' myname 2>&1) |
| 661 | if [ "$result" = "myname" ]; then |
| 662 | pass '$0 can be set via argument after -c' |
| 663 | else |
| 664 | fail '$0 can be set via argument after -c' "myname" "$result" |
| 665 | fi |
| 666 | |
| 667 | # ===================================== |
| 668 | section "374. SHIFT EDGE CASES" |
| 669 | # ===================================== |
| 670 | |
| 671 | # shift removes first positional param |
| 672 | result=$("$FORTSH_BIN" -c 'set -- a b c; shift; echo $1' 2>&1) |
| 673 | if [ "$result" = "b" ]; then |
| 674 | pass 'shift removes first parameter' |
| 675 | else |
| 676 | fail 'shift removes first parameter' "b" "$result" |
| 677 | fi |
| 678 | |
| 679 | # shift N removes N params |
| 680 | result=$("$FORTSH_BIN" -c 'set -- a b c d e; shift 2; echo $1' 2>&1) |
| 681 | if [ "$result" = "c" ]; then |
| 682 | pass 'shift N removes N parameters' |
| 683 | else |
| 684 | fail 'shift N removes N parameters' "c" "$result" |
| 685 | fi |
| 686 | |
| 687 | # shift updates $# |
| 688 | result=$("$FORTSH_BIN" -c 'set -- a b c d e; shift 3; echo $#' 2>&1) |
| 689 | if [ "$result" = "2" ]; then |
| 690 | pass 'shift updates $#' |
| 691 | else |
| 692 | fail 'shift updates $#' "2" "$result" |
| 693 | fi |
| 694 | |
| 695 | # shift more than $# fails |
| 696 | result=$("$FORTSH_BIN" -c 'set -- a b; shift 5 2>/dev/null; echo $?' 2>&1) |
| 697 | if [ "$result" != "0" ]; then |
| 698 | pass 'shift beyond $# returns non-zero' |
| 699 | else |
| 700 | fail 'shift beyond $# returns non-zero' "non-zero" "$result" |
| 701 | fi |
| 702 | |
| 703 | # ===================================== |
| 704 | section "375. ENVIRONMENT VARIABLES" |
| 705 | # ===================================== |
| 706 | |
| 707 | # HOME is set |
| 708 | result=$("$FORTSH_BIN" -c 'echo $HOME' 2>&1) |
| 709 | if [ -n "$result" ] && [ -d "$result" ]; then |
| 710 | pass "HOME is set to valid directory" |
| 711 | else |
| 712 | fail "HOME is set to valid directory" "directory" "$result" |
| 713 | fi |
| 714 | |
| 715 | # PATH is set |
| 716 | result=$("$FORTSH_BIN" -c 'echo $PATH' 2>&1) |
| 717 | if [ -n "$result" ]; then |
| 718 | pass "PATH is set" |
| 719 | else |
| 720 | fail "PATH is set" "non-empty" "$result" |
| 721 | fi |
| 722 | |
| 723 | # PATH affects command lookup - use actual system paths |
| 724 | LS_DIR=$(dirname "$(which ls 2>/dev/null)") |
| 725 | result=$("$FORTSH_BIN" -c "PATH=$LS_DIR; ls / >/dev/null 2>&1; echo \$?" 2>&1) |
| 726 | if [ "$result" = "0" ]; then |
| 727 | pass "PATH affects command resolution" |
| 728 | else |
| 729 | fail "PATH affects command resolution" "0" "$result" |
| 730 | fi |
| 731 | |
| 732 | # Empty PATH means current directory only |
| 733 | result=$("$FORTSH_BIN" -c 'PATH=""; ls / 2>/dev/null; echo $?' 2>&1) |
| 734 | if [ "$result" != "0" ]; then |
| 735 | pass "Empty PATH prevents finding commands" |
| 736 | else |
| 737 | fail "Empty PATH prevents finding commands" "non-zero" "$result" |
| 738 | fi |
| 739 | |
| 740 | # ===================================== |
| 741 | section "376. EVAL SPECIAL CASES" |
| 742 | # ===================================== |
| 743 | |
| 744 | # eval with variable containing command |
| 745 | result=$("$FORTSH_BIN" -c 'cmd="echo hello"; eval "$cmd"' 2>&1) |
| 746 | if [ "$result" = "hello" ]; then |
| 747 | pass "eval executes command in variable" |
| 748 | else |
| 749 | fail "eval executes command in variable" "hello" "$result" |
| 750 | fi |
| 751 | |
| 752 | # eval with multiple arguments |
| 753 | result=$("$FORTSH_BIN" -c 'eval echo hello world' 2>&1) |
| 754 | if [ "$result" = "hello world" ]; then |
| 755 | pass "eval concatenates arguments" |
| 756 | else |
| 757 | fail "eval concatenates arguments" "hello world" "$result" |
| 758 | fi |
| 759 | |
| 760 | # eval with variable expansion |
| 761 | result=$("$FORTSH_BIN" -c 'x=y; y=z; eval echo \$$x' 2>&1) |
| 762 | if [ "$result" = "z" ]; then |
| 763 | pass "eval performs double expansion" |
| 764 | else |
| 765 | fail "eval performs double expansion" "z" "$result" |
| 766 | fi |
| 767 | |
| 768 | # eval exit status |
| 769 | result=$("$FORTSH_BIN" -c 'eval false; echo $?' 2>&1) |
| 770 | if [ "$result" = "1" ]; then |
| 771 | pass "eval returns command exit status" |
| 772 | else |
| 773 | fail "eval returns command exit status" "1" "$result" |
| 774 | fi |
| 775 | |
| 776 | # ===================================== |
| 777 | section "377. COMMAND EXECUTION MODES" |
| 778 | # ===================================== |
| 779 | |
| 780 | # Command with -c flag |
| 781 | result=$("$FORTSH_BIN" -c 'echo test' 2>&1) |
| 782 | if [ "$result" = "test" ]; then |
| 783 | pass "fortsh -c executes command" |
| 784 | else |
| 785 | fail "fortsh -c executes command" "test" "$result" |
| 786 | fi |
| 787 | |
| 788 | # Command with environment var |
| 789 | result=$(X=value "$FORTSH_BIN" -c 'echo $X' 2>&1) |
| 790 | if [ "$result" = "value" ]; then |
| 791 | pass "env var passed to fortsh" |
| 792 | else |
| 793 | fail "env var passed to fortsh" "value" "$result" |
| 794 | fi |
| 795 | |
| 796 | # ===================================== |
| 797 | section "378. SPECIAL BUILTIN BEHAVIORS" |
| 798 | # ===================================== |
| 799 | |
| 800 | # break outside loop |
| 801 | result=$("$FORTSH_BIN" -c 'break 2>/dev/null; echo $?' 2>&1) |
| 802 | if echo "$result" | grep -qE '^[0-9]'; then |
| 803 | pass "break outside loop returns error status" |
| 804 | else |
| 805 | fail "break outside loop returns error status" |
| 806 | fi |
| 807 | |
| 808 | # continue outside loop |
| 809 | result=$("$FORTSH_BIN" -c 'continue 2>/dev/null; echo $?' 2>&1) |
| 810 | if echo "$result" | grep -qE '^[0-9]'; then |
| 811 | pass "continue outside loop returns error status" |
| 812 | else |
| 813 | fail "continue outside loop returns error status" |
| 814 | fi |
| 815 | |
| 816 | # return outside function |
| 817 | result=$("$FORTSH_BIN" -c 'return 2>/dev/null; echo $?' 2>&1) |
| 818 | if echo "$result" | grep -qE '^[0-9]'; then |
| 819 | pass "return outside function handled" |
| 820 | else |
| 821 | fail "return outside function handled" |
| 822 | fi |
| 823 | |
| 824 | # ===================================== |
| 825 | section "379. REDIRECTION EDGE CASES" |
| 826 | # ===================================== |
| 827 | |
| 828 | # Redirect to /dev/null |
| 829 | result=$("$FORTSH_BIN" -c 'echo test > /dev/null; echo done' 2>&1) |
| 830 | if [ "$result" = "done" ]; then |
| 831 | pass "redirect to /dev/null works" |
| 832 | else |
| 833 | fail "redirect to /dev/null works" "done" "$result" |
| 834 | fi |
| 835 | |
| 836 | # Redirect stderr to stdout |
| 837 | result=$("$FORTSH_BIN" -c 'echo stderr >&2' 2>&1) |
| 838 | if [ "$result" = "stderr" ]; then |
| 839 | pass "redirect stderr to stdout" |
| 840 | else |
| 841 | fail "redirect stderr to stdout" "stderr" "$result" |
| 842 | fi |
| 843 | |
| 844 | # ===================================== |
| 845 | section "380. POSITIONAL PARAMETERS EDGE CASES" |
| 846 | # ===================================== |
| 847 | |
| 848 | # More than 9 positional params |
| 849 | result=$("$FORTSH_BIN" -c 'set -- a b c d e f g h i j k; echo $1 ${10} ${11}' 2>&1) |
| 850 | if echo "$result" | grep -q "a j k"; then |
| 851 | pass "access to \${10} and beyond" |
| 852 | else |
| 853 | fail "access to \${10} and beyond" "a j k" "$result" |
| 854 | fi |
| 855 | |
| 856 | # shift with count |
| 857 | result=$("$FORTSH_BIN" -c 'set -- a b c d e; shift 3; echo $1' 2>&1) |
| 858 | if [ "$result" = "d" ]; then |
| 859 | pass "shift with count" |
| 860 | else |
| 861 | fail "shift with count" "d" "$result" |
| 862 | fi |
| 863 | |
| 864 | # ===================================== |
| 865 | section "381. ARITHMETIC EDGE CASES" |
| 866 | # ===================================== |
| 867 | |
| 868 | # Negative numbers |
| 869 | result=$("$FORTSH_BIN" -c 'echo $((-5))' 2>&1) |
| 870 | if [ "$result" = "-5" ]; then |
| 871 | pass "negative numbers in arithmetic" |
| 872 | else |
| 873 | fail "negative numbers in arithmetic" "-5" "$result" |
| 874 | fi |
| 875 | |
| 876 | # Parentheses for grouping |
| 877 | result=$("$FORTSH_BIN" -c 'echo $(( (2+3) * 4 ))' 2>&1) |
| 878 | if [ "$result" = "20" ]; then |
| 879 | pass "parentheses in arithmetic" |
| 880 | else |
| 881 | fail "parentheses in arithmetic" "20" "$result" |
| 882 | fi |
| 883 | |
| 884 | # Comparison operators return 0/1 |
| 885 | result=$("$FORTSH_BIN" -c 'echo $((5 > 3))' 2>&1) |
| 886 | if [ "$result" = "1" ]; then |
| 887 | pass "arithmetic comparison returns 1 for true" |
| 888 | else |
| 889 | fail "arithmetic comparison returns 1 for true" "1" "$result" |
| 890 | fi |
| 891 | |
| 892 | result=$("$FORTSH_BIN" -c 'echo $((3 > 5))' 2>&1) |
| 893 | if [ "$result" = "0" ]; then |
| 894 | pass "arithmetic comparison returns 0 for false" |
| 895 | else |
| 896 | fail "arithmetic comparison returns 0 for false" "0" "$result" |
| 897 | fi |
| 898 | |
| 899 | # ===================================== |
| 900 | section "382. VARIABLE ASSIGNMENT EDGE CASES" |
| 901 | # ===================================== |
| 902 | |
| 903 | # Assignment with empty value |
| 904 | result=$("$FORTSH_BIN" -c 'X=; echo "[$X]"' 2>&1) |
| 905 | if [ "$result" = "[]" ]; then |
| 906 | pass "empty variable assignment" |
| 907 | else |
| 908 | fail "empty variable assignment" "[]" "$result" |
| 909 | fi |
| 910 | |
| 911 | # Assignment with quotes |
| 912 | result=$("$FORTSH_BIN" -c 'X="hello world"; echo $X' 2>&1) |
| 913 | if [ "$result" = "hello world" ]; then |
| 914 | pass "quoted variable assignment" |
| 915 | else |
| 916 | fail "quoted variable assignment" "hello world" "$result" |
| 917 | fi |
| 918 | |
| 919 | # Multiple assignments on one line |
| 920 | result=$("$FORTSH_BIN" -c 'A=1 B=2 C=3; echo $A $B $C' 2>&1) |
| 921 | if [ "$result" = "1 2 3" ]; then |
| 922 | pass "multiple assignments" |
| 923 | else |
| 924 | fail "multiple assignments" "1 2 3" "$result" |
| 925 | fi |
| 926 | |
| 927 | # ===================================== |
| 928 | section "383. QUOTING EDGE CASES" |
| 929 | # ===================================== |
| 930 | |
| 931 | # Single quotes preserve literally |
| 932 | result=$("$FORTSH_BIN" -c "echo '\$HOME'" 2>&1) |
| 933 | if [ "$result" = '$HOME' ]; then |
| 934 | pass "single quotes preserve dollar" |
| 935 | else |
| 936 | fail "single quotes preserve dollar" "\$HOME" "$result" |
| 937 | fi |
| 938 | |
| 939 | # Double quotes allow expansion |
| 940 | result=$("$FORTSH_BIN" -c 'X=test; echo "$X"' 2>&1) |
| 941 | if [ "$result" = "test" ]; then |
| 942 | pass "double quotes allow expansion" |
| 943 | else |
| 944 | fail "double quotes allow expansion" "test" "$result" |
| 945 | fi |
| 946 | |
| 947 | # Mixed quoting |
| 948 | result=$("$FORTSH_BIN" -c "X=val; echo 'literal'\"\$X\"'more'" 2>&1) |
| 949 | if [ "$result" = "literalvalmore" ]; then |
| 950 | pass "mixed quoting works" |
| 951 | else |
| 952 | fail "mixed quoting works" "literalvalmore" "$result" |
| 953 | fi |
| 954 | |
| 955 | # ===================================== |
| 956 | section "384. PIPELINE EDGE CASES" |
| 957 | # ===================================== |
| 958 | |
| 959 | # Pipeline with three stages |
| 960 | result=$("$FORTSH_BIN" -c 'echo test | cat | cat' 2>&1) |
| 961 | if [ "$result" = "test" ]; then |
| 962 | pass "three-stage pipeline" |
| 963 | else |
| 964 | fail "three-stage pipeline" "test" "$result" |
| 965 | fi |
| 966 | |
| 967 | # Pipeline with grep |
| 968 | result=$("$FORTSH_BIN" -c 'echo "hello world" | grep hello' 2>&1) |
| 969 | if [ "$result" = "hello world" ]; then |
| 970 | pass "pipeline with grep" |
| 971 | else |
| 972 | fail "pipeline with grep" "hello world" "$result" |
| 973 | fi |
| 974 | |
| 975 | # ===================================== |
| 976 | section "385. CASE STATEMENT EDGE CASES" |
| 977 | # ===================================== |
| 978 | |
| 979 | # Case with wildcards |
| 980 | result=$("$FORTSH_BIN" -c 'x=hello; case $x in h*) echo match;; esac' 2>&1) |
| 981 | if [ "$result" = "match" ]; then |
| 982 | pass "case with wildcard pattern" |
| 983 | else |
| 984 | fail "case with wildcard pattern" "match" "$result" |
| 985 | fi |
| 986 | |
| 987 | # Case with multiple patterns |
| 988 | result=$("$FORTSH_BIN" -c 'x=b; case $x in a|b|c) echo abc;; esac' 2>&1) |
| 989 | if [ "$result" = "abc" ]; then |
| 990 | pass "case with multiple patterns" |
| 991 | else |
| 992 | fail "case with multiple patterns" "abc" "$result" |
| 993 | fi |
| 994 | |
| 995 | # Case with default |
| 996 | result=$("$FORTSH_BIN" -c 'x=z; case $x in a) echo a;; *) echo default;; esac' 2>&1) |
| 997 | if [ "$result" = "default" ]; then |
| 998 | pass "case with default pattern" |
| 999 | else |
| 1000 | fail "case with default pattern" "default" "$result" |
| 1001 | fi |
| 1002 | |
| 1003 | # ===================================== |
| 1004 | section "386. LOOP EDGE CASES" |
| 1005 | # ===================================== |
| 1006 | |
| 1007 | # For loop with break |
| 1008 | result=$("$FORTSH_BIN" -c 'for i in 1 2 3 4 5; do [ $i -eq 3 ] && break; echo $i; done' 2>&1) |
| 1009 | expected="1 |
| 1010 | 2" |
| 1011 | if [ "$result" = "$expected" ]; then |
| 1012 | pass "for loop with break" |
| 1013 | else |
| 1014 | fail "for loop with break" |
| 1015 | fi |
| 1016 | |
| 1017 | # For loop with continue |
| 1018 | result=$("$FORTSH_BIN" -c 'for i in 1 2 3 4 5; do [ $i -eq 3 ] && continue; echo $i; done' 2>&1) |
| 1019 | if echo "$result" | grep -q "1" && echo "$result" | grep -q "4" && ! echo "$result" | grep -q "3"; then |
| 1020 | pass "for loop with continue" |
| 1021 | else |
| 1022 | fail "for loop with continue" |
| 1023 | fi |
| 1024 | |
| 1025 | # ===================================== |
| 1026 | section "387. SUBSHELL EDGE CASES" |
| 1027 | # ===================================== |
| 1028 | |
| 1029 | # Subshell variable isolation |
| 1030 | result=$("$FORTSH_BIN" -c 'X=outer; (X=inner; echo $X); echo $X' 2>&1) |
| 1031 | expected="inner |
| 1032 | outer" |
| 1033 | if [ "$result" = "$expected" ]; then |
| 1034 | pass "subshell variable isolation" |
| 1035 | else |
| 1036 | fail "subshell variable isolation" |
| 1037 | fi |
| 1038 | |
| 1039 | # Subshell exit status |
| 1040 | result=$("$FORTSH_BIN" -c '(exit 42); echo $?' 2>&1) |
| 1041 | if [ "$result" = "42" ]; then |
| 1042 | pass "subshell exit status captured" |
| 1043 | else |
| 1044 | fail "subshell exit status captured" "42" "$result" |
| 1045 | fi |
| 1046 | |
| 1047 | # ===================================== |
| 1048 | section "388. FUNCTION EDGE CASES" |
| 1049 | # ===================================== |
| 1050 | |
| 1051 | # Function with local-like behavior (via subshell) |
| 1052 | result=$("$FORTSH_BIN" -c 'X=global; f() { (X=local; echo $X); }; f; echo $X' 2>&1) |
| 1053 | expected="local |
| 1054 | global" |
| 1055 | if [ "$result" = "$expected" ]; then |
| 1056 | pass "function with subshell for local vars" |
| 1057 | else |
| 1058 | fail "function with subshell for local vars" |
| 1059 | fi |
| 1060 | |
| 1061 | # Recursive function |
| 1062 | result=$("$FORTSH_BIN" -c 'count() { if [ $1 -gt 0 ]; then echo $1; count $(($1-1)); fi; }; count 3' 2>&1) |
| 1063 | expected="3 |
| 1064 | 2 |
| 1065 | 1" |
| 1066 | if [ "$result" = "$expected" ]; then |
| 1067 | pass "recursive function" |
| 1068 | else |
| 1069 | fail "recursive function" |
| 1070 | fi |
| 1071 | |
| 1072 | # ===================================== |
| 1073 | section "389. HERE DOCUMENT EDGE CASES" |
| 1074 | # ===================================== |
| 1075 | |
| 1076 | # Heredoc with variable expansion |
| 1077 | result=$("$FORTSH_BIN" -c 'X=value; cat <<EOF |
| 1078 | $X |
| 1079 | EOF' 2>&1) |
| 1080 | if [ "$result" = "value" ]; then |
| 1081 | pass "heredoc variable expansion" |
| 1082 | else |
| 1083 | fail "heredoc variable expansion" "value" "$result" |
| 1084 | fi |
| 1085 | |
| 1086 | # Quoted heredoc (no expansion) |
| 1087 | result=$("$FORTSH_BIN" -c 'cat <<'\''EOF'\'' |
| 1088 | $VAR |
| 1089 | EOF' 2>&1) |
| 1090 | if [ "$result" = '$VAR' ]; then |
| 1091 | pass "quoted heredoc no expansion" |
| 1092 | else |
| 1093 | fail "quoted heredoc no expansion" "\$VAR" "$result" |
| 1094 | fi |
| 1095 | |
| 1096 | # ===================================== |
| 1097 | section "390. COMMAND SUBSTITUTION EDGE CASES" |
| 1098 | # ===================================== |
| 1099 | |
| 1100 | # Nested command substitution |
| 1101 | result=$("$FORTSH_BIN" -c 'echo $(echo $(echo nested))' 2>&1) |
| 1102 | if [ "$result" = "nested" ]; then |
| 1103 | pass "nested command substitution" |
| 1104 | else |
| 1105 | fail "nested command substitution" "nested" "$result" |
| 1106 | fi |
| 1107 | |
| 1108 | # Command substitution with quotes |
| 1109 | result=$("$FORTSH_BIN" -c 'echo "$(echo "hello world")"' 2>&1) |
| 1110 | if [ "$result" = "hello world" ]; then |
| 1111 | pass "command substitution with quotes" |
| 1112 | else |
| 1113 | fail "command substitution with quotes" "hello world" "$result" |
| 1114 | fi |
| 1115 | |
| 1116 | # Backtick substitution |
| 1117 | result=$("$FORTSH_BIN" -c 'echo `echo backtick`' 2>&1) |
| 1118 | if [ "$result" = "backtick" ]; then |
| 1119 | pass "backtick command substitution" |
| 1120 | else |
| 1121 | fail "backtick command substitution" "backtick" "$result" |
| 1122 | fi |
| 1123 | |
| 1124 | # ===================================== |
| 1125 | section "391. EXPR COMMAND" |
| 1126 | # ===================================== |
| 1127 | |
| 1128 | result=$("$FORTSH_BIN" -c 'expr 5 + 3' 2>&1) |
| 1129 | if [ "$result" = "8" ]; then |
| 1130 | pass "expr addition" |
| 1131 | else |
| 1132 | fail "expr addition" "8" "$result" |
| 1133 | fi |
| 1134 | |
| 1135 | result=$("$FORTSH_BIN" -c 'expr 10 - 4' 2>&1) |
| 1136 | if [ "$result" = "6" ]; then |
| 1137 | pass "expr subtraction" |
| 1138 | else |
| 1139 | fail "expr subtraction" "6" "$result" |
| 1140 | fi |
| 1141 | |
| 1142 | result=$("$FORTSH_BIN" -c 'expr 6 \* 7' 2>&1) |
| 1143 | if [ "$result" = "42" ]; then |
| 1144 | pass "expr multiplication" |
| 1145 | else |
| 1146 | fail "expr multiplication" "42" "$result" |
| 1147 | fi |
| 1148 | |
| 1149 | result=$("$FORTSH_BIN" -c 'expr 20 / 4' 2>&1) |
| 1150 | if [ "$result" = "5" ]; then |
| 1151 | pass "expr division" |
| 1152 | else |
| 1153 | fail "expr division" "5" "$result" |
| 1154 | fi |
| 1155 | |
| 1156 | # ===================================== |
| 1157 | section "392. TEST COMMAND VARIATIONS" |
| 1158 | # ===================================== |
| 1159 | |
| 1160 | result=$("$FORTSH_BIN" -c 'test -f /etc/passwd && echo yes' 2>&1) |
| 1161 | if [ "$result" = "yes" ]; then |
| 1162 | pass "test -f regular file" |
| 1163 | else |
| 1164 | fail "test -f regular file" "yes" "$result" |
| 1165 | fi |
| 1166 | |
| 1167 | result=$("$FORTSH_BIN" -c 'test -d /tmp && echo yes' 2>&1) |
| 1168 | if [ "$result" = "yes" ]; then |
| 1169 | pass "test -d directory" |
| 1170 | else |
| 1171 | fail "test -d directory" "yes" "$result" |
| 1172 | fi |
| 1173 | |
| 1174 | result=$("$FORTSH_BIN" -c 'test 5 -eq 5 && echo yes' 2>&1) |
| 1175 | if [ "$result" = "yes" ]; then |
| 1176 | pass "test numeric equal" |
| 1177 | else |
| 1178 | fail "test numeric equal" "yes" "$result" |
| 1179 | fi |
| 1180 | |
| 1181 | result=$("$FORTSH_BIN" -c 'test "abc" = "abc" && echo yes' 2>&1) |
| 1182 | if [ "$result" = "yes" ]; then |
| 1183 | pass "test string equal" |
| 1184 | else |
| 1185 | fail "test string equal" "yes" "$result" |
| 1186 | fi |
| 1187 | |
| 1188 | # ===================================== |
| 1189 | section "393. BASENAME AND DIRNAME SIMULATION" |
| 1190 | # ===================================== |
| 1191 | |
| 1192 | result=$("$FORTSH_BIN" -c 'X=/path/to/file.txt; echo ${X##*/}' 2>&1) |
| 1193 | if [ "$result" = "file.txt" ]; then |
| 1194 | pass "basename via parameter expansion" |
| 1195 | else |
| 1196 | fail "basename via parameter expansion" "file.txt" "$result" |
| 1197 | fi |
| 1198 | |
| 1199 | result=$("$FORTSH_BIN" -c 'X=/path/to/file.txt; echo ${X%/*}' 2>&1) |
| 1200 | if [ "$result" = "/path/to" ]; then |
| 1201 | pass "dirname via parameter expansion" |
| 1202 | else |
| 1203 | fail "dirname via parameter expansion" "/path/to" "$result" |
| 1204 | fi |
| 1205 | |
| 1206 | result=$("$FORTSH_BIN" -c 'X=file.tar.gz; echo ${X%.gz}' 2>&1) |
| 1207 | if [ "$result" = "file.tar" ]; then |
| 1208 | pass "remove extension" |
| 1209 | else |
| 1210 | fail "remove extension" "file.tar" "$result" |
| 1211 | fi |
| 1212 | |
| 1213 | result=$("$FORTSH_BIN" -c 'X=file.tar.gz; echo ${X%%.*}' 2>&1) |
| 1214 | if [ "$result" = "file" ]; then |
| 1215 | pass "remove all extensions" |
| 1216 | else |
| 1217 | fail "remove all extensions" "file" "$result" |
| 1218 | fi |
| 1219 | |
| 1220 | # ===================================== |
| 1221 | section "394. COMPLEX PIPELINES" |
| 1222 | # ===================================== |
| 1223 | |
| 1224 | result=$("$FORTSH_BIN" -c 'echo "hello world" | tr " " "\n" | wc -l' 2>&1) |
| 1225 | if [ "$result" -eq 2 ] 2>/dev/null; then |
| 1226 | pass "pipeline with tr and wc" |
| 1227 | else |
| 1228 | fail "pipeline with tr and wc" "2" "$result" |
| 1229 | fi |
| 1230 | |
| 1231 | result=$("$FORTSH_BIN" -c 'printf "c\na\nb\n" | sort | head -1' 2>&1) |
| 1232 | if [ "$result" = "a" ]; then |
| 1233 | pass "pipeline sort and head" |
| 1234 | else |
| 1235 | fail "pipeline sort and head" "a" "$result" |
| 1236 | fi |
| 1237 | |
| 1238 | result=$("$FORTSH_BIN" -c 'echo "test" | cat | cat | cat' 2>&1) |
| 1239 | if [ "$result" = "test" ]; then |
| 1240 | pass "triple cat pipeline" |
| 1241 | else |
| 1242 | fail "triple cat pipeline" "test" "$result" |
| 1243 | fi |
| 1244 | |
| 1245 | # ===================================== |
| 1246 | section "395. COMMAND GROUPING" |
| 1247 | # ===================================== |
| 1248 | |
| 1249 | result=$("$FORTSH_BIN" -c '{ echo a; echo b; echo c; } | wc -l' 2>&1) |
| 1250 | if [ "$result" -eq 3 ] 2>/dev/null; then |
| 1251 | pass "brace group to pipeline" |
| 1252 | else |
| 1253 | fail "brace group to pipeline" "3" "$result" |
| 1254 | fi |
| 1255 | |
| 1256 | result=$("$FORTSH_BIN" -c '(echo x; echo y) | wc -l' 2>&1) |
| 1257 | if [ "$result" -eq 2 ] 2>/dev/null; then |
| 1258 | pass "subshell to pipeline" |
| 1259 | else |
| 1260 | fail "subshell to pipeline" "2" "$result" |
| 1261 | fi |
| 1262 | |
| 1263 | result=$("$FORTSH_BIN" -c 'X=1; { X=2; echo $X; }; echo $X' 2>&1) |
| 1264 | expected=$(printf "2\n2") |
| 1265 | if [ "$result" = "$expected" ]; then |
| 1266 | pass "brace group modifies parent var" |
| 1267 | else |
| 1268 | fail "brace group modifies parent var" |
| 1269 | fi |
| 1270 | |
| 1271 | # ===================================== |
| 1272 | section "396. WORD EXPANSION ORDER" |
| 1273 | # ===================================== |
| 1274 | |
| 1275 | result=$("$FORTSH_BIN" -c 'X="a b c"; echo $X' 2>&1) |
| 1276 | if [ "$result" = "a b c" ]; then |
| 1277 | pass "unquoted var word splits" |
| 1278 | else |
| 1279 | fail "unquoted var word splits" "a b c" "$result" |
| 1280 | fi |
| 1281 | |
| 1282 | result=$("$FORTSH_BIN" -c 'X="a b c"; echo "$X"' 2>&1) |
| 1283 | if [ "$result" = "a b c" ]; then |
| 1284 | pass "quoted var preserves spaces" |
| 1285 | else |
| 1286 | fail "quoted var preserves spaces" "a b c" "$result" |
| 1287 | fi |
| 1288 | |
| 1289 | result=$("$FORTSH_BIN" -c 'echo ~ | grep -c "^/"' 2>&1) |
| 1290 | if [ "$result" = "1" ]; then |
| 1291 | pass "tilde expands to home" |
| 1292 | else |
| 1293 | fail "tilde expands to home" "1" "$result" |
| 1294 | fi |
| 1295 | |
| 1296 | # ===================================== |
| 1297 | section "397. SIGNAL NAMES" |
| 1298 | # ===================================== |
| 1299 | |
| 1300 | result=$("$FORTSH_BIN" -c 'kill -l | grep -c HUP' 2>&1) |
| 1301 | if [ "$result" -ge 1 ]; then |
| 1302 | pass "kill -l shows HUP" |
| 1303 | else |
| 1304 | fail "kill -l shows HUP" |
| 1305 | fi |
| 1306 | |
| 1307 | result=$("$FORTSH_BIN" -c 'kill -l | grep -c INT' 2>&1) |
| 1308 | if [ "$result" -ge 1 ]; then |
| 1309 | pass "kill -l shows INT" |
| 1310 | else |
| 1311 | fail "kill -l shows INT" |
| 1312 | fi |
| 1313 | |
| 1314 | result=$("$FORTSH_BIN" -c 'kill -l | grep -c TERM' 2>&1) |
| 1315 | if [ "$result" -ge 1 ]; then |
| 1316 | pass "kill -l shows TERM" |
| 1317 | else |
| 1318 | fail "kill -l shows TERM" |
| 1319 | fi |
| 1320 | |
| 1321 | # ===================================== |
| 1322 | section "398. EXEC WITH FD" |
| 1323 | # ===================================== |
| 1324 | |
| 1325 | result=$("$FORTSH_BIN" -c 'exec 3>&1; echo test >&3; exec 3>&-' 2>&1) |
| 1326 | if [ "$result" = "test" ]; then |
| 1327 | pass "exec fd redirect" |
| 1328 | else |
| 1329 | fail "exec fd redirect" "test" "$result" |
| 1330 | fi |
| 1331 | |
| 1332 | # ===================================== |
| 1333 | section "399. COMPLEX FUNCTIONS" |
| 1334 | # ===================================== |
| 1335 | |
| 1336 | result=$("$FORTSH_BIN" -c 'max() { [ $1 -gt $2 ] && echo $1 || echo $2; }; max 5 3' 2>&1) |
| 1337 | if [ "$result" = "5" ]; then |
| 1338 | pass "function max returns larger" |
| 1339 | else |
| 1340 | fail "function max returns larger" "5" "$result" |
| 1341 | fi |
| 1342 | |
| 1343 | result=$("$FORTSH_BIN" -c 'sum() { echo $(($1 + $2)); }; sum 10 20' 2>&1) |
| 1344 | if [ "$result" = "30" ]; then |
| 1345 | pass "function sum" |
| 1346 | else |
| 1347 | fail "function sum" "30" "$result" |
| 1348 | fi |
| 1349 | |
| 1350 | result=$("$FORTSH_BIN" -c 'greet() { echo "Hello, $1!"; }; greet World' 2>&1) |
| 1351 | if [ "$result" = "Hello, World!" ]; then |
| 1352 | pass "function with string interpolation" |
| 1353 | else |
| 1354 | fail "function with string interpolation" "Hello, World!" "$result" |
| 1355 | fi |
| 1356 | |
| 1357 | # ===================================== |
| 1358 | section "400. ADVANCED REDIRECTIONS" |
| 1359 | # ===================================== |
| 1360 | |
| 1361 | result=$("$FORTSH_BIN" -c 'echo out; echo err >&2' 2>&1) |
| 1362 | expected=$(printf "out\nerr") |
| 1363 | if [ "$result" = "$expected" ]; then |
| 1364 | pass "stdout and stderr" |
| 1365 | else |
| 1366 | fail "stdout and stderr" |
| 1367 | fi |
| 1368 | |
| 1369 | result=$("$FORTSH_BIN" -c '{ echo a; echo b >&2; } 2>&1 | wc -l' 2>&1) |
| 1370 | if [ "$result" -eq 2 ] 2>/dev/null; then |
| 1371 | pass "redirect stderr to stdout in pipeline" |
| 1372 | else |
| 1373 | fail "redirect stderr to stdout in pipeline" "2" "$result" |
| 1374 | fi |
| 1375 | |
| 1376 | # ===================================== |
| 1377 | # Summary |
| 1378 | # ===================================== |
| 1379 | printf "\n" |
| 1380 | printf "${BLUE}==========================================\n" |
| 1381 | printf "POSIX Special Features Test Summary\n" |
| 1382 | printf "==========================================${NC}\n" |
| 1383 | printf "Passed: ${GREEN}%d${NC}\n" "$PASSED" |
| 1384 | printf "Failed: ${RED}%d${NC}\n" "$FAILED" |
| 1385 | printf "Skipped: ${YELLOW}%d${NC}\n" "$SKIPPED" |
| 1386 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 1387 | |
| 1388 | if [ -n "$FAILED_TESTS_LIST" ]; then |
| 1389 | printf "\n${RED}Failed tests:${NC}\n" |
| 1390 | printf "%b" "$FAILED_TESTS_LIST" |
| 1391 | fi |
| 1392 | |
| 1393 | if [ "$FAILED" -gt 0 ]; then |
| 1394 | exit 1 |
| 1395 | fi |
| 1396 | exit 0 |