Bash · 13575 bytes Raw Blame History
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