Bash · 16893 bytes Raw Blame History
1 #!/bin/sh
2 # =====================================
3 # POSIX Compliance Test Suite for shell
4 # =====================================
5 # Tests compliance with POSIX shell specification
6 # Uses /bin/sh for comparison (typically dash or bash in POSIX mode)
7
8 # Note: Using only POSIX-compliant constructs in this script
9 # No bash-isms allowed!
10
11 # Colors (POSIX-compliant way)
12 RED='\033[0;31m'
13 GREEN='\033[0;32m'
14 YELLOW='\033[1;33m'
15 BLUE='\033[0;34m'
16 NC='\033[0m'
17
18 # Test identification
19 TEST_PREFIX="[posix-test]"
20 CURRENT_SECTION=""
21 TEST_NUM=0
22
23 PASSED=0
24 FAILED=0
25 SKIPPED=0
26 FAILED_TESTS_LIST=""
27
28 # Get script directory (POSIX way)
29 SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
30 SHELL_BIN="${SHELL_BIN:?ERROR: SHELL_BIN must be set}"
31 BASH_REF="${BASH_REF:-bash}"
32
33 # Check if shell binary exists
34 if [ ! -x "$SHELL_BIN" ]; then
35 printf "${RED}ERROR${NC}: shell binary not found at $SHELL_BIN\n"
36 printf "Please set SHELL_BIN or set SHELL_BIN environment variable\n"
37 exit 1
38 fi
39
40 # Test result trackers
41 pass() {
42 TEST_NUM=$((TEST_NUM + 1))
43 printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1"
44 PASSED=$((PASSED + 1))
45 }
46
47 fail() {
48 TEST_NUM=$((TEST_NUM + 1))
49 TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}"
50 printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1"
51 FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n"
52 if [ -n "$2" ]; then
53 printf " posix: %s\n" "$2"
54 fi
55 if [ -n "$3" ]; then
56 printf " shell: %s\n" "$3"
57 fi
58 FAILED=$((FAILED + 1))
59 }
60
61 skip() {
62 TEST_NUM=$((TEST_NUM + 1))
63 printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s - %s\n" "$1" "$2"
64 SKIPPED=$((SKIPPED + 1))
65 }
66
67 section() {
68 # Extract section number from header like "3. POSIX PARAMETER EXPANSION"
69 CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0")
70 TEST_NUM=0
71 printf "\n"
72 printf "${BLUE}==========================================\n"
73 printf "%s\n" "$1"
74 printf "==========================================${NC}\n"
75 }
76
77 # Helper function to run command in both shells and compare
78 compare_posix_output() {
79 test_name="$1"
80 command="$2"
81 posix_file="/tmp/posix_comp_$$_posix"
82 shell_file="/tmp/posix_comp_$$_fortsh"
83
84 # Run in POSIX shell (sh)
85 "$BASH_REF" -c "$command" > "$posix_file" 2>&1 || true
86
87 # Run in shell under test
88 "$SHELL_BIN" -c "$command" > "$shell_file" 2>&1 || true
89
90 # Compare outputs
91 if diff -q "$posix_file" "$shell_file" > /dev/null 2>&1; then
92 pass "$test_name"
93 else
94 fail "$test_name" "$(cat "$posix_file")" "$(cat "$shell_file")"
95 fi
96
97 rm -f "$posix_file" "$shell_file"
98 }
99
100 # Helper function to compare exit codes
101 compare_posix_exit_code() {
102 test_name="$1"
103 command="$2"
104
105 "$BASH_REF" -c "$command" > /dev/null 2>&1
106 posix_exit=$?
107
108 "$SHELL_BIN" -c "$command" > /dev/null 2>&1
109 shell_exit=$?
110
111 if [ "$posix_exit" -eq "$shell_exit" ]; then
112 pass "$test_name"
113 else
114 fail "$test_name" "exit=$posix_exit" "exit=$shell_exit"
115 fi
116 }
117
118 # Cleanup
119 cleanup() {
120 rm -f /tmp/posix_comp_$$_* 2>/dev/null
121 rm -f /tmp/posix_test_* 2>/dev/null
122 }
123 trap cleanup EXIT INT TERM
124
125 section "1. POSIX BASIC COMMANDS"
126
127 compare_posix_output "echo simple" "echo hello"
128 compare_posix_output "echo with args" "echo one two three"
129 compare_posix_output "printf basic" "printf 'test\n'"
130 compare_posix_output "printf with args" "printf '%s %d\n' hello 42"
131
132 section "2. POSIX VARIABLE EXPANSION"
133
134 compare_posix_output "simple variable" "VAR=test; echo \$VAR"
135 compare_posix_output "variable in quotes" 'VAR=test; echo "$VAR"'
136 compare_posix_output "multiple vars" "A=hello; B=world; echo \$A \$B"
137 compare_posix_output "undefined variable" "echo \$UNDEFINED_VAR_XYZ_987"
138
139 section "3. POSIX PARAMETER EXPANSION"
140
141 # Basic parameter expansion
142 compare_posix_output "default value" 'echo "${UNSET:-default}"'
143 compare_posix_output "assign default" 'UNSET=; echo "${UNSET:=assigned}"; echo $UNSET'
144 compare_posix_output "error if unset" 'echo "${VAR:+alternative}"'
145 compare_posix_output "string length" 'VAR=hello; echo "${#VAR}"'
146
147 # Prefix removal (# and ##)
148 compare_posix_output "remove shortest prefix" 'VAR=foo.bar.baz; echo "${VAR#*.}"'
149 compare_posix_output "remove longest prefix" 'VAR=foo.bar.baz; echo "${VAR##*.}"'
150 compare_posix_output "prefix no match" 'VAR=hello; echo "${VAR#x*}"'
151 compare_posix_output "prefix remove slash" 'VAR=/usr/local/bin; echo "${VAR#/*/}"'
152
153 # Suffix removal (% and %%)
154 compare_posix_output "remove shortest suffix" 'VAR=foo.bar.baz; echo "${VAR%.*}"'
155 compare_posix_output "remove longest suffix" 'VAR=foo.bar.baz; echo "${VAR%%.*}"'
156 compare_posix_output "suffix no match" 'VAR=hello; echo "${VAR%x*}"'
157 compare_posix_output "suffix remove extension" 'VAR=file.tar.gz; echo "${VAR%.gz}"'
158
159 section "4. POSIX COMMAND SUBSTITUTION"
160
161 compare_posix_output "backtick substitution" 'echo `echo test`'
162 compare_posix_output "dollar paren substitution" "echo \$(echo test)"
163 compare_posix_output "nested substitution" "echo \$(echo \$(echo nested))"
164
165 section "5. POSIX ARITHMETIC"
166
167 # POSIX arithmetic uses expr or $(( ))
168 compare_posix_output "expr addition" "expr 5 + 3"
169 compare_posix_output "expr multiplication" "expr 4 \* 3"
170 compare_posix_output "expr division" "expr 15 / 3"
171
172 section "6. POSIX REDIRECTION"
173
174 compare_posix_output "output redirect" "echo test > /tmp/posix_test_out; cat /tmp/posix_test_out"
175 compare_posix_output "append redirect" "echo line1 > /tmp/posix_test_app; echo line2 >> /tmp/posix_test_app; wc -l < /tmp/posix_test_app"
176 compare_posix_output "input redirect" "echo input > /tmp/posix_test_in; cat < /tmp/posix_test_in"
177 compare_posix_output "stderr redirect" "ls /nonexistent 2>&1 | grep -c 'cannot access\|No such\|not found'"
178
179 section "7. POSIX PIPELINES"
180
181 compare_posix_output "simple pipe" "echo hello | cat"
182 compare_posix_output "two-stage pipe" "echo test | cat | tr t T"
183 compare_posix_output "pipe with filter" "printf 'a\nb\nc\n' | grep b"
184
185 section "8. POSIX TEST COMMAND"
186
187 compare_posix_exit_code "test -f file" "touch /tmp/posix_test_file && test -f /tmp/posix_test_file"
188 compare_posix_exit_code "test -d directory" "test -d /tmp"
189 compare_posix_exit_code "test -n nonempty" "test -n 'hello'"
190 compare_posix_exit_code "test -z empty" "test -z ''"
191 compare_posix_exit_code "test string =" "test 'hello' = 'hello'"
192 compare_posix_exit_code "test string !=" "test 'hello' != 'world'"
193 compare_posix_exit_code "test number -eq" "test 5 -eq 5"
194 compare_posix_exit_code "test number -ne" "test 5 -ne 3"
195 compare_posix_exit_code "test number -gt" "test 5 -gt 3"
196 compare_posix_exit_code "test number -ge" "test 5 -ge 5"
197 compare_posix_exit_code "test number -lt" "test 3 -lt 5"
198 compare_posix_exit_code "test number -le" "test 3 -le 3"
199
200 section "9. POSIX CONDITIONALS"
201
202 compare_posix_output "if true" "if true; then echo yes; fi"
203 compare_posix_output "if false else" "if false; then echo no; else echo yes; fi"
204 compare_posix_output "if-elif-else" "X=2; if [ \$X -eq 1 ]; then echo one; elif [ \$X -eq 2 ]; then echo two; else echo other; fi"
205
206 section "10. POSIX LOOPS"
207
208 compare_posix_output "for loop" "for i in a b c; do echo \$i; done"
209 compare_posix_output "while loop" "i=3; while [ \$i -gt 0 ]; do echo \$i; i=\$((i - 1)); done"
210 compare_posix_output "until loop" "i=1; until [ \$i -gt 3 ]; do echo \$i; i=\$((i + 1)); done"
211
212 section "11. POSIX CASE STATEMENT"
213
214 compare_posix_output "case exact match" "x=2; case \$x in 1) echo one;; 2) echo two;; esac"
215 compare_posix_output "case pattern match" "x=hello; case \$x in h*) echo h_prefix;; esac"
216 compare_posix_output "case default" "x=z; case \$x in a) echo a;; b) echo b;; *) echo default;; esac"
217 compare_posix_output "case multiple patterns" "x=b; case \$x in a|b|c) echo abc;; *) echo other;; esac"
218
219 section "12. POSIX FUNCTIONS"
220
221 compare_posix_output "simple function" "func() { echo hello; }; func"
222 compare_posix_output "function with args" "func() { echo \$1 \$2; }; func foo bar"
223 compare_posix_output "function return" "func() { return 42; }; func; echo \$?"
224 compare_posix_output "function \$# args" "func() { echo \$#; }; func a b c"
225
226 section "13. POSIX SPECIAL VARIABLES"
227
228 compare_posix_output "\$? exit status" "true; echo \$?"
229 compare_posix_output "\$? after false" "false; echo \$?"
230 compare_posix_output "\$# argument count" "set -- a b c; echo \$#"
231 compare_posix_output "\$@ all arguments" "set -- a b c; echo \$@"
232 compare_posix_output "\$* all arguments" "set -- a b c; echo \$*"
233 compare_posix_output "\$0 script name" "echo \$0 | grep -c sh"
234
235 section "14. POSIX LOGICAL OPERATORS"
236
237 compare_posix_exit_code "true && true" "true && true"
238 compare_posix_exit_code "true && false" "true && false"
239 compare_posix_exit_code "false || true" "false || true"
240 compare_posix_exit_code "false || false" "false || false"
241 compare_posix_output "command && echo" "true && echo success"
242 compare_posix_output "command || echo" "false || echo fallback"
243 compare_posix_output "! negation" "! false && echo negated"
244
245 section "15. POSIX QUOTING"
246
247 compare_posix_output "single quote literal" "echo '\$VAR'"
248 compare_posix_output "double quote expand" 'VAR=test; echo "$VAR"'
249 compare_posix_output "escape in double" 'echo "test\$var"'
250 compare_posix_output "backslash escape" 'echo test\ word'
251
252 section "16. POSIX SUBSHELLS"
253
254 compare_posix_output "subshell grouping" "(echo a; echo b) | wc -l"
255 compare_posix_output "subshell var isolation" "(VAR=inner; echo \$VAR); echo \$VAR"
256
257 section "17. POSIX COMPOUND COMMANDS"
258
259 compare_posix_output "command grouping {}" "{ echo a; echo b; } | wc -l"
260 compare_posix_output "command list ;" "echo a; echo b"
261
262 section "18. POSIX HERE DOCUMENTS"
263
264 compare_posix_output "simple heredoc" "cat <<EOF
265 line1
266 line2
267 EOF"
268
269 compare_posix_output "heredoc with vars" "VAR=test; cat <<EOF
270 value=\$VAR
271 EOF"
272
273 compare_posix_output "quoted heredoc" "cat <<'EOF'
274 \$VAR
275 EOF"
276
277 section "19. POSIX WORD EXPANSION ORDER"
278
279 # POSIX specifies: tilde, parameter, command subst, arithmetic, field splitting, pathname, quote removal
280 compare_posix_output "expansion order" "VAR='a b'; echo \$VAR"
281 compare_posix_output "quoted expansion" 'VAR="a b"; echo "$VAR"'
282
283 section "20. POSIX PATHNAME EXPANSION (GLOBBING)"
284
285 # Setup test files
286 mkdir -p /tmp/posix_test_glob
287 touch /tmp/posix_test_glob/a.txt /tmp/posix_test_glob/b.txt /tmp/posix_test_glob/c.dat
288
289 compare_posix_output "glob * pattern" "ls /tmp/posix_test_glob/*.txt 2>/dev/null | wc -l"
290 compare_posix_output "glob ? pattern" "ls /tmp/posix_test_glob/?.txt 2>/dev/null | wc -l"
291 compare_posix_output "glob [abc] pattern" "ls /tmp/posix_test_glob/[ab].txt 2>/dev/null | wc -l"
292
293 section "21. POSIX FIELD SPLITTING (IFS)"
294
295 compare_posix_output "default IFS" "VAR='a b c'; set -- \$VAR; echo \$#"
296 compare_posix_output "custom IFS" "IFS=:; VAR='a:b:c'; set -- \$VAR; echo \$1"
297
298 section "22. POSIX EXIT STATUS"
299
300 compare_posix_exit_code "true exit status" "true"
301 compare_posix_exit_code "false exit status" "false"
302 compare_posix_exit_code "command not found" "nonexistent_command_xyz 2>/dev/null"
303 compare_posix_exit_code "return from function" "func() { return 3; }; func"
304
305 section "23. POSIX SET BUILTIN"
306
307 compare_posix_output "set positional" "set -- a b c; echo \$1 \$2 \$3"
308 compare_posix_output "set shift" "set -- a b c; shift; echo \$1"
309 compare_posix_output "set shift n" "set -- a b c d; shift 2; echo \$1"
310
311 section "24. POSIX EXPORT"
312
313 compare_posix_output "export variable" "export VAR=test; sh -c 'echo \$VAR'"
314
315 section "25. POSIX READONLY"
316
317 compare_posix_exit_code "readonly assignment" "readonly VAR=test; VAR=new 2>/dev/null"
318
319 section "26. POSIX UNSET"
320
321 compare_posix_output "unset variable" "VAR=test; unset VAR; echo \${VAR:-empty}"
322 compare_posix_output "unset nonexistent" "unset NONEXISTENT_VAR; echo ok"
323 compare_posix_exit_code "unset readonly fails" "readonly X=1; unset X 2>/dev/null"
324 compare_posix_output "unset function" "f() { echo hi; }; unset -f f; f 2>/dev/null || echo gone"
325
326 section "27. POSIX EVAL"
327
328 compare_posix_output "eval simple" "eval 'echo hello'"
329 compare_posix_output "eval variable" "CMD='echo test'; eval \$CMD"
330 compare_posix_output "eval assignment" "eval 'X=5'; echo \$X"
331 compare_posix_output "eval command subst" "eval 'echo \$(echo nested)'"
332 compare_posix_output "eval with semicolon" "eval 'echo a; echo b'"
333
334 section "28. POSIX EXEC"
335
336 compare_posix_output "exec replaces shell" "exec echo done"
337 compare_posix_exit_code "exec nonexistent" "exec /nonexistent/command 2>/dev/null"
338
339 section "29. POSIX COLON BUILTIN"
340
341 compare_posix_output "colon no-op" ": ; echo ok"
342 compare_posix_exit_code "colon exit status" ":"
343 compare_posix_output "colon with args" ": arg1 arg2; echo ok"
344 compare_posix_output "colon in if" "if :; then echo yes; fi"
345
346 section "30. POSIX DOT/SOURCE"
347
348 # Create temp script
349 echo 'SOURCED_VAR=from_source' > /tmp/posix_test_source.sh
350 compare_posix_output "dot source script" ". /tmp/posix_test_source.sh; echo \$SOURCED_VAR"
351 compare_posix_exit_code "dot nonexistent" ". /nonexistent_file 2>/dev/null"
352
353 section "31. POSIX CD AND PWD"
354
355 compare_posix_output "cd and pwd" "cd /tmp && pwd"
356 compare_posix_output "cd - returns to OLDPWD" "cd /tmp; cd /; cd -"
357 compare_posix_exit_code "cd nonexistent" "cd /nonexistent_dir 2>/dev/null"
358 compare_posix_output "pwd builtin" "pwd | grep -c /"
359
360 section "32. POSIX UMASK"
361
362 compare_posix_output "umask display" "umask | grep -E '^[0-9]{3,4}\$'"
363 compare_posix_output "umask set and restore" "OLD=\$(umask); umask 077; umask \$OLD"
364
365 section "33. POSIX WAIT"
366
367 compare_posix_output "wait for background" "sleep 0.1 & wait; echo done"
368 compare_posix_exit_code "wait no jobs" "wait"
369
370 section "34. POSIX TIMES"
371
372 # times is optional but common
373 # times output contains variable timing values, so just verify format
374 _times_out=$("$SHELL_BIN" -c 'times' 2>/dev/null)
375 if echo "$_times_out" | grep -qE '[0-9]+m[0-9]+\.[0-9]+s'; then
376 pass "times output exists"
377 else
378 fail "times output exists" "NmN.NNNs NmN.NNNs" "$_times_out"
379 fi
380
381 section "35. POSIX BREAK AND CONTINUE"
382
383 compare_posix_output "break in for" "for i in 1 2 3 4 5; do [ \$i -eq 3 ] && break; echo \$i; done"
384 compare_posix_output "continue in for" "for i in 1 2 3 4 5; do [ \$i -eq 3 ] && continue; echo \$i; done"
385 compare_posix_output "break in while" "i=0; while [ \$i -lt 10 ]; do i=\$((i+1)); [ \$i -eq 3 ] && break; echo \$i; done"
386 compare_posix_output "break 2 nested" "for i in a b; do for j in 1 2 3; do [ \$j -eq 2 ] && break 2; echo \$i\$j; done; done"
387 compare_posix_output "continue 2 nested" "for i in a b; do for j in 1 2 3; do [ \$j -eq 2 ] && continue 2; echo \$i\$j; done; done"
388
389 section "36. POSIX SIGNAL HANDLING"
390
391 # Note: trap output may vary by environment - test exit code
392 compare_posix_exit_code "trap list" "trap >/dev/null 2>&1"
393 compare_posix_output "trap on exit" "trap 'echo exiting' EXIT; exit 0"
394 compare_posix_exit_code "trap reset" "trap - INT"
395
396 section "37. POSIX BACKGROUND AND JOBS"
397
398 compare_posix_output "background job" "sleep 0.1 & echo started; wait"
399 # Note: $! returns different PIDs in each shell, so we check both output valid PIDs (exit code)
400 compare_posix_exit_code "\$! last background pid" "sleep 0.1 & echo \$! | grep -qE '^[0-9]+\$'"
401
402 section "38. POSIX ALIAS"
403
404 compare_posix_output "alias definition" "alias ll='ls -l'; alias | grep ll"
405 compare_posix_output "unalias" "alias x='echo test'; unalias x; alias | grep -c 'x=' || echo 0"
406
407 section "39. POSIX COMMAND SEARCH"
408
409 compare_posix_output "type builtin" "type echo | grep -c builtin"
410 compare_posix_output "command -v" "command -v echo | grep -c echo"
411 compare_posix_exit_code "command not found" "command -v nonexistent_xyz 2>/dev/null"
412
413 section "40. POSIX COMPLEX EXPANSIONS"
414
415 compare_posix_output "nested parameter expansion" 'A=hello; B=A; eval "echo \$$B"'
416 compare_posix_output "expansion in assignment" 'X=$(echo test); echo $X'
417 compare_posix_output "arithmetic in expansion" 'echo $((2 + 3 * 4))'
418 compare_posix_output "multiple substitutions" 'A=1; B=2; echo $(echo $A) $(echo $B)'
419
420 # Summary
421 printf "\n"
422 printf "==========================================\n"
423 printf "POSIX COMPLIANCE TEST RESULTS ${TEST_PREFIX}\n"
424 printf "==========================================\n"
425 printf "${GREEN}Passed:${NC} %d\n" "$PASSED"
426 printf "${RED}Failed:${NC} %d\n" "$FAILED"
427 printf "${YELLOW}Skipped:${NC} %d\n" "$SKIPPED"
428 printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))"
429 printf "==========================================\n"
430
431 if [ $((PASSED + FAILED)) -gt 0 ]; then
432 PASS_RATE=$((PASSED * 100 / (PASSED + FAILED)))
433 printf "Pass rate: %d%%\n" "$PASS_RATE"
434 fi
435
436 if [ "$FAILED" -gt 0 ]; then
437 printf "\n${RED}Failed tests:${NC}\n"
438 printf "%b" "$FAILED_TESTS_LIST"
439 printf "==========================================\n"
440 fi
441
442 if [ "$FAILED" -eq 0 ]; then
443 printf "${GREEN}ALL POSIX COMPLIANCE TESTS PASSED!${NC} ✓\n"
444 exit 0
445 else
446 printf "${RED}SOME TESTS FAILED${NC} ✗\n"
447 exit 1
448 fi