| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Special Parameters Gap Tests |
| 4 | # ===================================== |
| 5 | # Tests for POSIX special parameters and expansion |
| 6 | # Split from posix_compliance_gaps.sh for better organization |
| 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="[gaps-special]" |
| 17 | CURRENT_SECTION="" |
| 18 | TEST_NUM=0 |
| 19 | |
| 20 | PASSED=0 |
| 21 | FAILED=0 |
| 22 | SKIPPED=0 |
| 23 | FAILED_TESTS_LIST="" |
| 24 | DEBUG_INFO="" |
| 25 | |
| 26 | # Get script directory (POSIX way) |
| 27 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) |
| 28 | SHELL_BIN="${SHELL_BIN:?ERROR: SHELL_BIN must be set}" |
| 29 | BASH_REF="${BASH_REF:-bash}" |
| 30 | |
| 31 | # Check if shell binary exists |
| 32 | if [ ! -x "$SHELL_BIN" ]; then |
| 33 | printf "${RED}ERROR${NC}: shell binary not found at $SHELL_BIN\n" |
| 34 | printf "Please set SHELL_BIN or set SHELL_BIN environment variable\n" |
| 35 | exit 1 |
| 36 | fi |
| 37 | |
| 38 | pass() { |
| 39 | TEST_NUM=$((TEST_NUM + 1)) |
| 40 | printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 41 | PASSED=$((PASSED + 1)) |
| 42 | } |
| 43 | |
| 44 | fail() { |
| 45 | TEST_NUM=$((TEST_NUM + 1)) |
| 46 | TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}" |
| 47 | printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1" |
| 48 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n" |
| 49 | if [ -n "$2" ]; then printf " posix: %s\n" "$2"; fi |
| 50 | if [ -n "$3" ]; then printf " shell: %s\n" "$3"; fi |
| 51 | FAILED=$((FAILED + 1)) |
| 52 | } |
| 53 | |
| 54 | section() { |
| 55 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 56 | TEST_NUM=0 |
| 57 | printf "\n${BLUE}==========================================\n%s\n==========================================${NC}\n" "$1" |
| 58 | } |
| 59 | |
| 60 | normalize_output() { sed -e 's|^[^ ]*/[a-z]*sh[0-9]*: |sh: |' -e 's|^[a-z]*sh[0-9]*: |sh: |' -e 's/line [0-9]*: //'; } |
| 61 | |
| 62 | compare_posix_output() { |
| 63 | test_name="$1"; command="$2" |
| 64 | posix_out=$("$BASH_REF" -c "$command" 2>&1 | normalize_output) |
| 65 | shell_out=$("$SHELL_BIN" -c "$command" 2>&1 | normalize_output) |
| 66 | if [ "$posix_out" = "$shell_out" ]; then pass "$test_name" |
| 67 | else |
| 68 | fail "$test_name" "$posix_out" "$shell_out" |
| 69 | # Accumulate debug for CI summary |
| 70 | DEBUG_INFO="${DEBUG_INFO}DEBUG [$test_name]: cmd='$command'\n" |
| 71 | DEBUG_INFO="${DEBUG_INFO} bash: '${posix_out}' hex=$(printf '%s' "$posix_out" | od -A x -t x1z | head -1)\n" |
| 72 | DEBUG_INFO="${DEBUG_INFO} shell: '${shell_out}' hex=$(printf '%s' "$shell_out" | od -A x -t x1z | head -1)\n" |
| 73 | fi |
| 74 | } |
| 75 | |
| 76 | # ============================================================================ |
| 77 | # SPECIAL PARAMETERS |
| 78 | # ============================================================================ |
| 79 | |
| 80 | section "1. POSITIONAL PARAMETERS" |
| 81 | compare_posix_output "dollar at" 'set -- a b c; echo "$@"' |
| 82 | compare_posix_output "dollar star" 'set -- a b c; echo "$*"' |
| 83 | compare_posix_output "dollar hash" 'set -- a b c; echo $#' |
| 84 | compare_posix_output "dollar at with IFS" "IFS=:; set -- a b c; echo \"\$*\"" |
| 85 | compare_posix_output "dollar star vs at unquoted" "set -- a b c; for x in \$*; do echo \$x; done | wc -l" |
| 86 | compare_posix_output "dollar at quoted iteration" "set -- 'a b' 'c d'; for x in \"\$@\"; do echo \$x; done | wc -l" |
| 87 | compare_posix_output "dollar hash after shift" "set -- a b c; shift; echo \$#" |
| 88 | |
| 89 | section "2. SHELL STATUS PARAMETERS" |
| 90 | compare_posix_output "dollar question" 'true; echo $?' |
| 91 | compare_posix_output "dollar pid" 'echo $$ | grep -cE "^[0-9]+$"' |
| 92 | compare_posix_output "dollar zero" 'echo $0 | grep -c .' |
| 93 | compare_posix_output "dollar dash shows options" "echo \$- | grep -c '[a-z]'" |
| 94 | compare_posix_output "dollar dollar is numeric" "echo \$\$ | grep -c '^[0-9]*\$'" |
| 95 | compare_posix_output "dollar zero is set" "echo \${0:-none} | grep -c '.'" |
| 96 | |
| 97 | # ============================================================================ |
| 98 | # PARAMETER EXPANSION |
| 99 | # ============================================================================ |
| 100 | |
| 101 | section "3. DEFAULT VALUES" |
| 102 | compare_posix_output "pe default" 'unset x; echo ${x:-default}' |
| 103 | compare_posix_output "pe assign" 'unset x; echo ${x:=assigned}; echo $x' |
| 104 | compare_posix_output "pe error" '(unset x; echo ${x:?msg}) 2>/dev/null; echo $?' |
| 105 | compare_posix_output "pe alt" 'x=val; echo ${x:+alt}' |
| 106 | compare_posix_output "default empty" 'x=""; echo ${x:-default}' |
| 107 | compare_posix_output "default unset" 'unset x; echo ${x:-default}' |
| 108 | compare_posix_output "default set" 'x="value"; echo ${x:-default}' |
| 109 | |
| 110 | section "4. STRING LENGTH" |
| 111 | compare_posix_output "pe length" 'x=hello; echo ${#x}' |
| 112 | compare_posix_output "length of empty" 'x=""; echo ${#x}' |
| 113 | compare_posix_output "length of one" 'x="a"; echo ${#x}' |
| 114 | compare_posix_output "length of special" 'x="a b c"; echo ${#x}' |
| 115 | |
| 116 | section "5. PATTERN REMOVAL" |
| 117 | compare_posix_output "pe suffix" 'x=file.txt; echo ${x%.txt}' |
| 118 | compare_posix_output "pe prefix" 'x=/path/file; echo ${x##*/}' |
| 119 | compare_posix_output "suffix remove" 'x="file.txt"; echo ${x%.txt}' |
| 120 | compare_posix_output "prefix remove" 'x="prefix_name"; echo ${x#prefix_}' |
| 121 | compare_posix_output "longest suffix" 'x="a.b.c"; echo ${x%%.*}' |
| 122 | compare_posix_output "longest prefix" 'x="a.b.c"; echo ${x##*.}' |
| 123 | |
| 124 | section "6. NESTED EXPANSION" |
| 125 | compare_posix_output "nested default" "unset A; B=inner; echo \${A:-\${B}}" |
| 126 | compare_posix_output "nested length" "VAR=hello; echo \${#VAR}" |
| 127 | compare_posix_output "nested pattern removal" "VAR=/usr/local/bin; echo \${VAR#\${VAR%/*}}" |
| 128 | compare_posix_output "multiple expansions" "A=foo; B=bar; echo \${A}\${B}" |
| 129 | compare_posix_output "nested with quotes" "A='a b'; echo \"\${A}\"" |
| 130 | |
| 131 | # ============================================================================ |
| 132 | # IFS SPLITTING |
| 133 | # ============================================================================ |
| 134 | |
| 135 | section "7. IFS SPLITTING" |
| 136 | compare_posix_output "ifs default" 'x="a b c"; set -- $x; echo $#' |
| 137 | compare_posix_output "ifs colon" 'IFS=:; x="a:b:c"; set -- $x; echo $#' |
| 138 | compare_posix_output "ifs empty" 'IFS=""; x="abc"; set -- $x; echo $#' |
| 139 | compare_posix_output "ifs star" 'IFS=:; set -- a b c; echo "$*"' |
| 140 | |
| 141 | # ============================================================================ |
| 142 | # COMMAND SUBSTITUTION |
| 143 | # ============================================================================ |
| 144 | |
| 145 | section "8. COMMAND SUBSTITUTION" |
| 146 | compare_posix_output "cmdsub basic" 'echo $(echo hello)' |
| 147 | compare_posix_output "cmdsub backtick" 'echo `echo hello`' |
| 148 | compare_posix_output "cmdsub nested" 'echo $(echo $(echo deep))' |
| 149 | compare_posix_output "cmdsub quoted" 'echo "$(echo hello world)"' |
| 150 | compare_posix_output "cmdsub multi" 'echo $(echo a; echo b)' |
| 151 | |
| 152 | section "9. COMMAND SUBSTITUTION VARIANTS" |
| 153 | compare_posix_output "cmd sub echo" 'echo $(echo hello)' |
| 154 | compare_posix_output "cmd sub pwd" 'echo $(pwd | grep -c "/")' |
| 155 | compare_posix_output "cmd sub math" 'echo $(($(echo 5) + 3))' |
| 156 | compare_posix_output "cmd sub in var" 'x=$(echo test); echo $x' |
| 157 | compare_posix_output "backtick equiv" 'echo `echo hello`' |
| 158 | compare_posix_output "cmd sub whitespace" 'echo "$(echo " spaces ")"' |
| 159 | compare_posix_output "cmd sub multiline" 'echo "$(echo -e "a\nb")" | wc -l' |
| 160 | compare_posix_output "cmd sub exit code" '$(exit 0); echo $?' |
| 161 | compare_posix_output "cmd sub fail code" '$(exit 1); echo $?' |
| 162 | |
| 163 | # ============================================================================ |
| 164 | # TILDE EXPANSION |
| 165 | # ============================================================================ |
| 166 | |
| 167 | section "10. TILDE EXPANSION" |
| 168 | compare_posix_output "tilde in assignment" "VAR=~/test; echo \$VAR | grep -c '^/'" |
| 169 | compare_posix_output "tilde in middle no expand" "echo a~b | grep -c '~'" |
| 170 | |
| 171 | # ============================================================================ |
| 172 | # VARIABLE ASSIGNMENT CONTEXTS |
| 173 | # ============================================================================ |
| 174 | |
| 175 | section "11. VARIABLE ASSIGNMENT" |
| 176 | compare_posix_output "simple assign" 'x=5; echo $x' |
| 177 | compare_posix_output "multi assign" 'x=1 y=2 z=3; echo $x $y $z' |
| 178 | compare_posix_output "assign in subshell" '(x=inner; echo $x); echo ${x:-unset}' |
| 179 | compare_posix_output "assign export" 'export X=exported; printenv X 2>/dev/null || echo $X' |
| 180 | compare_posix_output "assign readonly" 'readonly X=constant; echo $X' |
| 181 | compare_posix_output "assign with cmd" 'X=$(echo value); echo $X' |
| 182 | compare_posix_output "assign quoted" 'X="with spaces"; echo "$X"' |
| 183 | compare_posix_output "assign empty" 'X=""; echo "[$X]"' |
| 184 | compare_posix_output "assign special" 'X="$HOME"; echo ${X:+set}' |
| 185 | |
| 186 | # ============================================================================ |
| 187 | # PIPELINES |
| 188 | # ============================================================================ |
| 189 | |
| 190 | section "12. PIPELINES" |
| 191 | compare_posix_output "pipe two" 'echo hello | cat' |
| 192 | compare_posix_output "pipe three" 'echo hello | cat | cat' |
| 193 | compare_posix_output "pipe with grep" 'echo hello | grep -o h' |
| 194 | compare_posix_output "pipe word count" 'echo "a b c" | wc -w' |
| 195 | compare_posix_output "pipe line count" 'printf "a\nb\nc\n" | wc -l' |
| 196 | compare_posix_output "pipe sort" 'printf "c\na\nb\n" | sort | head -1' |
| 197 | compare_posix_output "pipe uniq" 'printf "a\na\nb\n" | uniq | wc -l' |
| 198 | compare_posix_output "pipe head" 'printf "1\n2\n3\n4\n5\n" | head -2' |
| 199 | compare_posix_output "pipe tail" 'printf "1\n2\n3\n4\n5\n" | tail -2' |
| 200 | compare_posix_output "pipe tr" 'echo abc | tr a-z A-Z' |
| 201 | |
| 202 | # ============================================================================ |
| 203 | # COMMAND NAME RESOLUTION |
| 204 | # ============================================================================ |
| 205 | |
| 206 | section "13. COMMAND NAME RESOLUTION" |
| 207 | compare_posix_output "function overrides echo" "echo() { printf 'function\n'; }; echo test | grep -c function" |
| 208 | compare_posix_output "command -v finds function" "func() { :; }; command -v func | grep -c func" |
| 209 | compare_posix_output "command bypasses function" "echo() { printf 'func\n'; }; command echo test" |
| 210 | |
| 211 | # Summary |
| 212 | printf "\n==========================================\n" |
| 213 | printf "SPECIAL PARAMETERS GAP TEST RESULTS\n" |
| 214 | printf "==========================================\n" |
| 215 | printf "${GREEN}Passed:${NC} %d\n" "$PASSED" |
| 216 | printf "${RED}Failed:${NC} %d\n" "$FAILED" |
| 217 | printf "Total: %d\n" "$((PASSED + FAILED))" |
| 218 | if [ "$FAILED" -gt 0 ]; then |
| 219 | printf "\n${RED}Failed tests:${NC}\n%b" "$FAILED_TESTS_LIST" |
| 220 | if [ -n "$DEBUG_INFO" ]; then |
| 221 | printf "\n${YELLOW}Debug info:${NC}\n%b" "$DEBUG_INFO" |
| 222 | fi |
| 223 | exit 1 |
| 224 | fi |
| 225 | exit 0 |