| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Compliance printf Builtin Test Suite for shell |
| 4 | # ===================================== |
| 5 | # Tests the printf builtin command per IEEE Std 1003.1-2017 |
| 6 | # Section: Shell & Utilities - printf |
| 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-printf]" |
| 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 | # ===================================== |
| 73 | section "358. PRINTF BASIC STRING FORMAT %s" |
| 74 | # ===================================== |
| 75 | |
| 76 | result=$("$SHELL_BIN" -c 'printf "%s\n" "hello"' 2>&1) |
| 77 | if [ "$result" = "hello" ]; then |
| 78 | pass "printf %s basic string" |
| 79 | else |
| 80 | fail "printf %s basic string" "hello" "$result" |
| 81 | fi |
| 82 | |
| 83 | result=$("$SHELL_BIN" -c 'printf "%s %s\n" "hello" "world"' 2>&1) |
| 84 | if [ "$result" = "hello world" ]; then |
| 85 | pass "printf %s multiple arguments" |
| 86 | else |
| 87 | fail "printf %s multiple arguments" "hello world" "$result" |
| 88 | fi |
| 89 | |
| 90 | result=$("$SHELL_BIN" -c 'printf "%s" ""' 2>&1) |
| 91 | if [ "$result" = "" ]; then |
| 92 | pass "printf %s empty string" |
| 93 | else |
| 94 | fail "printf %s empty string" "(empty)" "$result" |
| 95 | fi |
| 96 | |
| 97 | result=$("$SHELL_BIN" -c 'printf "%.3s\n" "hello"' 2>&1) |
| 98 | if [ "$result" = "hel" ]; then |
| 99 | pass "printf %.3s precision truncates" |
| 100 | else |
| 101 | fail "printf %.3s precision truncates" "hel" "$result" |
| 102 | fi |
| 103 | |
| 104 | result=$("$SHELL_BIN" -c 'printf "%10s\n" "hi"' 2>&1) |
| 105 | if [ "$result" = " hi" ]; then |
| 106 | pass "printf %10s width right-align" |
| 107 | else |
| 108 | fail "printf %10s width right-align" " hi" "$result" |
| 109 | fi |
| 110 | |
| 111 | result=$("$SHELL_BIN" -c 'printf "%-10s|\n" "hi"' 2>&1) |
| 112 | if [ "$result" = "hi |" ]; then |
| 113 | pass "printf %-10s width left-align" |
| 114 | else |
| 115 | fail "printf %-10s width left-align" "hi |" "$result" |
| 116 | fi |
| 117 | |
| 118 | # ===================================== |
| 119 | section "359. PRINTF INTEGER FORMATS %d %i" |
| 120 | # ===================================== |
| 121 | |
| 122 | result=$("$SHELL_BIN" -c 'printf "%d\n" 42' 2>&1) |
| 123 | if [ "$result" = "42" ]; then |
| 124 | pass "printf %d basic decimal" |
| 125 | else |
| 126 | fail "printf %d basic decimal" "42" "$result" |
| 127 | fi |
| 128 | |
| 129 | result=$("$SHELL_BIN" -c 'printf "%i\n" 42' 2>&1) |
| 130 | if [ "$result" = "42" ]; then |
| 131 | pass "printf %i basic integer" |
| 132 | else |
| 133 | fail "printf %i basic integer" "42" "$result" |
| 134 | fi |
| 135 | |
| 136 | result=$("$SHELL_BIN" -c 'printf "%d\n" -42' 2>&1) |
| 137 | if [ "$result" = "-42" ]; then |
| 138 | pass "printf %d negative number" |
| 139 | else |
| 140 | fail "printf %d negative number" "-42" "$result" |
| 141 | fi |
| 142 | |
| 143 | result=$("$SHELL_BIN" -c 'printf "%5d\n" 42' 2>&1) |
| 144 | if [ "$result" = " 42" ]; then |
| 145 | pass "printf %5d width padding" |
| 146 | else |
| 147 | fail "printf %5d width padding" " 42" "$result" |
| 148 | fi |
| 149 | |
| 150 | result=$("$SHELL_BIN" -c 'printf "%05d\n" 42' 2>&1) |
| 151 | if [ "$result" = "00042" ]; then |
| 152 | pass "printf %05d zero padding" |
| 153 | else |
| 154 | fail "printf %05d zero padding" "00042" "$result" |
| 155 | fi |
| 156 | |
| 157 | result=$("$SHELL_BIN" -c 'printf "%-5d|\n" 42' 2>&1) |
| 158 | if [ "$result" = "42 |" ]; then |
| 159 | pass "printf %-5d left-align" |
| 160 | else |
| 161 | fail "printf %-5d left-align" "42 |" "$result" |
| 162 | fi |
| 163 | |
| 164 | result=$("$SHELL_BIN" -c 'printf "%+d\n" 42' 2>&1) |
| 165 | if [ "$result" = "+42" ]; then |
| 166 | pass "printf %+d explicit plus sign" |
| 167 | else |
| 168 | fail "printf %+d explicit plus sign" "+42" "$result" |
| 169 | fi |
| 170 | |
| 171 | # ===================================== |
| 172 | section "360. PRINTF OCTAL AND HEX FORMATS %o %x %X" |
| 173 | # ===================================== |
| 174 | |
| 175 | result=$("$SHELL_BIN" -c 'printf "%o\n" 8' 2>&1) |
| 176 | if [ "$result" = "10" ]; then |
| 177 | pass "printf %o octal format" |
| 178 | else |
| 179 | fail "printf %o octal format" "10" "$result" |
| 180 | fi |
| 181 | |
| 182 | result=$("$SHELL_BIN" -c 'printf "%x\n" 255' 2>&1) |
| 183 | if [ "$result" = "ff" ]; then |
| 184 | pass "printf %x lowercase hex" |
| 185 | else |
| 186 | fail "printf %x lowercase hex" "ff" "$result" |
| 187 | fi |
| 188 | |
| 189 | result=$("$SHELL_BIN" -c 'printf "%X\n" 255' 2>&1) |
| 190 | if [ "$result" = "FF" ]; then |
| 191 | pass "printf %X uppercase hex" |
| 192 | else |
| 193 | fail "printf %X uppercase hex" "FF" "$result" |
| 194 | fi |
| 195 | |
| 196 | result=$("$SHELL_BIN" -c 'printf "%#x\n" 255' 2>&1) |
| 197 | if [ "$result" = "0xff" ]; then |
| 198 | pass "printf %#x alternate form" |
| 199 | else |
| 200 | fail "printf %#x alternate form" "0xff" "$result" |
| 201 | fi |
| 202 | |
| 203 | result=$("$SHELL_BIN" -c 'printf "%#o\n" 8' 2>&1) |
| 204 | if [ "$result" = "010" ]; then |
| 205 | pass "printf %#o alternate octal form" |
| 206 | else |
| 207 | fail "printf %#o alternate octal form" "010" "$result" |
| 208 | fi |
| 209 | |
| 210 | # ===================================== |
| 211 | section "361. PRINTF CHARACTER FORMAT %c" |
| 212 | # ===================================== |
| 213 | |
| 214 | result=$("$SHELL_BIN" -c 'printf "%c\n" A' 2>&1) |
| 215 | if [ "$result" = "A" ]; then |
| 216 | pass "printf %c single character" |
| 217 | else |
| 218 | fail "printf %c single character" "A" "$result" |
| 219 | fi |
| 220 | |
| 221 | result=$("$SHELL_BIN" -c 'printf "%c\n" "hello"' 2>&1) |
| 222 | if [ "$result" = "h" ]; then |
| 223 | pass "printf %c first char of string" |
| 224 | else |
| 225 | fail "printf %c first char of string" "h" "$result" |
| 226 | fi |
| 227 | |
| 228 | result=$("$SHELL_BIN" -c 'printf "%c%c%c\n" a b c' 2>&1) |
| 229 | if [ "$result" = "abc" ]; then |
| 230 | pass "printf %c multiple chars" |
| 231 | else |
| 232 | fail "printf %c multiple chars" "abc" "$result" |
| 233 | fi |
| 234 | |
| 235 | # ===================================== |
| 236 | section "362. PRINTF ESCAPE SEQUENCES" |
| 237 | # ===================================== |
| 238 | |
| 239 | result=$("$SHELL_BIN" -c 'printf "hello\nworld\n"' 2>&1) |
| 240 | expected=$(printf "hello\nworld") |
| 241 | if [ "$result" = "$expected" ]; then |
| 242 | pass "printf \\n newline" |
| 243 | else |
| 244 | fail "printf \\n newline" "$expected" "$result" |
| 245 | fi |
| 246 | |
| 247 | result=$("$SHELL_BIN" -c 'printf "a\tb\n"' 2>&1) |
| 248 | expected=$(printf "a\tb") |
| 249 | if [ "$result" = "$expected" ]; then |
| 250 | pass "printf \\t tab" |
| 251 | else |
| 252 | fail "printf \\t tab" "$expected" "$result" |
| 253 | fi |
| 254 | |
| 255 | result=$("$SHELL_BIN" -c 'printf "back\\\\slash\n"' 2>&1) |
| 256 | if [ "$result" = 'back\slash' ]; then |
| 257 | pass "printf \\\\ literal backslash" |
| 258 | else |
| 259 | fail "printf \\\\ literal backslash" 'back\slash' "$result" |
| 260 | fi |
| 261 | |
| 262 | result=$("$SHELL_BIN" -c 'printf "100%%\n"' 2>&1) |
| 263 | if [ "$result" = "100%" ]; then |
| 264 | pass "printf %% literal percent" |
| 265 | else |
| 266 | fail "printf %% literal percent" "100%" "$result" |
| 267 | fi |
| 268 | |
| 269 | result=$("$SHELL_BIN" -c 'printf "\101\n"' 2>&1) |
| 270 | if [ "$result" = "A" ]; then |
| 271 | pass "printf \\NNN octal escape" |
| 272 | else |
| 273 | fail "printf \\NNN octal escape" "A" "$result" |
| 274 | fi |
| 275 | |
| 276 | # ===================================== |
| 277 | section "363. PRINTF %b ESCAPE INTERPRETATION" |
| 278 | # ===================================== |
| 279 | |
| 280 | result=$("$SHELL_BIN" -c 'printf "%b\n" "hello\nworld"' 2>&1) |
| 281 | expected=$(printf "hello\nworld") |
| 282 | if [ "$result" = "$expected" ]; then |
| 283 | pass "printf %b interprets backslash escapes" |
| 284 | else |
| 285 | fail "printf %b interprets backslash escapes" "(newline)" "$result" |
| 286 | fi |
| 287 | |
| 288 | result=$("$SHELL_BIN" -c 'printf "%b\n" "tab\there"' 2>&1) |
| 289 | expected=$(printf "tab\there") |
| 290 | if [ "$result" = "$expected" ]; then |
| 291 | pass "printf %b interprets \\t" |
| 292 | else |
| 293 | fail "printf %b interprets \\t" "(tab)" "$result" |
| 294 | fi |
| 295 | |
| 296 | # ===================================== |
| 297 | section "364. PRINTF FORMAT REUSE" |
| 298 | # ===================================== |
| 299 | |
| 300 | result=$("$SHELL_BIN" -c 'printf "%s\n" one two three' 2>&1) |
| 301 | expected=$(printf "one\ntwo\nthree") |
| 302 | if [ "$result" = "$expected" ]; then |
| 303 | pass "printf format reused for extra args" |
| 304 | else |
| 305 | fail "printf format reused for extra args" "$expected" "$result" |
| 306 | fi |
| 307 | |
| 308 | result=$("$SHELL_BIN" -c 'printf "%d " 1 2 3 4 5; printf "\n"' 2>&1) |
| 309 | if [ "$result" = "1 2 3 4 5 " ]; then |
| 310 | pass "printf %d reused for multiple integers" |
| 311 | else |
| 312 | fail "printf %d reused for multiple integers" "1 2 3 4 5 " "$result" |
| 313 | fi |
| 314 | |
| 315 | # ===================================== |
| 316 | section "365. PRINTF WITH VARIABLES" |
| 317 | # ===================================== |
| 318 | |
| 319 | result=$("$SHELL_BIN" -c 'x="hello"; printf "%s\n" "$x"' 2>&1) |
| 320 | if [ "$result" = "hello" ]; then |
| 321 | pass "printf with variable argument" |
| 322 | else |
| 323 | fail "printf with variable argument" "hello" "$result" |
| 324 | fi |
| 325 | |
| 326 | result=$("$SHELL_BIN" -c 'n=42; printf "Value: %d\n" "$n"' 2>&1) |
| 327 | if [ "$result" = "Value: 42" ]; then |
| 328 | pass "printf integer from variable" |
| 329 | else |
| 330 | fail "printf integer from variable" "Value: 42" "$result" |
| 331 | fi |
| 332 | |
| 333 | result=$("$SHELL_BIN" -c 'fmt="%s: %d\n"; printf "$fmt" name 42' 2>&1) |
| 334 | if [ "$result" = "name: 42" ]; then |
| 335 | pass "printf format from variable" |
| 336 | else |
| 337 | fail "printf format from variable" "name: 42" "$result" |
| 338 | fi |
| 339 | |
| 340 | # ===================================== |
| 341 | section "366. PRINTF DYNAMIC WIDTH AND PRECISION" |
| 342 | # ===================================== |
| 343 | |
| 344 | result=$("$SHELL_BIN" -c 'printf "%*s\n" 10 hi' 2>&1) |
| 345 | if [ "$result" = " hi" ]; then |
| 346 | pass "printf %*s dynamic width" |
| 347 | else |
| 348 | fail "printf %*s dynamic width" " hi" "$result" |
| 349 | fi |
| 350 | |
| 351 | result=$("$SHELL_BIN" -c 'printf "%.*s\n" 3 hello' 2>&1) |
| 352 | if [ "$result" = "hel" ]; then |
| 353 | pass "printf %.*s dynamic precision" |
| 354 | else |
| 355 | fail "printf %.*s dynamic precision" "hel" "$result" |
| 356 | fi |
| 357 | |
| 358 | result=$("$SHELL_BIN" -c 'printf "%*.*s\n" 10 3 hello' 2>&1) |
| 359 | if [ "$result" = " hel" ]; then |
| 360 | pass "printf %*.*s dynamic width and precision" |
| 361 | else |
| 362 | fail "printf %*.*s dynamic width and precision" " hel" "$result" |
| 363 | fi |
| 364 | |
| 365 | # ===================================== |
| 366 | section "367. PRINTF RETURN VALUE" |
| 367 | # ===================================== |
| 368 | |
| 369 | result=$("$SHELL_BIN" -c 'printf "%s" "test"; echo $?' 2>&1) |
| 370 | if echo "$result" | grep -q "0"; then |
| 371 | pass "printf returns 0 on success" |
| 372 | else |
| 373 | fail "printf returns 0 on success" "0" "$result" |
| 374 | fi |
| 375 | |
| 376 | # ===================================== |
| 377 | section "368. PRINTF EDGE CASES" |
| 378 | # ===================================== |
| 379 | |
| 380 | result=$("$SHELL_BIN" -c 'printf "%d\n" 0' 2>&1) |
| 381 | if [ "$result" = "0" ]; then |
| 382 | pass "printf %d zero" |
| 383 | else |
| 384 | fail "printf %d zero" "0" "$result" |
| 385 | fi |
| 386 | |
| 387 | result=$("$SHELL_BIN" -c 'printf "%s\n" ""' 2>&1) |
| 388 | if [ "$result" = "" ]; then |
| 389 | pass "printf empty argument" |
| 390 | else |
| 391 | fail "printf empty argument" "(empty)" "$result" |
| 392 | fi |
| 393 | |
| 394 | result=$("$SHELL_BIN" -c 'printf "no format"' 2>&1) |
| 395 | if [ "$result" = "no format" ]; then |
| 396 | pass "printf literal string no format" |
| 397 | else |
| 398 | fail "printf literal string no format" "no format" "$result" |
| 399 | fi |
| 400 | |
| 401 | result=$("$SHELL_BIN" -c 'printf ""' 2>&1) |
| 402 | if [ "$result" = "" ]; then |
| 403 | pass "printf empty format string" |
| 404 | else |
| 405 | fail "printf empty format string" "(empty)" "$result" |
| 406 | fi |
| 407 | |
| 408 | # ===================================== |
| 409 | # Summary |
| 410 | # ===================================== |
| 411 | printf "\n" |
| 412 | printf "${BLUE}==========================================\n" |
| 413 | printf "POSIX printf Builtin Test Summary\n" |
| 414 | printf "==========================================${NC}\n" |
| 415 | printf "Passed: ${GREEN}%d${NC}\n" "$PASSED" |
| 416 | printf "Failed: ${RED}%d${NC}\n" "$FAILED" |
| 417 | printf "Skipped: ${YELLOW}%d${NC}\n" "$SKIPPED" |
| 418 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 419 | |
| 420 | if [ -n "$FAILED_TESTS_LIST" ]; then |
| 421 | printf "\n${RED}Failed tests:${NC}\n" |
| 422 | printf "%b" "$FAILED_TESTS_LIST" |
| 423 | fi |
| 424 | |
| 425 | if [ "$FAILED" -gt 0 ]; then |
| 426 | exit 1 |
| 427 | fi |
| 428 | exit 0 |