| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Compliance Character Class Test Suite for shell |
| 4 | # ===================================== |
| 5 | # Tests POSIX bracket expression character classes per IEEE Std 1003.1-2017 |
| 6 | # Section 9.3.5 RE Bracket Expression |
| 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-charclass]" |
| 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 | SHELL_BIN="${SHELL_BIN:?ERROR: SHELL_BIN must be set}" |
| 28 | |
| 29 | # Check if shell binary exists |
| 30 | if [ ! -x "$SHELL_BIN" ]; then |
| 31 | printf "${RED}ERROR${NC}: shell binary not found at $SHELL_BIN\n" |
| 32 | printf "Please set SHELL_BIN or set SHELL_BIN environment variable\n" |
| 33 | exit 1 |
| 34 | fi |
| 35 | |
| 36 | # Test result trackers |
| 37 | pass() { |
| 38 | TEST_NUM=$((TEST_NUM + 1)) |
| 39 | printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 40 | PASSED=$((PASSED + 1)) |
| 41 | } |
| 42 | |
| 43 | fail() { |
| 44 | TEST_NUM=$((TEST_NUM + 1)) |
| 45 | TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}" |
| 46 | printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1" |
| 47 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n" |
| 48 | if [ -n "$2" ]; then |
| 49 | printf " expected: %s\n" "$2" |
| 50 | fi |
| 51 | if [ -n "$3" ]; then |
| 52 | printf " got: %s\n" "$3" |
| 53 | fi |
| 54 | FAILED=$((FAILED + 1)) |
| 55 | } |
| 56 | |
| 57 | skip() { |
| 58 | TEST_NUM=$((TEST_NUM + 1)) |
| 59 | printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s - %s\n" "$1" "$2" |
| 60 | SKIPPED=$((SKIPPED + 1)) |
| 61 | } |
| 62 | |
| 63 | section() { |
| 64 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 65 | TEST_NUM=0 |
| 66 | printf "\n" |
| 67 | printf "${BLUE}==========================================\n" |
| 68 | printf "%s\n" "$1" |
| 69 | printf "==========================================${NC}\n" |
| 70 | } |
| 71 | |
| 72 | # Create test directory with test files |
| 73 | setup_test_files() { |
| 74 | TEST_DIR="/tmp/bensch_charclass_$$" |
| 75 | mkdir -p "$TEST_DIR" |
| 76 | cd "$TEST_DIR" || exit 1 |
| 77 | |
| 78 | # Create files for character class testing |
| 79 | touch "file1.txt" "file2.txt" "file3.txt" |
| 80 | touch "FileA.txt" "FileB.txt" "FileC.txt" |
| 81 | touch "data_01.csv" "data_02.csv" "data_99.csv" |
| 82 | touch "test-file" "test_file" "test.file" |
| 83 | touch "UPPER.TXT" "lower.txt" "MiXeD.TxT" |
| 84 | touch "a1b2c3" "xyz789" "ABC123" |
| 85 | touch "file with space.txt" |
| 86 | touch ".hidden" ".dotfile" |
| 87 | touch "special!file" "special@file" |
| 88 | } |
| 89 | |
| 90 | cleanup_test_files() { |
| 91 | cd / |
| 92 | rm -rf "$TEST_DIR" |
| 93 | } |
| 94 | |
| 95 | # Trap to ensure cleanup |
| 96 | trap cleanup_test_files EXIT |
| 97 | |
| 98 | setup_test_files |
| 99 | |
| 100 | # ===================================== |
| 101 | section "341. POSIX CHARACTER CLASS [:alpha:]" |
| 102 | # ===================================== |
| 103 | |
| 104 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [[:alpha:]]*' 2>&1) |
| 105 | # Should match files starting with alphabetic characters |
| 106 | if echo "$result" | grep -q "file1.txt\|FileA.txt"; then |
| 107 | pass "[:alpha:] matches alphabetic characters" |
| 108 | else |
| 109 | fail "[:alpha:] matches alphabetic characters" "files starting with letters" "$result" |
| 110 | fi |
| 111 | |
| 112 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && for f in [[:alpha:]]*.txt; do echo "$f"; done | head -1' 2>&1) |
| 113 | if [ -n "$result" ] && [ "$result" != '[[:alpha:]]*.txt' ]; then |
| 114 | pass "[:alpha:] in loop context" |
| 115 | else |
| 116 | fail "[:alpha:] in loop context" "matched files" "$result" |
| 117 | fi |
| 118 | |
| 119 | # ===================================== |
| 120 | section "342. POSIX CHARACTER CLASS [:digit:]" |
| 121 | # ===================================== |
| 122 | |
| 123 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo data_[[:digit:]]*.csv' 2>&1) |
| 124 | if echo "$result" | grep -q "data_0"; then |
| 125 | pass "[:digit:] matches numeric characters" |
| 126 | else |
| 127 | fail "[:digit:] matches numeric characters" "data_0*.csv files" "$result" |
| 128 | fi |
| 129 | |
| 130 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo *[[:digit:]][[:digit:]].csv' 2>&1) |
| 131 | if echo "$result" | grep -q "data_"; then |
| 132 | pass "[:digit:][:digit:] matches two digits" |
| 133 | else |
| 134 | fail "[:digit:][:digit:] matches two digits" "files ending in two digits" "$result" |
| 135 | fi |
| 136 | |
| 137 | # ===================================== |
| 138 | section "343. POSIX CHARACTER CLASS [:alnum:]" |
| 139 | # ===================================== |
| 140 | |
| 141 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [[:alnum:]]*.txt' 2>&1) |
| 142 | if echo "$result" | grep -q "file"; then |
| 143 | pass "[:alnum:] matches alphanumeric characters" |
| 144 | else |
| 145 | fail "[:alnum:] matches alphanumeric characters" "alphanumeric files" "$result" |
| 146 | fi |
| 147 | |
| 148 | # ===================================== |
| 149 | section "344. POSIX CHARACTER CLASS [:upper:]" |
| 150 | # ===================================== |
| 151 | |
| 152 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [[:upper:]]*.TXT' 2>&1) |
| 153 | if echo "$result" | grep -q "UPPER.TXT\|File"; then |
| 154 | pass "[:upper:] matches uppercase characters" |
| 155 | else |
| 156 | fail "[:upper:] matches uppercase characters" "uppercase files" "$result" |
| 157 | fi |
| 158 | |
| 159 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [[:upper:]][[:upper:]][[:upper:]]*.TXT' 2>&1) |
| 160 | if echo "$result" | grep -q "UPPER.TXT\|ABC"; then |
| 161 | pass "[:upper:] repeated matches multiple uppercase" |
| 162 | else |
| 163 | fail "[:upper:] repeated matches multiple uppercase" "all-caps files" "$result" |
| 164 | fi |
| 165 | |
| 166 | # ===================================== |
| 167 | section "345. POSIX CHARACTER CLASS [:lower:]" |
| 168 | # ===================================== |
| 169 | |
| 170 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [[:lower:]]*.txt' 2>&1) |
| 171 | if echo "$result" | grep -q "file\|lower"; then |
| 172 | pass "[:lower:] matches lowercase characters" |
| 173 | else |
| 174 | fail "[:lower:] matches lowercase characters" "lowercase files" "$result" |
| 175 | fi |
| 176 | |
| 177 | # ===================================== |
| 178 | section "346. POSIX CHARACTER CLASS [:space:]" |
| 179 | # ===================================== |
| 180 | |
| 181 | # [:space:] in variable content |
| 182 | result=$("$SHELL_BIN" -c 'x="hello world"; case "$x" in *[[:space:]]*) echo "has space";; esac' 2>&1) |
| 183 | if [ "$result" = "has space" ]; then |
| 184 | pass "[:space:] matches space in case pattern" |
| 185 | else |
| 186 | fail "[:space:] matches space in case pattern" "has space" "$result" |
| 187 | fi |
| 188 | |
| 189 | # ===================================== |
| 190 | section "347. POSIX CHARACTER CLASS [:blank:]" |
| 191 | # ===================================== |
| 192 | |
| 193 | # [:blank:] matches space and tab only |
| 194 | result=$("$SHELL_BIN" -c 'x="a b"; case "$x" in *[[:blank:]]*) echo "has blank";; esac' 2>&1) |
| 195 | if [ "$result" = "has blank" ]; then |
| 196 | pass "[:blank:] matches tab character" |
| 197 | else |
| 198 | fail "[:blank:] matches tab character" "has blank" "$result" |
| 199 | fi |
| 200 | |
| 201 | # ===================================== |
| 202 | section "348. POSIX CHARACTER CLASS [:xdigit:]" |
| 203 | # ===================================== |
| 204 | |
| 205 | result=$("$SHELL_BIN" -c 'case "a1b2c3" in [[:xdigit:]]*) echo "starts with hex";; esac' 2>&1) |
| 206 | if [ "$result" = "starts with hex" ]; then |
| 207 | pass "[:xdigit:] matches hexadecimal digits" |
| 208 | else |
| 209 | fail "[:xdigit:] matches hexadecimal digits" "starts with hex" "$result" |
| 210 | fi |
| 211 | |
| 212 | result=$("$SHELL_BIN" -c 'case "DEADBEEF" in [[:xdigit:]]*) echo "valid hex";; esac' 2>&1) |
| 213 | if [ "$result" = "valid hex" ]; then |
| 214 | pass "[:xdigit:] matches uppercase hex" |
| 215 | else |
| 216 | fail "[:xdigit:] matches uppercase hex" "valid hex" "$result" |
| 217 | fi |
| 218 | |
| 219 | # ===================================== |
| 220 | section "349. POSIX CHARACTER CLASS [:punct:]" |
| 221 | # ===================================== |
| 222 | |
| 223 | result=$("$SHELL_BIN" -c 'case "hello!" in *[[:punct:]]) echo "ends with punct";; esac' 2>&1) |
| 224 | if [ "$result" = "ends with punct" ]; then |
| 225 | pass "[:punct:] matches punctuation" |
| 226 | else |
| 227 | fail "[:punct:] matches punctuation" "ends with punct" "$result" |
| 228 | fi |
| 229 | |
| 230 | result=$("$SHELL_BIN" -c 'case "@#$%" in [[:punct:]]*) echo "starts with punct";; esac' 2>&1) |
| 231 | if [ "$result" = "starts with punct" ]; then |
| 232 | pass "[:punct:] matches special characters" |
| 233 | else |
| 234 | fail "[:punct:] matches special characters" "starts with punct" "$result" |
| 235 | fi |
| 236 | |
| 237 | # ===================================== |
| 238 | section "350. POSIX CHARACTER CLASS [:print:]" |
| 239 | # ===================================== |
| 240 | |
| 241 | result=$("$SHELL_BIN" -c 'case "hello" in [[:print:]]*) echo "printable";; esac' 2>&1) |
| 242 | if [ "$result" = "printable" ]; then |
| 243 | pass "[:print:] matches printable characters" |
| 244 | else |
| 245 | fail "[:print:] matches printable characters" "printable" "$result" |
| 246 | fi |
| 247 | |
| 248 | # ===================================== |
| 249 | section "351. POSIX CHARACTER CLASS [:graph:]" |
| 250 | # ===================================== |
| 251 | |
| 252 | result=$("$SHELL_BIN" -c 'case "abc123" in [[:graph:]]*) echo "graphical";; esac' 2>&1) |
| 253 | if [ "$result" = "graphical" ]; then |
| 254 | pass "[:graph:] matches graphical characters" |
| 255 | else |
| 256 | fail "[:graph:] matches graphical characters" "graphical" "$result" |
| 257 | fi |
| 258 | |
| 259 | # ===================================== |
| 260 | section "352. POSIX BRACKET NEGATION [^...] and [!...]" |
| 261 | # ===================================== |
| 262 | |
| 263 | # Note: POSIX uses [!...] for negation, [^...] is bash extension |
| 264 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [!A-Z]*.txt' 2>&1) |
| 265 | if echo "$result" | grep -q "file\|lower"; then |
| 266 | pass "[!...] negation matches non-uppercase (POSIX standard)" |
| 267 | else |
| 268 | fail "[!...] negation matches non-uppercase (POSIX standard)" "lowercase files" "$result" |
| 269 | fi |
| 270 | |
| 271 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [!0-9]*.txt' 2>&1) |
| 272 | if echo "$result" | grep -q "file\|File"; then |
| 273 | pass "[!...] negation matches non-digits" |
| 274 | else |
| 275 | fail "[!...] negation matches non-digits" "non-digit files" "$result" |
| 276 | fi |
| 277 | |
| 278 | # ===================================== |
| 279 | section "353. POSIX RANGE EXPRESSIONS" |
| 280 | # ===================================== |
| 281 | |
| 282 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo file[1-3].txt' 2>&1) |
| 283 | if echo "$result" | grep -q "file1.txt\|file2.txt\|file3.txt"; then |
| 284 | pass "[1-3] range matches digits 1-3" |
| 285 | else |
| 286 | fail "[1-3] range matches digits 1-3" "file1-3.txt" "$result" |
| 287 | fi |
| 288 | |
| 289 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo File[A-C].txt' 2>&1) |
| 290 | if echo "$result" | grep -q "FileA.txt\|FileB.txt\|FileC.txt"; then |
| 291 | pass "[A-C] range matches uppercase A-C" |
| 292 | else |
| 293 | fail "[A-C] range matches uppercase A-C" "FileA-C.txt" "$result" |
| 294 | fi |
| 295 | |
| 296 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [a-z]*.txt' 2>&1) |
| 297 | if echo "$result" | grep -q "file\|lower"; then |
| 298 | pass "[a-z] range matches lowercase" |
| 299 | else |
| 300 | fail "[a-z] range matches lowercase" "lowercase files" "$result" |
| 301 | fi |
| 302 | |
| 303 | # ===================================== |
| 304 | section "354. COMBINED CHARACTER CLASSES" |
| 305 | # ===================================== |
| 306 | |
| 307 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [[:alpha:][:digit:]]*.txt' 2>&1) |
| 308 | if echo "$result" | grep -q "file\|File"; then |
| 309 | pass "[[:alpha:][:digit:]] combined classes" |
| 310 | else |
| 311 | fail "[[:alpha:][:digit:]] combined classes" "alphanumeric start" "$result" |
| 312 | fi |
| 313 | |
| 314 | result=$("$SHELL_BIN" -c 'case "test123" in [[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]][[:digit:]][[:digit:]][[:digit:]]) echo "matched";; esac' 2>&1) |
| 315 | if [ "$result" = "matched" ]; then |
| 316 | pass "Combined classes in exact pattern" |
| 317 | else |
| 318 | fail "Combined classes in exact pattern" "matched" "$result" |
| 319 | fi |
| 320 | |
| 321 | # ===================================== |
| 322 | section "355. CHARACTER CLASS IN CASE STATEMENTS" |
| 323 | # ===================================== |
| 324 | |
| 325 | result=$("$SHELL_BIN" -c ' |
| 326 | for word in hello WORLD 123 test!; do |
| 327 | case "$word" in |
| 328 | [[:upper:]]*) echo "$word: upper" ;; |
| 329 | [[:lower:]]*) echo "$word: lower" ;; |
| 330 | [[:digit:]]*) echo "$word: digit" ;; |
| 331 | *) echo "$word: other" ;; |
| 332 | esac |
| 333 | done |
| 334 | ' 2>&1) |
| 335 | if echo "$result" | grep -q "hello: lower" && echo "$result" | grep -q "WORLD: upper" && echo "$result" | grep -q "123: digit"; then |
| 336 | pass "Character classes in case statement" |
| 337 | else |
| 338 | fail "Character classes in case statement" "categorized output" "$result" |
| 339 | fi |
| 340 | |
| 341 | # ===================================== |
| 342 | section "356. CHARACTER CLASS EDGE CASES" |
| 343 | # ===================================== |
| 344 | |
| 345 | # Literal hyphen at start |
| 346 | result=$("$SHELL_BIN" -c 'case "-test" in [-a]*) echo "matched";; esac' 2>&1) |
| 347 | if [ "$result" = "matched" ]; then |
| 348 | pass "Literal hyphen at bracket start" |
| 349 | else |
| 350 | fail "Literal hyphen at bracket start" "matched" "$result" |
| 351 | fi |
| 352 | |
| 353 | # Literal bracket |
| 354 | result=$("$SHELL_BIN" -c 'case "[test]" in \[*) echo "matched";; esac' 2>&1) |
| 355 | if [ "$result" = "matched" ]; then |
| 356 | pass "Escaped bracket in pattern" |
| 357 | else |
| 358 | fail "Escaped bracket in pattern" "matched" "$result" |
| 359 | fi |
| 360 | |
| 361 | # Empty class should not match |
| 362 | result=$("$SHELL_BIN" -c 'case "test" in []) echo "empty";; *) echo "star";; esac' 2>&1) |
| 363 | if [ "$result" = "star" ]; then |
| 364 | pass "Empty bracket expression fallthrough" |
| 365 | else |
| 366 | skip "Empty bracket expression fallthrough" "implementation varies" |
| 367 | fi |
| 368 | |
| 369 | # ===================================== |
| 370 | section "357. CHARACTER CLASS WITH GLOB PATTERNS" |
| 371 | # ===================================== |
| 372 | |
| 373 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo *[[:digit:]].*' 2>&1) |
| 374 | if echo "$result" | grep -q "data_0\|file"; then |
| 375 | pass "Character class with glob wildcards" |
| 376 | else |
| 377 | fail "Character class with glob wildcards" "digit-containing files" "$result" |
| 378 | fi |
| 379 | |
| 380 | result=$("$SHELL_BIN" -c 'cd '"$TEST_DIR"' && echo [[:alpha:]]?[[:alpha:]]?[[:alpha:]]*.txt' 2>&1) |
| 381 | if echo "$result" | grep -q "file\|File\|lower"; then |
| 382 | pass "Character class with ? wildcards" |
| 383 | else |
| 384 | fail "Character class with ? wildcards" "pattern matched files" "$result" |
| 385 | fi |
| 386 | |
| 387 | # ===================================== |
| 388 | # Summary |
| 389 | # ===================================== |
| 390 | printf "\n" |
| 391 | printf "${BLUE}==========================================\n" |
| 392 | printf "POSIX Character Class Test Summary\n" |
| 393 | printf "==========================================${NC}\n" |
| 394 | printf "Passed: ${GREEN}%d${NC}\n" "$PASSED" |
| 395 | printf "Failed: ${RED}%d${NC}\n" "$FAILED" |
| 396 | printf "Skipped: ${YELLOW}%d${NC}\n" "$SKIPPED" |
| 397 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 398 | |
| 399 | if [ -n "$FAILED_TESTS_LIST" ]; then |
| 400 | printf "\n${RED}Failed tests:${NC}\n" |
| 401 | printf "%b" "$FAILED_TESTS_LIST" |
| 402 | fi |
| 403 | |
| 404 | if [ "$FAILED" -gt 0 ]; then |
| 405 | exit 1 |
| 406 | fi |
| 407 | exit 0 |