| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Compliance Extended Test Suite for rush |
| 4 | # ===================================== |
| 5 | # Comprehensive POSIX compliance testing based on IEEE Std 1003.1-2017 |
| 6 | # This extends the basic test suite with additional coverage |
| 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-extended]" |
| 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 | RUSH_BIN="${RUSH_BIN:-$SCRIPT_DIR/../target/release/rush}" |
| 28 | |
| 29 | # Check if fortsh exists |
| 30 | if [ ! -x "$RUSH_BIN" ]; then |
| 31 | printf "${RED}ERROR${NC}: rush binary not found at $RUSH_BIN\n" |
| 32 | printf "Please run 'make' first or set RUSH_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 " posix: %s\n" "$2" |
| 50 | fi |
| 51 | if [ -n "$3" ]; then |
| 52 | printf " rush: %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 | # Extract section number from header like "26. EXTENDED TEST" |
| 65 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 66 | TEST_NUM=0 |
| 67 | printf "\n" |
| 68 | printf "${BLUE}==========================================\n" |
| 69 | printf "%s\n" "$1" |
| 70 | printf "==========================================${NC}\n" |
| 71 | } |
| 72 | |
| 73 | # Helper function to run command in both shells and compare |
| 74 | compare_posix_output() { |
| 75 | test_name="$1" |
| 76 | command="$2" |
| 77 | posix_file="/tmp/posix_ext_$$_posix" |
| 78 | fortsh_file="/tmp/posix_ext_$$_fortsh" |
| 79 | |
| 80 | # Run in POSIX shell (sh) |
| 81 | bash -c "$command" > "$posix_file" 2>&1 || true |
| 82 | |
| 83 | # Run in rush |
| 84 | "$RUSH_BIN" -c "$command" > "$fortsh_file" 2>&1 || true |
| 85 | |
| 86 | # Compare outputs |
| 87 | if diff -q "$posix_file" "$fortsh_file" > /dev/null 2>&1; then |
| 88 | pass "$test_name" |
| 89 | else |
| 90 | fail "$test_name" "$(cat "$posix_file")" "$(cat "$fortsh_file")" |
| 91 | fi |
| 92 | |
| 93 | rm -f "$posix_file" "$fortsh_file" |
| 94 | } |
| 95 | |
| 96 | # Helper function to compare exit codes |
| 97 | compare_posix_exit_code() { |
| 98 | test_name="$1" |
| 99 | command="$2" |
| 100 | |
| 101 | bash -c "$command" > /dev/null 2>&1 |
| 102 | posix_exit=$? |
| 103 | |
| 104 | "$RUSH_BIN" -c "$command" > /dev/null 2>&1 |
| 105 | fortsh_exit=$? |
| 106 | |
| 107 | if [ "$posix_exit" -eq "$fortsh_exit" ]; then |
| 108 | pass "$test_name" |
| 109 | else |
| 110 | fail "$test_name" "exit=$posix_exit" "exit=$fortsh_exit" |
| 111 | fi |
| 112 | } |
| 113 | |
| 114 | # Cleanup |
| 115 | cleanup() { |
| 116 | rm -f /tmp/posix_ext_$$_* 2>/dev/null |
| 117 | rm -f /tmp/posix_test_* 2>/dev/null |
| 118 | rm -rf /tmp/posix_test_dir 2>/dev/null |
| 119 | } |
| 120 | trap cleanup EXIT INT TERM |
| 121 | |
| 122 | # Setup test environment |
| 123 | mkdir -p /tmp/posix_test_dir |
| 124 | cd /tmp/posix_test_dir || exit 1 |
| 125 | |
| 126 | section "26. EXTENDED TEST COMMAND - FILE TYPE TESTS" |
| 127 | |
| 128 | # Create test files with different properties |
| 129 | touch /tmp/posix_test_regular |
| 130 | chmod 644 /tmp/posix_test_regular |
| 131 | echo "data" > /tmp/posix_test_nonempty |
| 132 | mkdir -p /tmp/posix_test_directory |
| 133 | mkfifo /tmp/posix_test_fifo 2>/dev/null || skip "mkfifo" "not available" |
| 134 | ln -sf /tmp/posix_test_regular /tmp/posix_test_symlink 2>/dev/null |
| 135 | |
| 136 | compare_posix_exit_code "test -e exists" "test -e /tmp/posix_test_regular" |
| 137 | compare_posix_exit_code "test -e nonexistent" "! test -e /tmp/nonexistent_xyz_$$" |
| 138 | compare_posix_exit_code "test -f regular file" "test -f /tmp/posix_test_regular" |
| 139 | compare_posix_exit_code "test -d directory" "test -d /tmp/posix_test_directory" |
| 140 | compare_posix_exit_code "test -s non-empty" "test -s /tmp/posix_test_nonempty" |
| 141 | compare_posix_exit_code "test -s empty" "! test -s /tmp/posix_test_regular" |
| 142 | |
| 143 | # Symlink tests (if supported) |
| 144 | if [ -L /tmp/posix_test_symlink ]; then |
| 145 | compare_posix_exit_code "test -L symlink" "test -L /tmp/posix_test_symlink" |
| 146 | compare_posix_exit_code "test -h symlink" "test -h /tmp/posix_test_symlink" |
| 147 | fi |
| 148 | |
| 149 | # FIFO tests (if created successfully) |
| 150 | if [ -p /tmp/posix_test_fifo ]; then |
| 151 | compare_posix_exit_code "test -p fifo" "test -p /tmp/posix_test_fifo" |
| 152 | fi |
| 153 | |
| 154 | section "27. EXTENDED TEST COMMAND - FILE PERMISSION TESTS" |
| 155 | |
| 156 | # Create files with specific permissions |
| 157 | touch /tmp/posix_test_readable |
| 158 | chmod 644 /tmp/posix_test_readable |
| 159 | touch /tmp/posix_test_writable |
| 160 | chmod 644 /tmp/posix_test_writable |
| 161 | touch /tmp/posix_test_executable |
| 162 | chmod 755 /tmp/posix_test_executable |
| 163 | |
| 164 | compare_posix_exit_code "test -r readable" "test -r /tmp/posix_test_readable" |
| 165 | compare_posix_exit_code "test -w writable" "test -w /tmp/posix_test_writable" |
| 166 | compare_posix_exit_code "test -x executable" "test -x /tmp/posix_test_executable" |
| 167 | compare_posix_exit_code "test ! -x nonexec" "! test -x /tmp/posix_test_readable" |
| 168 | |
| 169 | section "28. EXTENDED TEST COMMAND - FILE COMPARISON" |
| 170 | |
| 171 | # Create files for comparison |
| 172 | echo "old" > /tmp/posix_test_old |
| 173 | sleep 1 |
| 174 | echo "new" > /tmp/posix_test_new |
| 175 | ln -f /tmp/posix_test_old /tmp/posix_test_hardlink 2>/dev/null |
| 176 | |
| 177 | compare_posix_exit_code "test -nt newer than" "test /tmp/posix_test_new -nt /tmp/posix_test_old" |
| 178 | compare_posix_exit_code "test -ot older than" "test /tmp/posix_test_old -ot /tmp/posix_test_new" |
| 179 | |
| 180 | # Hard link test (if supported) |
| 181 | if [ -f /tmp/posix_test_hardlink ]; then |
| 182 | compare_posix_exit_code "test -ef same file" "test /tmp/posix_test_old -ef /tmp/posix_test_hardlink" |
| 183 | fi |
| 184 | |
| 185 | section "29. EXTENDED TEST COMMAND - STRING TESTS" |
| 186 | |
| 187 | compare_posix_exit_code "test -n nonempty string" "test -n 'hello'" |
| 188 | compare_posix_exit_code "test -z empty string" "test -z ''" |
| 189 | compare_posix_exit_code "test string = equal" "test 'abc' = 'abc'" |
| 190 | compare_posix_exit_code "test string != not equal" "test 'abc' != 'xyz'" |
| 191 | compare_posix_exit_code "test unary string" "test 'hello'" |
| 192 | compare_posix_exit_code "test ! negation" "! test -z 'hello'" |
| 193 | |
| 194 | section "30. EXTENDED TEST COMMAND - INTEGER COMPARISON" |
| 195 | |
| 196 | compare_posix_exit_code "test -eq equal" "test 42 -eq 42" |
| 197 | compare_posix_exit_code "test -ne not equal" "test 42 -ne 13" |
| 198 | compare_posix_exit_code "test -gt greater" "test 10 -gt 5" |
| 199 | compare_posix_exit_code "test -ge greater or equal" "test 10 -ge 10" |
| 200 | compare_posix_exit_code "test -lt less" "test 5 -lt 10" |
| 201 | compare_posix_exit_code "test -le less or equal" "test 5 -le 5" |
| 202 | compare_posix_exit_code "test negative numbers" "test -5 -lt 0" |
| 203 | |
| 204 | section "31. EXTENDED TEST COMMAND - LOGICAL OPERATORS" |
| 205 | |
| 206 | compare_posix_exit_code "test -a and" "test 5 -gt 3 -a 10 -gt 8" |
| 207 | compare_posix_exit_code "test -o or" "test 5 -gt 10 -o 10 -gt 8" |
| 208 | compare_posix_exit_code "test ! negation" "! test 5 -gt 10" |
| 209 | compare_posix_exit_code "test ( ) grouping" "test \( 5 -gt 3 \) -a \( 10 -gt 8 \)" |
| 210 | |
| 211 | section "32. EXTENDED PARAMETER EXPANSION - DEFAULT VALUES" |
| 212 | |
| 213 | compare_posix_output "use default unset" 'unset VAR; echo "${VAR-default}"' |
| 214 | compare_posix_output "use default null" 'VAR=; echo "${VAR-default}"' |
| 215 | compare_posix_output "use default null colon" 'VAR=; echo "${VAR:-default}"' |
| 216 | compare_posix_output "use default set" 'VAR=value; echo "${VAR-default}"' |
| 217 | compare_posix_output "assign default unset" 'unset VAR; echo "${VAR=assigned}"; echo $VAR' |
| 218 | compare_posix_output "assign default null colon" 'VAR=; echo "${VAR:=assigned}"; echo $VAR' |
| 219 | |
| 220 | section "33. EXTENDED PARAMETER EXPANSION - ERROR IF UNSET" |
| 221 | |
| 222 | compare_posix_exit_code "error if unset" 'unset VAR; echo "${VAR?error}" 2>/dev/null' |
| 223 | compare_posix_exit_code "error if null colon" 'VAR=; echo "${VAR:?error}" 2>/dev/null' |
| 224 | compare_posix_output "no error if set" 'VAR=value; echo "${VAR?error}"' |
| 225 | |
| 226 | section "34. EXTENDED PARAMETER EXPANSION - ALTERNATIVE VALUE" |
| 227 | |
| 228 | compare_posix_output "alt unset" 'unset VAR; echo "${VAR+alternative}"' |
| 229 | compare_posix_output "alt null" 'VAR=; echo "${VAR+alternative}"' |
| 230 | compare_posix_output "alt null colon" 'VAR=; echo "${VAR:+alternative}"' |
| 231 | compare_posix_output "alt set" 'VAR=value; echo "${VAR+alternative}"' |
| 232 | compare_posix_output "alt set colon" 'VAR=value; echo "${VAR:+alternative}"' |
| 233 | |
| 234 | section "35. EXTENDED PARAMETER EXPANSION - STRING LENGTH" |
| 235 | |
| 236 | compare_posix_output "length empty" 'VAR=; echo "${#VAR}"' |
| 237 | compare_posix_output "length short" 'VAR=hi; echo "${#VAR}"' |
| 238 | compare_posix_output "length long" 'VAR="hello world"; echo "${#VAR}"' |
| 239 | compare_posix_output "length special chars" 'VAR="a b c"; echo "${#VAR}"' |
| 240 | |
| 241 | section "36. EXTENDED PARAMETER EXPANSION - PATTERN REMOVAL" |
| 242 | |
| 243 | # Prefix removal |
| 244 | compare_posix_output "prefix # simple" 'VAR=hello; echo "${VAR#hel}"' |
| 245 | compare_posix_output "prefix # nomatch" 'VAR=hello; echo "${VAR#xyz}"' |
| 246 | compare_posix_output "prefix # glob" 'VAR=foo.bar.baz; echo "${VAR#*.}"' |
| 247 | compare_posix_output "prefix ## glob" 'VAR=foo.bar.baz; echo "${VAR##*.}"' |
| 248 | compare_posix_output "prefix # star" 'VAR=/usr/local/bin; echo "${VAR#*/}"' |
| 249 | compare_posix_output "prefix ## star" 'VAR=/usr/local/bin; echo "${VAR##*/}"' |
| 250 | |
| 251 | # Suffix removal |
| 252 | compare_posix_output "suffix % simple" 'VAR=hello; echo "${VAR%lo}"' |
| 253 | compare_posix_output "suffix % nomatch" 'VAR=hello; echo "${VAR%xyz}"' |
| 254 | compare_posix_output "suffix % glob" 'VAR=foo.bar.baz; echo "${VAR%.*}"' |
| 255 | compare_posix_output "suffix %% glob" 'VAR=foo.bar.baz; echo "${VAR%%.*}"' |
| 256 | compare_posix_output "suffix % extension" 'VAR=file.tar.gz; echo "${VAR%.gz}"' |
| 257 | compare_posix_output "suffix %% extension" 'VAR=file.tar.gz; echo "${VAR%%.*}"' |
| 258 | |
| 259 | section "37. ARITHMETIC EXPANSION - BASIC OPERATIONS" |
| 260 | |
| 261 | compare_posix_output "arith addition" 'echo $((5 + 3))' |
| 262 | compare_posix_output "arith subtraction" 'echo $((10 - 4))' |
| 263 | compare_posix_output "arith multiplication" 'echo $((6 * 7))' |
| 264 | compare_posix_output "arith division" 'echo $((20 / 4))' |
| 265 | compare_posix_output "arith modulo" 'echo $((17 % 5))' |
| 266 | compare_posix_output "arith negative" 'echo $((-5 + 10))' |
| 267 | compare_posix_output "arith zero" 'echo $((0 + 0))' |
| 268 | |
| 269 | section "38. ARITHMETIC EXPANSION - PRECEDENCE" |
| 270 | |
| 271 | compare_posix_output "arith precedence mult" 'echo $((2 + 3 * 4))' |
| 272 | compare_posix_output "arith precedence paren" 'echo $(((2 + 3) * 4))' |
| 273 | compare_posix_output "arith precedence div" 'echo $((10 - 8 / 2))' |
| 274 | compare_posix_output "arith precedence complex" 'echo $((2 * 3 + 4 * 5))' |
| 275 | |
| 276 | section "39. ARITHMETIC EXPANSION - VARIABLES" |
| 277 | |
| 278 | compare_posix_output "arith var" 'X=5; echo $((X + 3))' |
| 279 | compare_posix_output "arith var no dollar" 'X=10; Y=20; echo $((X + Y))' |
| 280 | compare_posix_output "arith var assign" 'X=5; Y=$((X * 2)); echo $Y' |
| 281 | compare_posix_output "arith var complex" 'A=3; B=4; echo $((A * A + B * B))' |
| 282 | |
| 283 | section "40. ARITHMETIC EXPANSION - COMPARISON" |
| 284 | |
| 285 | compare_posix_output "arith compare eq true" 'echo $((5 == 5))' |
| 286 | compare_posix_output "arith compare eq false" 'echo $((5 == 3))' |
| 287 | compare_posix_output "arith compare ne true" 'echo $((5 != 3))' |
| 288 | compare_posix_output "arith compare ne false" 'echo $((5 != 5))' |
| 289 | compare_posix_output "arith compare lt" 'echo $((3 < 5))' |
| 290 | compare_posix_output "arith compare le" 'echo $((5 <= 5))' |
| 291 | compare_posix_output "arith compare gt" 'echo $((7 > 5))' |
| 292 | compare_posix_output "arith compare ge" 'echo $((5 >= 5))' |
| 293 | |
| 294 | section "41. ARITHMETIC EXPANSION - LOGICAL" |
| 295 | |
| 296 | compare_posix_output "arith logical and true" 'echo $((1 && 1))' |
| 297 | compare_posix_output "arith logical and false" 'echo $((1 && 0))' |
| 298 | compare_posix_output "arith logical or true" 'echo $((0 || 1))' |
| 299 | compare_posix_output "arith logical or false" 'echo $((0 || 0))' |
| 300 | compare_posix_output "arith logical not true" 'echo $((! 0))' |
| 301 | compare_posix_output "arith logical not false" 'echo $((! 1))' |
| 302 | |
| 303 | section "42. SPECIAL PARAMETERS" |
| 304 | |
| 305 | compare_posix_output "\$\$ process id type" 'echo $$ | grep -c "^[0-9][0-9]*$"' |
| 306 | compare_posix_output "\$- shell flags type" 'echo $- | grep -c "^[a-z]*$"' |
| 307 | compare_posix_output "\$# arg count" 'set -- a b c; echo $#' |
| 308 | compare_posix_output "\$1 first arg" 'set -- first second; echo $1' |
| 309 | compare_posix_output "\$2 second arg" 'set -- first second; echo $2' |
| 310 | compare_posix_output "\$9 ninth arg" 'set -- a b c d e f g h i j; echo $9' |
| 311 | compare_posix_output "\$* all args" 'set -- a b c; echo $*' |
| 312 | compare_posix_output "\$@ all args" 'set -- a b c; echo $@' |
| 313 | |
| 314 | section "43. ADDITIONAL POSIX BUILTINS - CD/PWD" |
| 315 | |
| 316 | compare_posix_output "cd to /tmp" 'cd /tmp && pwd' |
| 317 | compare_posix_output "cd relative" 'cd /tmp && cd .. && pwd | grep -c "^/"' |
| 318 | compare_posix_output "cd $HOME" 'cd && pwd | grep -c "^/"' |
| 319 | |
| 320 | section "44. ADDITIONAL POSIX BUILTINS - UNSET" |
| 321 | |
| 322 | compare_posix_output "unset variable" 'VAR=test; unset VAR; echo ${VAR:-unset}' |
| 323 | compare_posix_output "unset nonexistent" 'unset NONEXISTENT_VAR_XYZ; echo ok' |
| 324 | |
| 325 | section "45. ADDITIONAL POSIX BUILTINS - EVAL" |
| 326 | |
| 327 | compare_posix_output "eval simple" 'CMD="echo hello"; eval $CMD' |
| 328 | compare_posix_output "eval with var" 'X=5; CMD="echo \$X"; eval $CMD' |
| 329 | compare_posix_output "eval complex" 'A=echo; B=test; eval $A $B' |
| 330 | |
| 331 | section "46. ADDITIONAL POSIX BUILTINS - COLON" |
| 332 | |
| 333 | compare_posix_output ": null command" ': ; echo ok' |
| 334 | compare_posix_output ": with args" ': this is ignored; echo ok' |
| 335 | compare_posix_exit_code ": exit status" ':' |
| 336 | |
| 337 | section "47. TILDE EXPANSION" |
| 338 | |
| 339 | compare_posix_output "tilde home" 'echo ~ | grep -c "^/"' |
| 340 | compare_posix_output "tilde in path" 'echo ~/test | grep -c "^/"' |
| 341 | |
| 342 | section "48. FIELD SPLITTING - ADVANCED" |
| 343 | |
| 344 | compare_posix_output "IFS multiple fields" 'IFS=:; VAR="a:b:c:d"; set -- $VAR; echo $# $1 $4' |
| 345 | compare_posix_output "IFS whitespace" 'IFS=" "; VAR="a b c"; set -- $VAR; echo $#' |
| 346 | compare_posix_output "IFS comma" 'IFS=,; VAR="x,y,z"; set -- $VAR; echo $2' |
| 347 | |
| 348 | section "49. BACKGROUND JOBS" |
| 349 | |
| 350 | compare_posix_exit_code "background process" 'sleep 0.1 & wait' |
| 351 | compare_posix_output "background exit" '(exit 0) & wait $!; echo $?' |
| 352 | |
| 353 | section "50. COMMAND GROUPING" |
| 354 | |
| 355 | compare_posix_output "subshell isolation" 'X=1; (X=2; echo $X); echo $X' |
| 356 | compare_posix_output "brace grouping" 'X=1; { X=2; echo $X; }; echo $X' |
| 357 | compare_posix_output "subshell exit" '(exit 5); echo $?' |
| 358 | |
| 359 | section "51. GLOB PATTERN MATCHING" |
| 360 | |
| 361 | # Create temp dir for glob tests |
| 362 | GLOB_DIR="/tmp/posix_glob_test_$$" |
| 363 | mkdir -p "$GLOB_DIR" |
| 364 | touch "$GLOB_DIR/file1.txt" "$GLOB_DIR/file2.txt" "$GLOB_DIR/file3.log" |
| 365 | touch "$GLOB_DIR/abc" "$GLOB_DIR/abd" "$GLOB_DIR/acd" |
| 366 | |
| 367 | compare_posix_output "star matches multiple" 'cd '"$GLOB_DIR"' && echo file*.txt | tr " " "\n" | wc -l' |
| 368 | compare_posix_output "question mark single char" 'cd '"$GLOB_DIR"' && echo ab? | tr " " "\n" | wc -l' |
| 369 | compare_posix_output "bracket range" 'cd '"$GLOB_DIR"' && echo a[bc]d | tr " " "\n" | wc -l' |
| 370 | compare_posix_output "no match literal" 'cd '"$GLOB_DIR"' && echo zzz*.xyz 2>/dev/null || echo "zzz*.xyz"' |
| 371 | |
| 372 | rm -rf "$GLOB_DIR" |
| 373 | |
| 374 | section "52. DOTFILE HANDLING" |
| 375 | |
| 376 | DOT_DIR="/tmp/posix_dot_test_$$" |
| 377 | mkdir -p "$DOT_DIR" |
| 378 | touch "$DOT_DIR/.hidden" "$DOT_DIR/visible" |
| 379 | |
| 380 | compare_posix_output "star no dotfiles" 'cd '"$DOT_DIR"' && echo * | grep -c hidden || echo 0' |
| 381 | compare_posix_output "explicit dot matches" 'cd '"$DOT_DIR"' && ls -d .* 2>/dev/null | grep -c hidden' |
| 382 | |
| 383 | rm -rf "$DOT_DIR" |
| 384 | |
| 385 | section "53. QUOTING IN GLOB" |
| 386 | |
| 387 | QUOTE_DIR="/tmp/posix_quote_glob_$$" |
| 388 | mkdir -p "$QUOTE_DIR" |
| 389 | touch "$QUOTE_DIR/star" |
| 390 | |
| 391 | compare_posix_output "quoted star literal" 'cd '"$QUOTE_DIR"' && echo "*" | grep -c "\\*"' |
| 392 | compare_posix_output "single quote prevents glob" "cd '$QUOTE_DIR' && echo '*' | grep -c '\\*'" |
| 393 | |
| 394 | rm -rf "$QUOTE_DIR" |
| 395 | |
| 396 | section "54. EMPTY AND NULL EXPANSION" |
| 397 | |
| 398 | compare_posix_output "unset var empty" 'unset X; echo "[$X]"' |
| 399 | compare_posix_output "null var empty" 'X=""; echo "[$X]"' |
| 400 | compare_posix_output "unset in arith" 'unset X; echo $((X + 1))' |
| 401 | |
| 402 | section "55. SPECIAL VARIABLES READONLY" |
| 403 | |
| 404 | compare_posix_output "cannot assign to ?" '(eval "?=5" 2>/dev/null); echo ok' |
| 405 | compare_posix_output "cannot assign to $" '(eval "\$=5" 2>/dev/null); echo ok' |
| 406 | |
| 407 | section "56. COMPLEX COMMAND LISTS" |
| 408 | |
| 409 | compare_posix_output "and-or chain" 'true && echo yes || echo no' |
| 410 | compare_posix_output "or-and chain" 'false || echo fallback && echo then' |
| 411 | compare_posix_output "semicolon list" 'echo a; echo b; echo c' |
| 412 | compare_posix_output "mixed operators" 'true && true && echo all || echo none' |
| 413 | |
| 414 | section "57. NESTED SUBSHELLS" |
| 415 | |
| 416 | compare_posix_output "double nesting" '( ( echo deep ) )' |
| 417 | compare_posix_output "triple nesting" '( ( ( echo deeper ) ) )' |
| 418 | compare_posix_output "nested with vars" 'X=1; ( X=2; ( X=3; echo $X ) ); echo $X' |
| 419 | |
| 420 | section "58. BRACE GROUP EDGE CASES" |
| 421 | |
| 422 | compare_posix_output "brace needs space" '{ echo test; }' |
| 423 | compare_posix_output "brace with semicolon" '{ echo a; echo b; }' |
| 424 | compare_posix_output "brace preserves vars" 'X=1; { X=2; }; echo $X' |
| 425 | |
| 426 | section "59. ARITHMETIC BASE LITERALS" |
| 427 | |
| 428 | compare_posix_output "octal 010" 'echo $((010))' |
| 429 | compare_posix_output "hex 0x10" 'echo $((0x10))' |
| 430 | compare_posix_output "hex 0xFF" 'echo $((0xFF))' |
| 431 | compare_posix_output "mixed bases" 'echo $((010 + 0x10 + 10))' |
| 432 | |
| 433 | section "60. STRING LENGTH EDGE CASES" |
| 434 | |
| 435 | compare_posix_output "length empty" 'X=""; echo ${#X}' |
| 436 | compare_posix_output "length one" 'X="a"; echo ${#X}' |
| 437 | compare_posix_output "length with spaces" 'X="a b c"; echo ${#X}' |
| 438 | compare_posix_output "length special chars" 'X="$@#"; echo ${#X}' |
| 439 | |
| 440 | section "61. DEFAULT VALUE EDGE CASES" |
| 441 | |
| 442 | compare_posix_output "default unset" 'unset X; echo ${X:-default}' |
| 443 | compare_posix_output "default null" 'X=""; echo ${X:-default}' |
| 444 | compare_posix_output "default set" 'X="value"; echo ${X:-default}' |
| 445 | compare_posix_output "no-colon unset" 'unset X; echo ${X-default}' |
| 446 | compare_posix_output "no-colon null" 'X=""; echo ${X-default}' |
| 447 | |
| 448 | section "62. PATTERN REMOVAL EDGE CASES" |
| 449 | |
| 450 | compare_posix_output "prefix no match" 'X="hello"; echo ${X#xyz}' |
| 451 | compare_posix_output "suffix no match" 'X="hello"; echo ${X%xyz}' |
| 452 | compare_posix_output "prefix full match" 'X="aaa"; echo ${X##a*}' |
| 453 | compare_posix_output "suffix full match" 'X="aaa"; echo ${X%%*a}' |
| 454 | |
| 455 | section "63. POSITIONAL PARAMS EDGE CASES" |
| 456 | |
| 457 | compare_posix_output "set clears all" 'set -- a b c; set --; echo $#' |
| 458 | compare_posix_output "set replaces" 'set -- a b; set -- x y z; echo $#' |
| 459 | compare_posix_output "shift all" 'set -- a b c; shift 3; echo $#' |
| 460 | |
| 461 | section "64. EXIT STATUS PROPAGATION" |
| 462 | |
| 463 | compare_posix_output "pipeline exit" 'true | true | false; echo $?' |
| 464 | compare_posix_output "subshell exit" '(exit 42); echo $?' |
| 465 | compare_posix_output "brace exit" '{ exit 7; }; echo $?' |
| 466 | compare_posix_output "command sub exit" 'X=$(exit 5); echo $?' |
| 467 | |
| 468 | section "65. WORD SPLITTING IFS VARIANTS" |
| 469 | |
| 470 | compare_posix_output "IFS empty no split" 'IFS=""; X="a b c"; set -- $X; echo $#' |
| 471 | compare_posix_output "IFS colon" 'IFS=":"; X="a:b:c"; set -- $X; echo $#' |
| 472 | compare_posix_output "IFS multiple" 'IFS=":;"; X="a:b;c"; set -- $X; echo $#' |
| 473 | compare_posix_output "IFS whitespace" 'IFS=" "; X="a b c"; set -- $X; echo $#' |
| 474 | compare_posix_output "IFS default" 'X="a b c"; set -- $X; echo $#' |
| 475 | |
| 476 | section "66. PATHNAME EXPANSION DISABLING" |
| 477 | |
| 478 | compare_posix_output "noglob off" 'touch /tmp/glob_test_a.x /tmp/glob_test_b.x 2>/dev/null; ls /tmp/glob_test_*.x 2>/dev/null | wc -l' |
| 479 | compare_posix_output "noglob on" 'set -f; echo /tmp/glob_test_*.x; set +f' |
| 480 | |
| 481 | section "67. TILDE EXPANSION CONTEXTS" |
| 482 | |
| 483 | compare_posix_output "tilde alone" 'echo ~ | grep -c "^/"' |
| 484 | compare_posix_output "tilde in var" 'X=~; echo $X | grep -c "^/"' |
| 485 | compare_posix_output "tilde quoted" 'echo "~" | grep -c "~"' |
| 486 | compare_posix_output "tilde in path" 'echo ~/. | grep -c "^/"' |
| 487 | |
| 488 | section "68. ASSIGNMENT CONTEXTS" |
| 489 | |
| 490 | compare_posix_output "simple assign" 'X=hello; echo $X' |
| 491 | compare_posix_output "assign with cmd sub" 'X=$(echo test); echo $X' |
| 492 | compare_posix_output "assign with arith" 'X=$((5+3)); echo $X' |
| 493 | compare_posix_output "multiple assign" 'X=1 Y=2 Z=3; echo $X $Y $Z' |
| 494 | compare_posix_output "assign before cmd" 'X=val sh -c "echo \$X"' |
| 495 | |
| 496 | section "69. SPECIAL CHARACTER HANDLING" |
| 497 | |
| 498 | compare_posix_output "escaped newline" 'echo a\ |
| 499 | b' |
| 500 | compare_posix_output "escaped dollar" 'echo \$VAR' |
| 501 | compare_posix_output "escaped backslash" 'echo \\\\' |
| 502 | compare_posix_output "escaped quote" 'echo \"quoted\"' |
| 503 | |
| 504 | section "70. WHILE LOOP VARIATIONS" |
| 505 | |
| 506 | compare_posix_output "while with pipe" 'i=0; while [ $i -lt 3 ]; do echo $i; i=$((i+1)); done | wc -l' |
| 507 | compare_posix_output "while false" 'while false; do echo never; done; echo done' |
| 508 | compare_posix_output "while break" 'i=0; while true; do i=$((i+1)); [ $i -ge 3 ] && break; done; echo $i' |
| 509 | compare_posix_output "while continue" 'i=0; while [ $i -lt 5 ]; do i=$((i+1)); [ $i -eq 3 ] && continue; echo $i; done' |
| 510 | |
| 511 | section "71. UNTIL LOOP VARIATIONS" |
| 512 | |
| 513 | compare_posix_output "until basic" 'i=0; until [ $i -ge 3 ]; do echo $i; i=$((i+1)); done' |
| 514 | compare_posix_output "until true" 'until true; do echo never; done; echo done' |
| 515 | compare_posix_output "until with break" 'i=0; until false; do i=$((i+1)); [ $i -ge 3 ] && break; done; echo $i' |
| 516 | |
| 517 | section "72. FOR LOOP WORD LIST" |
| 518 | |
| 519 | compare_posix_output "for with glob" 'touch /tmp/for_test_1.z /tmp/for_test_2.z 2>/dev/null; for f in /tmp/for_test_*.z; do echo found; done | wc -l' |
| 520 | compare_posix_output "for empty" 'for x in; do echo never; done; echo done' |
| 521 | compare_posix_output "for with expansion" 'X="a b c"; for i in $X; do echo $i; done | wc -l' |
| 522 | compare_posix_output "for quoted" 'for i in "a b" "c d"; do echo "[$i]"; done' |
| 523 | |
| 524 | section "73. FUNCTION ARGUMENT HANDLING" |
| 525 | |
| 526 | compare_posix_output "func args count" 'f() { echo $#; }; f a b c' |
| 527 | compare_posix_output "func args values" 'f() { echo $1 $2 $3; }; f x y z' |
| 528 | compare_posix_output "func args shift" 'f() { shift; echo $1; }; f a b c' |
| 529 | compare_posix_output "func args all" 'f() { echo $@; }; f 1 2 3' |
| 530 | compare_posix_output "func nested call" 'a() { b; }; b() { echo hello; }; a' |
| 531 | |
| 532 | section "74. RETURN AND EXIT" |
| 533 | |
| 534 | compare_posix_output "return value" 'f() { return 42; }; f; echo $?' |
| 535 | compare_posix_output "return default" 'f() { true; return; }; f; echo $?' |
| 536 | compare_posix_output "exit in subshell" '(exit 5); echo $?' |
| 537 | compare_posix_output "exit from func" 'f() { exit 7; }; f; echo never' |
| 538 | |
| 539 | section "75. EVAL COMPLEX" |
| 540 | |
| 541 | compare_posix_output "eval variable expansion" 'X=VAR; VAR=value; eval echo \$$X' |
| 542 | compare_posix_output "eval multiple" 'eval "A=1; B=2"; echo $A $B' |
| 543 | compare_posix_output "eval with special" 'eval "echo hello world"' |
| 544 | compare_posix_output "eval nested" 'eval eval echo test' |
| 545 | |
| 546 | section "76. EXEC REDIRECTIONS" |
| 547 | |
| 548 | compare_posix_output "exec redirect stdout" 'exec >/tmp/exec_test_$$; echo test; exec >&-; cat /tmp/exec_test_$$; rm /tmp/exec_test_$$' |
| 549 | |
| 550 | section "77. TRAP IN SUBSHELL" |
| 551 | |
| 552 | compare_posix_output "trap inherited" '(trap "echo trapped" EXIT; exit 0) 2>/dev/null' |
| 553 | compare_posix_output "trap reset in subshell" 'trap "echo outer" EXIT; (trap - EXIT; echo inner) 2>/dev/null; trap - EXIT' |
| 554 | |
| 555 | section "78. COLON COMMAND" |
| 556 | |
| 557 | compare_posix_output "colon returns 0" ':; echo $?' |
| 558 | compare_posix_output "colon with args" ': arg1 arg2 arg3; echo $?' |
| 559 | compare_posix_output "colon with expansion" 'X=test; : $X; echo $?' |
| 560 | compare_posix_output "colon in if" 'if :; then echo yes; fi' |
| 561 | |
| 562 | section "79. DOT SOURCE" |
| 563 | |
| 564 | compare_posix_output "dot sources" 'echo "X=sourced" > /tmp/dot_test_$$; . /tmp/dot_test_$$; echo $X; rm /tmp/dot_test_$$' |
| 565 | compare_posix_output "dot with args" 'echo "echo \$1" > /tmp/dot_arg_$$; . /tmp/dot_arg_$$ hello; rm /tmp/dot_arg_$$' |
| 566 | |
| 567 | section "80. UNSET BEHAVIOR" |
| 568 | |
| 569 | compare_posix_output "unset variable" 'X=test; unset X; echo ${X:-empty}' |
| 570 | compare_posix_output "unset function" 'f() { echo hi; }; unset -f f; f 2>/dev/null || echo gone' |
| 571 | compare_posix_output "unset nonexistent" 'unset NONEXISTENT_VAR_XYZ; echo $?' |
| 572 | |
| 573 | section "81. POSIX STRING OPERATIONS" |
| 574 | |
| 575 | compare_posix_output "string concat" 'A=hello; B=world; echo $A$B' |
| 576 | compare_posix_output "string in quotes" 'A="hello world"; echo "$A"' |
| 577 | compare_posix_output "string length indirect" 'A=test; echo ${#A}' |
| 578 | compare_posix_output "empty string" 'A=""; echo "[$A]"' |
| 579 | compare_posix_output "string with newline" 'A="line1 |
| 580 | line2"; echo "$A" | wc -l' |
| 581 | |
| 582 | section "82. POSIX NUMERIC OPERATIONS" |
| 583 | |
| 584 | compare_posix_output "add subtract" 'echo $((10 - 3 + 5))' |
| 585 | compare_posix_output "multiply divide" 'echo $((20 / 4 * 2))' |
| 586 | compare_posix_output "parentheses" 'echo $(((2 + 3) * (4 + 1)))' |
| 587 | compare_posix_output "comparison chain" 'echo $((5 > 3 && 3 > 1))' |
| 588 | compare_posix_output "ternary simulation" 'X=5; [ $X -gt 3 ] && echo big || echo small' |
| 589 | |
| 590 | section "83. POSIX ARRAY SIMULATION" |
| 591 | |
| 592 | compare_posix_output "positional as array" 'set -- a b c d e; echo $3' |
| 593 | compare_posix_output "array length" 'set -- a b c d e; echo $#' |
| 594 | compare_posix_output "array slice" 'set -- a b c d e; shift 2; echo $1' |
| 595 | compare_posix_output "array all" 'set -- a b c; echo "$@"' |
| 596 | compare_posix_output "array iterate" 'set -- a b c; for x in "$@"; do echo $x; done' |
| 597 | |
| 598 | section "84. POSIX PATTERN MATCHING" |
| 599 | |
| 600 | compare_posix_output "glob question" 'touch /tmp/pat_a /tmp/pat_b 2>/dev/null; ls /tmp/pat_? 2>/dev/null | wc -l; rm -f /tmp/pat_a /tmp/pat_b' |
| 601 | compare_posix_output "glob star" 'touch /tmp/patstar_1 /tmp/patstar_2 2>/dev/null; ls /tmp/patstar_* 2>/dev/null | wc -l; rm -f /tmp/patstar_*' |
| 602 | compare_posix_output "glob bracket" 'touch /tmp/patbr_a /tmp/patbr_b 2>/dev/null; ls /tmp/patbr_[ab] 2>/dev/null | wc -l; rm -f /tmp/patbr_*' |
| 603 | compare_posix_output "case pattern star" 'x=hello; case $x in h*) echo yes;; esac' |
| 604 | compare_posix_output "case pattern question" 'x=ab; case $x in ??) echo two;; esac' |
| 605 | |
| 606 | section "85. POSIX ENVIRONMENT" |
| 607 | |
| 608 | compare_posix_output "HOME exists" 'echo $HOME | grep -c "^/"' |
| 609 | compare_posix_output "PATH exists" 'echo $PATH | grep -c ":"' |
| 610 | compare_posix_output "PWD exists" 'echo $PWD | grep -c "^/"' |
| 611 | compare_posix_output "export visible" 'export X=val; sh -c "echo \$X"' |
| 612 | compare_posix_output "env assignment" 'X=test sh -c "echo \$X"' |
| 613 | |
| 614 | section "86. POSIX SPECIAL EXPANSIONS" |
| 615 | |
| 616 | compare_posix_output "dollar dollar" 'echo $$ | grep -E "^[0-9]+$" | wc -l' |
| 617 | compare_posix_output "dollar question" 'true; echo $?' |
| 618 | compare_posix_output "dollar bang" 'sleep 0.01 & echo $! | grep -E "^[0-9]+$" | wc -l; wait' |
| 619 | compare_posix_output "dollar hash" 'set -- a b c; echo $#' |
| 620 | compare_posix_output "dollar zero" 'echo $0 | wc -c | xargs test 0 -lt && echo yes' |
| 621 | |
| 622 | section "87. POSIX ERROR HANDLING" |
| 623 | |
| 624 | compare_posix_output "command not found" 'nonexistent_cmd_xyz 2>/dev/null; echo $?' |
| 625 | compare_posix_output "file not found" 'cat /nonexistent_file_xyz 2>/dev/null; echo $?' |
| 626 | compare_posix_output "permission denied sim" 'test -r /etc/shadow 2>/dev/null; echo done' |
| 627 | compare_posix_output "syntax error handled" 'eval "if" 2>/dev/null; echo recovered' |
| 628 | |
| 629 | section "88. POSIX CONDITIONAL CHAINS" |
| 630 | |
| 631 | compare_posix_output "if and" 'if true && true; then echo yes; fi' |
| 632 | compare_posix_output "if or" 'if false || true; then echo yes; fi' |
| 633 | compare_posix_output "if not" 'if ! false; then echo yes; fi' |
| 634 | compare_posix_output "complex condition" 'X=5; if [ $X -gt 3 ] && [ $X -lt 10 ]; then echo range; fi' |
| 635 | compare_posix_output "elif chain long" 'X=4; if [ $X -eq 1 ]; then echo 1; elif [ $X -eq 2 ]; then echo 2; elif [ $X -eq 3 ]; then echo 3; elif [ $X -eq 4 ]; then echo 4; fi' |
| 636 | |
| 637 | section "89. POSIX INPUT PROCESSING" |
| 638 | |
| 639 | compare_posix_output "read line" 'echo "hello" | { read x; echo $x; }' |
| 640 | compare_posix_output "read words" 'echo "a b c" | { read x y z; echo $y; }' |
| 641 | compare_posix_output "read with IFS" 'echo "a:b:c" | { IFS=: read x y z; echo $y; }' |
| 642 | compare_posix_output "cat file" 'echo "test" > /tmp/read_test_$$; cat /tmp/read_test_$$; rm /tmp/read_test_$$' |
| 643 | |
| 644 | section "90. POSIX OUTPUT FORMATTING" |
| 645 | |
| 646 | compare_posix_output "printf string" 'printf "%s\n" "hello"' |
| 647 | compare_posix_output "printf number" 'printf "%d\n" 42' |
| 648 | compare_posix_output "printf hex" 'printf "%x\n" 255' |
| 649 | compare_posix_output "printf octal" 'printf "%o\n" 64' |
| 650 | compare_posix_output "printf char" 'printf "%c\n" A' |
| 651 | compare_posix_output "echo no newline" 'printf "no newline"' |
| 652 | |
| 653 | # Summary |
| 654 | printf "\n" |
| 655 | printf "==========================================\n" |
| 656 | printf "EXTENDED POSIX COMPLIANCE TEST RESULTS ${TEST_PREFIX}\n" |
| 657 | printf "==========================================\n" |
| 658 | printf "${GREEN}Passed:${NC} %d\n" "$PASSED" |
| 659 | printf "${RED}Failed:${NC} %d\n" "$FAILED" |
| 660 | printf "${YELLOW}Skipped:${NC} %d\n" "$SKIPPED" |
| 661 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 662 | printf "==========================================\n" |
| 663 | |
| 664 | if [ $((PASSED + FAILED)) -gt 0 ]; then |
| 665 | PASS_RATE=$((PASSED * 100 / (PASSED + FAILED))) |
| 666 | printf "Pass rate: %d%%\n" "$PASS_RATE" |
| 667 | fi |
| 668 | |
| 669 | if [ "$FAILED" -gt 0 ]; then |
| 670 | printf "\n${RED}Failed tests:${NC}\n" |
| 671 | printf "%b" "$FAILED_TESTS_LIST" |
| 672 | printf "==========================================\n" |
| 673 | fi |
| 674 | |
| 675 | if [ "$FAILED" -eq 0 ]; then |
| 676 | printf "${GREEN}ALL EXTENDED POSIX TESTS PASSED!${NC} ✓\n" |
| 677 | exit 0 |
| 678 | else |
| 679 | printf "${RED}SOME TESTS FAILED${NC} ✗\n" |
| 680 | exit 1 |
| 681 | fi |