| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Compliance Advanced Test Suite for fortsh |
| 4 | # ===================================== |
| 5 | # Additional coverage based on OpenGroup POSIX.1-2017 specification |
| 6 | # Fills gaps not covered in basic, extended, and builtins test suites |
| 7 | # |
| 8 | # Coverage areas: |
| 9 | # - Advanced control flow (break/continue with levels) |
| 10 | # - Additional special built-ins (exec, command) |
| 11 | # - Extended set options (-f, -x, -v, -a) |
| 12 | # - File descriptor operations |
| 13 | # - Advanced arithmetic operators |
| 14 | # - Signal handling extensions |
| 15 | # - Pathname expansion edge cases |
| 16 | # - IFS edge cases |
| 17 | # - Function recursion and advanced usage |
| 18 | |
| 19 | # Colors (POSIX-compliant way) |
| 20 | RED='\033[0;31m' |
| 21 | GREEN='\033[0;32m' |
| 22 | YELLOW='\033[1;33m' |
| 23 | BLUE='\033[0;34m' |
| 24 | NC='\033[0m' |
| 25 | |
| 26 | # Test identification |
| 27 | TEST_PREFIX="[posix-advanced]" |
| 28 | CURRENT_SECTION="" |
| 29 | TEST_NUM=0 |
| 30 | |
| 31 | PASSED=0 |
| 32 | FAILED=0 |
| 33 | SKIPPED=0 |
| 34 | FAILED_TESTS_LIST="" |
| 35 | |
| 36 | # Get script directory (POSIX way) |
| 37 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) |
| 38 | FORTSH_BIN="${FORTSH_BIN:-$SCRIPT_DIR/../bin/fortsh}" |
| 39 | BASH_REF="${BASH_REF:-bash}" |
| 40 | |
| 41 | # Check if fortsh exists |
| 42 | if [ ! -x "$FORTSH_BIN" ]; then |
| 43 | printf "${RED}ERROR${NC}: fortsh binary not found at $FORTSH_BIN\n" |
| 44 | printf "Please run 'make' first or set FORTSH_BIN environment variable\n" |
| 45 | exit 1 |
| 46 | fi |
| 47 | |
| 48 | # Test result trackers |
| 49 | pass() { |
| 50 | TEST_NUM=$((TEST_NUM + 1)) |
| 51 | printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 52 | PASSED=$((PASSED + 1)) |
| 53 | } |
| 54 | |
| 55 | fail() { |
| 56 | TEST_NUM=$((TEST_NUM + 1)) |
| 57 | TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}" |
| 58 | printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1" |
| 59 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n" |
| 60 | if [ -n "$2" ]; then |
| 61 | printf " posix: %s\n" "$2" |
| 62 | fi |
| 63 | if [ -n "$3" ]; then |
| 64 | printf " fortsh: %s\n" "$3" |
| 65 | fi |
| 66 | FAILED=$((FAILED + 1)) |
| 67 | } |
| 68 | |
| 69 | skip() { |
| 70 | TEST_NUM=$((TEST_NUM + 1)) |
| 71 | printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s - %s\n" "$1" "$2" |
| 72 | SKIPPED=$((SKIPPED + 1)) |
| 73 | } |
| 74 | |
| 75 | section() { |
| 76 | # Extract section number from header like "51. BREAK CONTINUE" |
| 77 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 78 | TEST_NUM=0 |
| 79 | printf "\n" |
| 80 | printf "${BLUE}==========================================\n" |
| 81 | printf "%s\n" "$1" |
| 82 | printf "==========================================${NC}\n" |
| 83 | } |
| 84 | |
| 85 | # Normalize shell error messages by stripping shell name and "line N: " prefix |
| 86 | normalize_output() { |
| 87 | sed -e 's|^[^ ]*bash: |sh: |' -e 's|^[^ ]*fortsh: |sh: |' -e 's/line [0-9]*: //' |
| 88 | } |
| 89 | |
| 90 | # Helper function to run command in both shells and compare |
| 91 | compare_posix_output() { |
| 92 | test_name="$1" |
| 93 | command="$2" |
| 94 | posix_file="/tmp/posix_adv_$$_posix" |
| 95 | fortsh_file="/tmp/posix_adv_$$_fortsh" |
| 96 | |
| 97 | # Run in POSIX shell (sh) |
| 98 | "$BASH_REF" -c "$command" 2>&1 | normalize_output > "$posix_file" || true |
| 99 | |
| 100 | # Run in fortsh |
| 101 | "$FORTSH_BIN" -c "$command" 2>&1 | normalize_output > "$fortsh_file" || true |
| 102 | |
| 103 | # Compare outputs |
| 104 | if diff -q "$posix_file" "$fortsh_file" > /dev/null 2>&1; then |
| 105 | pass "$test_name" |
| 106 | else |
| 107 | fail "$test_name" "$(cat "$posix_file")" "$(cat "$fortsh_file")" |
| 108 | fi |
| 109 | |
| 110 | rm -f "$posix_file" "$fortsh_file" |
| 111 | } |
| 112 | |
| 113 | # Helper function to compare exit codes |
| 114 | compare_posix_exit_code() { |
| 115 | test_name="$1" |
| 116 | command="$2" |
| 117 | |
| 118 | "$BASH_REF" -c "$command" > /dev/null 2>&1 |
| 119 | posix_exit=$? |
| 120 | |
| 121 | "$FORTSH_BIN" -c "$command" > /dev/null 2>&1 |
| 122 | fortsh_exit=$? |
| 123 | |
| 124 | if [ "$posix_exit" -eq "$fortsh_exit" ]; then |
| 125 | pass "$test_name" |
| 126 | else |
| 127 | fail "$test_name" "exit=$posix_exit" "exit=$fortsh_exit" |
| 128 | fi |
| 129 | } |
| 130 | |
| 131 | # Cleanup |
| 132 | cleanup() { |
| 133 | rm -f /tmp/posix_adv_$$_* 2>/dev/null |
| 134 | rm -rf /tmp/posix_advanced_test_* 2>/dev/null |
| 135 | } |
| 136 | trap cleanup EXIT INT TERM |
| 137 | |
| 138 | section "51. BREAK AND CONTINUE IN LOOPS" |
| 139 | |
| 140 | compare_posix_output "break in for loop" 'for i in 1 2 3 4 5; do echo $i; if [ $i -eq 3 ]; then break; fi; done' |
| 141 | compare_posix_output "continue in for loop" 'for i in 1 2 3 4 5; do if [ $i -eq 3 ]; then continue; fi; echo $i; done' |
| 142 | compare_posix_output "break in while loop" 'i=0; while [ $i -lt 5 ]; do i=$((i+1)); echo $i; if [ $i -eq 3 ]; then break; fi; done' |
| 143 | compare_posix_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' |
| 144 | compare_posix_output "break in until loop" 'i=0; until [ $i -ge 5 ]; do i=$((i+1)); echo $i; if [ $i -eq 3 ]; then break; fi; done' |
| 145 | |
| 146 | section "52. NESTED LOOPS WITH BREAK/CONTINUE" |
| 147 | |
| 148 | compare_posix_output "break inner loop" 'for i in 1 2; do for j in a b c; do echo $i$j; if [ $j = b ]; then break; fi; done; done' |
| 149 | compare_posix_output "break with level" 'for i in 1 2; do for j in a b; do echo $i$j; if [ $j = b ]; then break 2; fi; done; done' |
| 150 | compare_posix_output "continue outer loop" 'for i in 1 2 3; do for j in a b; do echo $i$j; if [ $j = a ]; then continue 2; fi; done; done' |
| 151 | |
| 152 | section "53. EXEC BUILTIN" |
| 153 | |
| 154 | compare_posix_output "exec with redirect" 'exec 3>&1; echo test >&3; exec 3>&-; echo done' |
| 155 | compare_posix_exit_code "exec replace shell" '(exec true); echo $?' |
| 156 | compare_posix_output "exec without command" 'exec 2>&1; pwd >/dev/null' |
| 157 | |
| 158 | section "54. COMMAND BUILTIN" |
| 159 | |
| 160 | compare_posix_output "command -v test" 'command -v test | grep -c test' |
| 161 | compare_posix_output "command -v echo" 'command -v echo | grep -c echo' |
| 162 | compare_posix_exit_code "command -v nonexistent" '! command -v nonexistent_cmd_xyz' |
| 163 | |
| 164 | section "55. READ BUILTIN - THOROUGH TESTING" |
| 165 | |
| 166 | compare_posix_output "read single var" 'echo hello | read VAR 2>/dev/null || VAR=hello; echo $VAR' |
| 167 | compare_posix_output "read multiple vars" 'echo one two three | { read A B C; echo $A $B $C; }' |
| 168 | compare_posix_output "read with IFS" 'IFS=:; echo a:b:c | { read X Y Z; echo $X $Y $Z; }' |
| 169 | compare_posix_output "read remaining to last" 'echo a b c d e | { read X Y Z; echo "$Z"; }' |
| 170 | |
| 171 | section "56. FILE DESCRIPTOR DUPLICATION" |
| 172 | |
| 173 | compare_posix_output "dup stdout to fd3" 'exec 3>&1; echo test >&3' |
| 174 | compare_posix_output "dup stdin from fd" 'exec 3</dev/null; cat <&3; exec 3<&-; echo ok' |
| 175 | compare_posix_exit_code "close stdout" '(exec 1>&-; echo test 2>/dev/null); echo $?' |
| 176 | |
| 177 | section "57. SET -F (NOGLOB)" |
| 178 | |
| 179 | compare_posix_output "noglob disables expansion" 'set -f; echo /tmp/*.xyz; set +f' |
| 180 | compare_posix_output "noglob with literal star" 'set -f; VAR="a * b"; echo $VAR; set +f' |
| 181 | compare_posix_output "glob after set +f" 'set -f; set +f; echo /tmp 2>/dev/null | grep -c tmp' |
| 182 | |
| 183 | section "58. SET -X (XTRACE)" |
| 184 | |
| 185 | compare_posix_output "xtrace shows commands" 'set -x; echo test 2>/tmp/xtrace_$$; set +x; grep -c echo /tmp/xtrace_$$; rm -f /tmp/xtrace_$$' |
| 186 | compare_posix_output "xtrace in function" 'f() { set -x; echo inner 2>/tmp/xtrace_fn_$$; set +x; }; f; grep -c echo /tmp/xtrace_fn_$$; rm -f /tmp/xtrace_fn_$$' |
| 187 | |
| 188 | section "59. SET -V (VERBOSE)" |
| 189 | |
| 190 | compare_posix_output "verbose shows input" 'set -v; : test 2>&1 | grep -c test; set +v' |
| 191 | |
| 192 | section "60. SET -A (ALLEXPORT)" |
| 193 | |
| 194 | compare_posix_output "allexport exports vars" 'set -a; TEST_VAR=value; sh -c "echo \$TEST_VAR"' |
| 195 | compare_posix_output "allexport off" 'set +a; TEST2=val; sh -c "echo \${TEST2:-empty}"' |
| 196 | |
| 197 | section "61. TRAP WITH SIGNALS" |
| 198 | |
| 199 | compare_posix_output "trap INT signal" 'trap "echo caught" INT; trap | grep INT' |
| 200 | compare_posix_output "trap TERM signal" 'trap "echo term" TERM; trap | grep TERM' |
| 201 | compare_posix_output "trap HUP signal" 'trap "echo hup" HUP; trap | grep HUP' |
| 202 | compare_posix_output "trap with number" 'trap "echo sig15" 15; trap | grep -c 15' |
| 203 | |
| 204 | section "62. TRAP INHERITANCE IN SUBSHELLS" |
| 205 | |
| 206 | compare_posix_output "trap not inherited" 'trap "echo parent" EXIT; (trap | grep -c EXIT || echo 0)' |
| 207 | compare_posix_output "subshell can set trap" '(trap "echo sub" EXIT; exit 0)' |
| 208 | |
| 209 | section "63. PARAMETER EXPANSION :? ERROR" |
| 210 | |
| 211 | compare_posix_exit_code "error if unset" 'unset VAR; echo ${VAR:?error} 2>/dev/null' |
| 212 | compare_posix_exit_code "error if null" 'VAR=; echo ${VAR:?null} 2>/dev/null' |
| 213 | compare_posix_output "no error if set" 'VAR=test; echo ${VAR:?error}' |
| 214 | |
| 215 | section "64. PARAMETER EXPANSION WITH SPECIAL PARAMS" |
| 216 | |
| 217 | compare_posix_output "length of positional params" 'set -- a b c; echo ${#@}' |
| 218 | compare_posix_output "length of $*" 'set -- x y; echo ${#*}' |
| 219 | compare_posix_output "length of $1" 'set -- hello; echo ${#1}' |
| 220 | |
| 221 | section "65. WAIT WITH ARGUMENTS" |
| 222 | |
| 223 | compare_posix_output "wait specific PID" 'sleep 0.1 & pid=$!; wait $pid; echo $?' |
| 224 | compare_posix_output "wait all background" '(sleep 0.1 &); wait; echo ok' |
| 225 | compare_posix_exit_code "wait nonexistent PID" 'wait 999999' |
| 226 | |
| 227 | section "66. IFS EDGE CASES" |
| 228 | |
| 229 | compare_posix_output "empty IFS" 'IFS=; VAR="a b c"; set -- $VAR; echo $#' |
| 230 | compare_posix_output "IFS whitespace only" 'IFS=" \t\n"; VAR="a b"; set -- $VAR; echo $# $1 $2' |
| 231 | compare_posix_output "IFS custom delimiter" 'IFS=,; VAR="a,b,c"; set -- $VAR; echo $2' |
| 232 | compare_posix_output "IFS leading delimiters" 'IFS=:; VAR=":a:b"; set -- $VAR; echo $#' |
| 233 | |
| 234 | section "67. ARITHMETIC BITWISE OPERATORS" |
| 235 | |
| 236 | compare_posix_output "bitwise AND" 'echo $((12 & 10))' |
| 237 | compare_posix_output "bitwise OR" 'echo $((12 | 10))' |
| 238 | compare_posix_output "bitwise XOR" 'echo $((12 ^ 10))' |
| 239 | compare_posix_output "bitwise NOT" 'echo $((~5))' |
| 240 | compare_posix_output "left shift" 'echo $((3 << 2))' |
| 241 | compare_posix_output "right shift" 'echo $((12 >> 2))' |
| 242 | |
| 243 | section "68. ARITHMETIC ASSIGNMENT OPERATORS" |
| 244 | |
| 245 | compare_posix_output "add assign" 'X=5; echo $((X += 3)); echo $X' |
| 246 | compare_posix_output "subtract assign" 'X=10; echo $((X -= 3)); echo $X' |
| 247 | compare_posix_output "multiply assign" 'X=4; echo $((X *= 2)); echo $X' |
| 248 | compare_posix_output "divide assign" 'X=20; echo $((X /= 4)); echo $X' |
| 249 | compare_posix_output "modulo assign" 'X=17; echo $((X %= 5)); echo $X' |
| 250 | |
| 251 | section "69. ARITHMETIC INCREMENT/DECREMENT" |
| 252 | |
| 253 | compare_posix_output "post increment" 'X=5; echo $((X++)); echo $X' |
| 254 | compare_posix_output "pre increment" 'X=5; echo $((++X)); echo $X' |
| 255 | compare_posix_output "post decrement" 'X=5; echo $((X--)); echo $X' |
| 256 | compare_posix_output "pre decrement" 'X=5; echo $((--X)); echo $X' |
| 257 | |
| 258 | section "70. ARITHMETIC TERNARY OPERATOR" |
| 259 | |
| 260 | compare_posix_output "ternary true" 'echo $((5 > 3 ? 10 : 20))' |
| 261 | compare_posix_output "ternary false" 'echo $((5 < 3 ? 10 : 20))' |
| 262 | compare_posix_output "ternary nested" 'echo $((1 ? 2 : 3 ? 4 : 5))' |
| 263 | |
| 264 | section "71. PIPELINE NEGATION" |
| 265 | |
| 266 | compare_posix_exit_code "negate true" '! true' |
| 267 | compare_posix_exit_code "negate false" '! false' |
| 268 | compare_posix_output "negate pipeline" '! echo test | grep -q xyz; echo $?' |
| 269 | compare_posix_exit_code "negate compound" '! (exit 0)' |
| 270 | |
| 271 | section "72. THREE-STAGE PIPELINES" |
| 272 | |
| 273 | compare_posix_output "three stage pipe" 'echo abc | tr a A | tr b B' |
| 274 | compare_posix_output "four stage pipe" 'echo test | cat | cat | wc -l' |
| 275 | compare_posix_exit_code "multi-stage exit" 'true | true | true' |
| 276 | |
| 277 | section "73. CDPATH VARIABLE" |
| 278 | |
| 279 | compare_posix_output "CDPATH usage" 'mkdir -p /tmp/posix_cdpath_test/subdir; CDPATH=/tmp/posix_cdpath_test; (cd subdir 2>/dev/null && pwd) | grep -c subdir; rm -rf /tmp/posix_cdpath_test' |
| 280 | |
| 281 | section "74. OLDPWD VARIABLE" |
| 282 | |
| 283 | compare_posix_output "OLDPWD set" 'OLD=/tmp; cd /tmp >/dev/null; cd / >/dev/null; echo $OLDPWD | grep -c tmp' |
| 284 | compare_posix_output "cd - uses OLDPWD" 'cd /tmp >/dev/null; cd / >/dev/null; cd - >/dev/null; pwd | grep -c tmp' |
| 285 | |
| 286 | section "75. PATHNAME EXPANSION - BRACKET EXPRESSIONS" |
| 287 | |
| 288 | mkdir -p /tmp/posix_advanced_test_bracket |
| 289 | touch /tmp/posix_advanced_test_bracket/a1.txt /tmp/posix_advanced_test_bracket/a2.txt |
| 290 | touch /tmp/posix_advanced_test_bracket/b1.txt /tmp/posix_advanced_test_bracket/b2.txt |
| 291 | touch /tmp/posix_advanced_test_bracket/c1.txt |
| 292 | |
| 293 | compare_posix_output "bracket range" 'ls /tmp/posix_advanced_test_bracket/[a-b]1.txt 2>/dev/null | wc -l' |
| 294 | compare_posix_output "bracket negation" 'ls /tmp/posix_advanced_test_bracket/[!a]*.txt 2>/dev/null | wc -l' |
| 295 | compare_posix_output "bracket char class" 'ls /tmp/posix_advanced_test_bracket/[[:lower:]]1.txt 2>/dev/null | wc -l' |
| 296 | |
| 297 | rm -rf /tmp/posix_advanced_test_bracket |
| 298 | |
| 299 | section "76. PATHNAME EXPANSION - DOTFILE MATCHING" |
| 300 | |
| 301 | mkdir -p /tmp/posix_advanced_test_dotfile |
| 302 | touch /tmp/posix_advanced_test_dotfile/.hidden |
| 303 | touch /tmp/posix_advanced_test_dotfile/visible |
| 304 | |
| 305 | compare_posix_output "star no dotfiles" 'ls /tmp/posix_advanced_test_dotfile/* 2>/dev/null | wc -l' |
| 306 | compare_posix_output "explicit dot match" 'ls /tmp/posix_advanced_test_dotfile/.* 2>/dev/null | grep -c hidden' |
| 307 | |
| 308 | rm -rf /tmp/posix_advanced_test_dotfile |
| 309 | |
| 310 | section "77. FOR LOOP VARIATIONS" |
| 311 | |
| 312 | compare_posix_output "for with glob" 'touch /tmp/posix_for_1.txt /tmp/posix_for_2.txt /tmp/posix_for_3.txt; for f in /tmp/posix_for_*.txt; do echo $f; done | wc -l; rm /tmp/posix_for_*.txt' |
| 313 | compare_posix_output "for with no items" 'for x in; do echo $x; done; echo empty' |
| 314 | compare_posix_output "for with quoted items" 'for i in "a b" "c d"; do echo $i; done | wc -l' |
| 315 | |
| 316 | section "78. CASE PATTERN MATCHING" |
| 317 | |
| 318 | compare_posix_output "case multiple pattern" 'x=b; case $x in a|b|c) echo match;; *) echo no;; esac' |
| 319 | compare_posix_output "case glob pattern" 'x=hello; case $x in h*) echo prefix;; esac' |
| 320 | compare_posix_output "case question mark" 'x=ab; case $x in ??) echo two;; esac' |
| 321 | compare_posix_output "case bracket" 'x=a; case $x in [abc]) echo bracket;; esac' |
| 322 | |
| 323 | section "79. FUNCTION VARIATIONS" |
| 324 | |
| 325 | compare_posix_output "function recursion" 'fact() { if [ $1 -le 1 ]; then echo 1; else echo $(($1 * $(fact $(($1 - 1))))); fi; }; fact 5' |
| 326 | compare_posix_output "function unset" 'f() { echo test; }; f; unset -f f; command -v f >/dev/null 2>&1; echo $?' |
| 327 | compare_posix_output "nested return" 'a() { b; echo $?; }; b() { return 42; }; a' |
| 328 | |
| 329 | section "80. EXPANSION IN DIFFERENT CONTEXTS" |
| 330 | |
| 331 | compare_posix_output "tilde in assignment" 'VAR=~; echo $VAR | grep -c ^/' |
| 332 | compare_posix_output "expansion in case" 'VAR=test; case $VAR in test) echo match;; esac' |
| 333 | compare_posix_output "no expansion in quotes" 'echo "~" | grep -c "~"' |
| 334 | |
| 335 | section "81. REDIRECTION ORDER AND PRECEDENCE" |
| 336 | |
| 337 | compare_posix_output "multiple redirects" 'echo test >/tmp/redir1 2>&1 >/tmp/redir2; cat /tmp/redir1 /tmp/redir2 2>/dev/null | wc -l; rm -f /tmp/redir1 /tmp/redir2' |
| 338 | compare_posix_output "redirect compound cmd" '{ echo a; echo b; } >/tmp/redir_compound; wc -l < /tmp/redir_compound; rm -f /tmp/redir_compound' |
| 339 | |
| 340 | section "82. ERROR HANDLING IN EXPANSIONS" |
| 341 | |
| 342 | compare_posix_exit_code "division by zero" 'echo $((5 / 0)) 2>/dev/null' |
| 343 | compare_posix_exit_code "expansion error" 'set -u; echo $UNDEFINED_VAR_XYZ 2>/dev/null' |
| 344 | |
| 345 | section "83. SPECIAL PARAMETERS - ADDITIONAL" |
| 346 | |
| 347 | compare_posix_output "PPID is numeric" 'echo $PPID | grep -c "^[0-9]*$"' |
| 348 | compare_posix_output "LINENO exists" 'echo $LINENO | grep -c "^[0-9]*$"' |
| 349 | |
| 350 | section "84. ALIAS AND UNALIAS" |
| 351 | |
| 352 | compare_posix_output "alias definition" 'alias ll="ls -l"; alias ll | grep -c "ls -l"' |
| 353 | compare_posix_output "unalias removes" 'alias test_alias=echo; unalias test_alias; alias test_alias 2>&1 | grep -c "not found"' |
| 354 | |
| 355 | section "85. HASH BUILTIN OPERATIONS" |
| 356 | |
| 357 | compare_posix_output "hash -r clears" 'hash -r; echo $?' |
| 358 | compare_posix_exit_code "hash command" 'hash echo 2>/dev/null' |
| 359 | |
| 360 | section "86. MULTIPLE SEMICOLONS AND EDGE CASES" |
| 361 | |
| 362 | compare_posix_output "multiple semicolons" 'echo a;; echo b' |
| 363 | compare_posix_output "semicolon at start" '; echo test' |
| 364 | compare_posix_exit_code "empty command" '' |
| 365 | |
| 366 | section "87. BACKSLASH LINE CONTINUATION" |
| 367 | |
| 368 | compare_posix_output "backslash continuation" 'echo hel\ |
| 369 | lo' |
| 370 | compare_posix_output "backslash in command" 'ec\ |
| 371 | ho test' |
| 372 | |
| 373 | section "88. COMMENT HANDLING" |
| 374 | |
| 375 | compare_posix_output "comment after command" 'echo visible # this is comment' |
| 376 | compare_posix_output "comment alone" '# comment |
| 377 | echo after' |
| 378 | |
| 379 | section "89. QUOTING EDGE CASES" |
| 380 | |
| 381 | compare_posix_output "empty string quotes" 'echo "" | wc -l' |
| 382 | compare_posix_output "adjacent quotes" 'echo "a"b"c"' |
| 383 | compare_posix_output "quote within quote" "echo \"it's\" | grep -c \"'\"" |
| 384 | |
| 385 | section "90. COMPOUND COMMAND REDIRECTION" |
| 386 | |
| 387 | compare_posix_output "subshell with redirect" '(echo a; echo b) | wc -l' |
| 388 | compare_posix_output "brace group redirect" '{ echo x; echo y; } | wc -l' |
| 389 | compare_posix_output "if statement redirect" 'if true; then echo yes; fi | cat' |
| 390 | |
| 391 | section "91. POSIX PARAMETER LENGTH" |
| 392 | |
| 393 | compare_posix_output "length of empty" 'X=""; echo ${#X}' |
| 394 | compare_posix_output "length with spaces" 'X="a b c"; echo ${#X}' |
| 395 | compare_posix_output "length special chars" 'X="$@#!"; echo ${#X}' |
| 396 | compare_posix_output "length unicode" 'X="abc"; echo ${#X}' |
| 397 | |
| 398 | section "92. POSIX DEFAULT VALUES VARIANTS" |
| 399 | |
| 400 | compare_posix_output ":-colon vs dash" 'X=""; echo "${X:-default}" "${X-default}"' |
| 401 | compare_posix_output ":=colon vs equals" 'unset Y; echo "${Y:=def}"; echo $Y' |
| 402 | compare_posix_output ":+colon vs plus" 'X=val; echo "${X:+alt}" "${X+alt}"' |
| 403 | compare_posix_output ":?colon vs question" 'X=val; echo "${X:?err}" 2>/dev/null' |
| 404 | |
| 405 | section "93. POSIX PATTERN REMOVAL EDGE CASES" |
| 406 | |
| 407 | compare_posix_output "remove empty pattern" 'X=test; echo "${X#}"' |
| 408 | compare_posix_output "remove star pattern" 'X=test; echo "${X#*}"' |
| 409 | compare_posix_output "remove all with ##" 'X=test; echo "${X##*}"' |
| 410 | compare_posix_output "suffix empty" 'X=test; echo "${X%}"' |
| 411 | compare_posix_output "suffix star" 'X=test; echo "${X%*}"' |
| 412 | compare_posix_output "suffix all" 'X=test; echo "${X%%*}"' |
| 413 | |
| 414 | section "94. POSIX ARITHMETIC OPERATORS" |
| 415 | |
| 416 | compare_posix_output "arithmetic modulo" 'echo $((17 % 5))' |
| 417 | compare_posix_output "arithmetic negative" 'echo $((-5 + 3))' |
| 418 | compare_posix_output "arithmetic parens" 'echo $(((2 + 3) * 4))' |
| 419 | compare_posix_output "arithmetic bitwise and" 'echo $((12 & 10))' |
| 420 | compare_posix_output "arithmetic bitwise or" 'echo $((12 | 10))' |
| 421 | compare_posix_output "arithmetic bitwise xor" 'echo $((12 ^ 10))' |
| 422 | compare_posix_output "arithmetic shift left" 'echo $((1 << 4))' |
| 423 | compare_posix_output "arithmetic shift right" 'echo $((16 >> 2))' |
| 424 | |
| 425 | section "95. POSIX ARITHMETIC COMPARISONS" |
| 426 | |
| 427 | compare_posix_output "arith less than" 'echo $((3 < 5))' |
| 428 | compare_posix_output "arith greater than" 'echo $((5 > 3))' |
| 429 | compare_posix_output "arith less equal" 'echo $((3 <= 3))' |
| 430 | compare_posix_output "arith greater equal" 'echo $((3 >= 3))' |
| 431 | compare_posix_output "arith equal" 'echo $((5 == 5))' |
| 432 | compare_posix_output "arith not equal" 'echo $((5 != 3))' |
| 433 | compare_posix_output "arith logical and" 'echo $((1 && 1))' |
| 434 | compare_posix_output "arith logical or" 'echo $((0 || 1))' |
| 435 | |
| 436 | section "96. POSIX COMMAND SUBSTITUTION EDGE CASES" |
| 437 | |
| 438 | compare_posix_output "cmd sub with quotes" 'echo $(echo "hello world")' |
| 439 | compare_posix_output "cmd sub trailing newlines" 'echo "$(printf "a\n\n\n")"b' |
| 440 | compare_posix_output "cmd sub with pipe" 'echo $(echo test | tr t T)' |
| 441 | compare_posix_output "cmd sub with redirect" 'echo $(cat </dev/null)' |
| 442 | compare_posix_output "backtick with quotes" 'echo `echo "hello"`' |
| 443 | |
| 444 | section "97. POSIX SUBSHELL VARIABLE ISOLATION" |
| 445 | |
| 446 | compare_posix_output "subshell no export" 'X=1; (X=2; echo $X); echo $X' |
| 447 | compare_posix_output "subshell unset" 'X=1; (unset X; echo ${X:-empty}); echo $X' |
| 448 | # Note: (( expr )) is arithmetic syntax in bash, not nested subshell. Compare exit codes. |
| 449 | compare_posix_exit_code "double paren arithmetic" '(( echo nested ))' |
| 450 | compare_posix_output "subshell exit" '(exit 5); echo $?' |
| 451 | |
| 452 | section "98. POSIX BRACE GROUP VS SUBSHELL" |
| 453 | |
| 454 | compare_posix_output "brace group modifies" 'X=1; { X=2; }; echo $X' |
| 455 | compare_posix_output "subshell isolates" 'X=1; (X=2); echo $X' |
| 456 | compare_posix_output "brace with semicolon" '{ echo a; echo b; }' |
| 457 | compare_posix_output "brace in pipeline" '{ echo test; } | cat' |
| 458 | |
| 459 | section "99. POSIX HERE-STRING ALTERNATIVES" |
| 460 | |
| 461 | compare_posix_output "echo pipe vs redirect" 'echo test | cat' |
| 462 | compare_posix_output "printf to stdin" 'printf "test\n" | read X; echo $X' |
| 463 | |
| 464 | section "100. POSIX PIPELINE EXIT STATUS" |
| 465 | |
| 466 | compare_posix_output "pipe success" 'true | true; echo $?' |
| 467 | compare_posix_output "pipe last fails" 'true | false; echo $?' |
| 468 | compare_posix_output "pipe first fails" 'false | true; echo $?' |
| 469 | compare_posix_output "long pipeline" 'echo a | cat | cat | cat; echo $?' |
| 470 | |
| 471 | section "101. POSIX PROCESS SUBSTITUTION ALTERNATIVES" |
| 472 | |
| 473 | compare_posix_output "temp file pattern" 'echo test > /tmp/psub$$; cat /tmp/psub$$; rm /tmp/psub$$' |
| 474 | compare_posix_output "named pipe simulation" 'mkfifo /tmp/pfifo$$ 2>/dev/null || true; rm -f /tmp/pfifo$$; echo ok' |
| 475 | |
| 476 | section "102. POSIX TEST BRACKETS" |
| 477 | |
| 478 | compare_posix_output "test vs bracket" 'test 1 -eq 1 && [ 1 -eq 1 ] && echo ok' |
| 479 | compare_posix_output "bracket spacing" '[ "a" = "a" ] && echo match' |
| 480 | compare_posix_output "empty bracket test" '[ "" ] || echo empty' |
| 481 | compare_posix_output "nonempty test" '[ "x" ] && echo nonempty' |
| 482 | |
| 483 | section "103. POSIX GETOPTS ADVANCED" |
| 484 | |
| 485 | compare_posix_output "getopts multiple" 'while getopts "ab:c" opt 2>/dev/null; do echo $opt; done <<< "-a -b val -c"' |
| 486 | compare_posix_output "getopts OPTARG" 'getopts "a:" opt <<< "-a value" 2>/dev/null; echo $OPTARG' |
| 487 | compare_posix_output "getopts OPTIND" 'OPTIND=1; getopts "a" opt <<< "-a" 2>/dev/null; echo $OPTIND' |
| 488 | |
| 489 | section "104. POSIX READ BUILTIN" |
| 490 | |
| 491 | compare_posix_output "read single var" 'echo "hello" | { read X; echo $X; }' |
| 492 | compare_posix_output "read multiple vars" 'echo "a b c" | { read X Y Z; echo "$X:$Y:$Z"; }' |
| 493 | compare_posix_output "read with IFS" 'echo "a:b:c" | { IFS=: read X Y Z; echo "$X $Y $Z"; }' |
| 494 | compare_posix_output "read extra words" 'echo "a b c d" | { read X Y; echo "$X:$Y"; }' |
| 495 | |
| 496 | section "105. POSIX PRINTF FORMATTING" |
| 497 | |
| 498 | compare_posix_output "printf string" 'printf "%s\n" "test"' |
| 499 | compare_posix_output "printf integer" 'printf "%d\n" 42' |
| 500 | compare_posix_output "printf width" 'printf "%10s\n" "hi"' |
| 501 | compare_posix_output "printf left align" 'printf "%-10s|\n" "hi"' |
| 502 | compare_posix_output "printf zero pad" 'printf "%05d\n" 42' |
| 503 | compare_posix_output "printf multiple" 'printf "%s %d\n" "val" 123' |
| 504 | |
| 505 | section "106. POSIX ARITHMETIC EDGE CASES" |
| 506 | |
| 507 | compare_posix_output "unary minus" 'echo $((-5))' |
| 508 | compare_posix_output "unary plus" 'echo $((+5))' |
| 509 | compare_posix_output "double negative" 'echo $((--5))' |
| 510 | compare_posix_output "zero division check" 'echo $((5 / 1))' |
| 511 | compare_posix_output "modulo zero check" 'echo $((5 % 1))' |
| 512 | compare_posix_output "large number" 'echo $((1000000 * 1000))' |
| 513 | compare_posix_output "negative modulo" 'echo $((-7 % 3))' |
| 514 | compare_posix_output "chained ops" 'echo $((1 + 2 + 3 + 4 + 5))' |
| 515 | |
| 516 | section "107. POSIX VARIABLE EDGE CASES" |
| 517 | |
| 518 | compare_posix_output "underscore var" 'X_Y_Z=test; echo $X_Y_Z' |
| 519 | compare_posix_output "numeric suffix var" 'VAR1=a; VAR2=b; echo $VAR1$VAR2' |
| 520 | compare_posix_output "long var name" 'VERY_LONG_VARIABLE_NAME_HERE=val; echo $VERY_LONG_VARIABLE_NAME_HERE' |
| 521 | compare_posix_output "var with digits" 'A1B2C3=mix; echo $A1B2C3' |
| 522 | compare_posix_output "reassignment" 'X=1; X=2; X=3; echo $X' |
| 523 | compare_posix_output "self reference" 'X=a; X=${X}b; echo $X' |
| 524 | |
| 525 | section "108. POSIX FUNCTION EDGE CASES" |
| 526 | |
| 527 | compare_posix_output "function redefine" 'f() { echo first; }; f() { echo second; }; f' |
| 528 | compare_posix_output "function in pipeline" 'f() { echo test; }; f | cat' |
| 529 | compare_posix_output "function with subshell" 'f() { (echo sub); }; f' |
| 530 | compare_posix_output "function return in if" 'f() { if true; then return 0; fi; return 1; }; f; echo $?' |
| 531 | compare_posix_output "function shift" 'f() { shift; echo $1; }; f a b c' |
| 532 | compare_posix_output "function all args" 'f() { echo "$@"; }; f one two three' |
| 533 | |
| 534 | section "109. POSIX LOOP EDGE CASES" |
| 535 | |
| 536 | compare_posix_output "for single item" 'for i in single; do echo $i; done' |
| 537 | compare_posix_output "for many items" 'for i in 1 2 3 4 5 6 7 8 9 10; do echo $i; done | wc -l' |
| 538 | compare_posix_output "nested for" 'for i in a b; do for j in 1 2; do echo $i$j; done; done' |
| 539 | compare_posix_output "while nested" 'i=0; while [ $i -lt 2 ]; do j=0; while [ $j -lt 2 ]; do echo $i$j; j=$((j+1)); done; i=$((i+1)); done' |
| 540 | compare_posix_output "for with continue" 'for i in 1 2 3; do [ $i -eq 2 ] && continue; echo $i; done' |
| 541 | |
| 542 | section "110. POSIX CASE EDGE CASES" |
| 543 | |
| 544 | compare_posix_output "case empty pattern" 'x=; case "$x" in "") echo empty;; esac' |
| 545 | compare_posix_output "case glob star" 'x=anything; case $x in *) echo matched;; esac' |
| 546 | compare_posix_output "case multiple star" 'x=test; case $x in a*) echo a;; t*) echo t;; esac' |
| 547 | compare_posix_output "case bracket range" 'x=5; case $x in [0-9]) echo digit;; esac' |
| 548 | compare_posix_output "case no default" 'x=z; case $x in a) echo a;; b) echo b;; esac; echo done' |
| 549 | |
| 550 | section "111. POSIX REDIRECTION EDGE CASES" |
| 551 | |
| 552 | compare_posix_output "append create" 'rm -f /tmp/append_test_$$; echo first >> /tmp/append_test_$$; cat /tmp/append_test_$$; rm /tmp/append_test_$$' |
| 553 | compare_posix_output "redirect in loop" 'for i in 1 2 3; do echo $i; done > /tmp/loop_redir_$$; wc -l < /tmp/loop_redir_$$; rm /tmp/loop_redir_$$' |
| 554 | compare_posix_output "redirect in func" 'f() { echo test; }; f > /tmp/func_redir_$$; cat /tmp/func_redir_$$; rm /tmp/func_redir_$$' |
| 555 | compare_posix_output "multiple output" 'echo a > /tmp/multi1_$$; echo b > /tmp/multi2_$$; cat /tmp/multi1_$$ /tmp/multi2_$$; rm /tmp/multi1_$$ /tmp/multi2_$$' |
| 556 | |
| 557 | section "112. POSIX PIPELINE EDGE CASES" |
| 558 | |
| 559 | compare_posix_output "five stage pipe" 'echo test | cat | cat | cat | cat | cat' |
| 560 | compare_posix_output "pipe with grep" 'printf "a\nb\nc\n" | grep b' |
| 561 | compare_posix_output "pipe with wc" 'printf "a\nb\nc\n" | wc -l' |
| 562 | compare_posix_output "pipe with sort" 'printf "c\na\nb\n" | sort' |
| 563 | compare_posix_output "pipe with uniq" 'printf "a\na\nb\n" | uniq' |
| 564 | compare_posix_output "pipe with tr" 'echo abc | tr a-z A-Z' |
| 565 | |
| 566 | section "113. POSIX SUBSHELL EDGE CASES" |
| 567 | |
| 568 | compare_posix_output "subshell pwd" '(cd /tmp; pwd)' |
| 569 | compare_posix_output "subshell var" 'X=outer; (X=inner); echo $X' |
| 570 | compare_posix_output "subshell function" 'f() { echo hi; }; (f)' |
| 571 | compare_posix_output "subshell pipeline" '(echo a; echo b) | wc -l' |
| 572 | # Note: ((( expr ))) is arithmetic syntax in bash. Compare exit codes. |
| 573 | compare_posix_exit_code "triple paren arithmetic" '(((echo deep)))' |
| 574 | compare_posix_output "subshell arithmetic" '(echo $((2+2)))' |
| 575 | |
| 576 | section "114. POSIX BRACE GROUP EDGE CASES" |
| 577 | |
| 578 | compare_posix_output "brace simple" '{ echo test; }' |
| 579 | compare_posix_output "brace multi" '{ echo a; echo b; echo c; }' |
| 580 | compare_posix_output "brace var" 'X=1; { X=2; }; echo $X' |
| 581 | compare_posix_output "brace redirect" '{ echo test; } > /tmp/brace_$$; cat /tmp/brace_$$; rm /tmp/brace_$$' |
| 582 | compare_posix_output "brace in pipe" '{ echo a; echo b; } | wc -l' |
| 583 | |
| 584 | section "115. POSIX SPECIAL CHAR EDGE CASES" |
| 585 | |
| 586 | compare_posix_output "semicolon list" 'echo a; echo b; echo c' |
| 587 | compare_posix_output "ampersand bg" 'sleep 0.01 & wait; echo done' |
| 588 | compare_posix_output "double ampersand" 'true && echo yes' |
| 589 | compare_posix_output "double pipe" 'false || echo fallback' |
| 590 | compare_posix_output "mixed logic" 'true && true && echo all_true' |
| 591 | compare_posix_output "negation" '! false && echo negated' |
| 592 | |
| 593 | # Summary |
| 594 | printf "\n" |
| 595 | printf "==========================================\n" |
| 596 | printf "ADVANCED POSIX COMPLIANCE TEST RESULTS ${TEST_PREFIX}\n" |
| 597 | printf "==========================================\n" |
| 598 | printf "${GREEN}Passed:${NC} %d\n" "$PASSED" |
| 599 | printf "${RED}Failed:${NC} %d\n" "$FAILED" |
| 600 | printf "${YELLOW}Skipped:${NC} %d\n" "$SKIPPED" |
| 601 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 602 | printf "==========================================\n" |
| 603 | |
| 604 | if [ $((PASSED + FAILED)) -gt 0 ]; then |
| 605 | PASS_RATE=$((PASSED * 100 / (PASSED + FAILED))) |
| 606 | printf "Pass rate: %d%%\n" "$PASS_RATE" |
| 607 | fi |
| 608 | |
| 609 | if [ "$FAILED" -gt 0 ]; then |
| 610 | printf "\n${RED}Failed tests:${NC}\n" |
| 611 | printf "%b" "$FAILED_TESTS_LIST" |
| 612 | printf "==========================================\n" |
| 613 | fi |
| 614 | |
| 615 | if [ "$FAILED" -eq 0 ]; then |
| 616 | printf "${GREEN}ALL ADVANCED POSIX TESTS PASSED!${NC} ✓\n" |
| 617 | exit 0 |
| 618 | else |
| 619 | printf "${RED}SOME TESTS FAILED${NC} ✗\n" |
| 620 | exit 1 |
| 621 | fi |