logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/

printf.c (15264B)


  1. // utils-std: Collection of commonly available Unix tools
  2. // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
  3. // SPDX-License-Identifier: MPL-2.0
  4. #define _POSIX_C_SOURCE 200809L
  5. #include <errno.h>
  6. #include <stdbool.h>
  7. #include <stdio.h> // printf
  8. #include <stdlib.h> // strtoul, strtod
  9. #include <string.h> // strlen, memchr
  10. #undef MAX
  11. #define MAX(a, b) (a > b) ? a : b
  12. // [1-9]
  13. static int
  14. isndigit(int c)
  15. {
  16. return c >= '1' && c <= '9';
  17. }
  18. // digits [0-9]
  19. static int
  20. isdigit(int c)
  21. {
  22. return c >= '0' && c <= '9';
  23. }
  24. // hex digits [0-9A-Fa-f]
  25. static int
  26. isxdigit(int c)
  27. {
  28. return isdigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
  29. }
  30. static int
  31. iscntrl(int c)
  32. {
  33. return (unsigned)c < 0x20 || c == 0x7f;
  34. }
  35. // len parameter needed because of NULL escapes
  36. // returns 1 for handling '\c' early ends
  37. static int
  38. unescape(char *fmt, size_t *len, int percent)
  39. {
  40. char *start = fmt;
  41. char *store;
  42. char c = '\0';
  43. int value;
  44. /*
  45. * Required by POSIX.1-2024 for printf(1): \\ \a \b \c \f \n \r \t \v \000
  46. *
  47. * As inspiration, required by POSIX.1-2024 for dollar-single-quote($'…'):
  48. * \" \' \\ \a \b \e \f \n\ r\ t\ \v \c0 \x00 \000
  49. * <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02_04>
  50. */
  51. for(store = fmt; ((c = *fmt) != '\0') && fmt < (start + *len); ++fmt, ++store)
  52. {
  53. if(c != '\\')
  54. {
  55. *store = c;
  56. continue;
  57. }
  58. switch(*++fmt)
  59. {
  60. case '\\': /* backslash; POSIX */
  61. case '\'': /* single quote */
  62. default:
  63. *store = *fmt;
  64. break;
  65. case 'a': /* bell/alert; POSIX */
  66. *store = '\a';
  67. break;
  68. case 'b': /* backspace; POSIX */
  69. *store = '\b';
  70. break;
  71. case 'c':
  72. if(!percent)
  73. {
  74. /* clear; POSIX */
  75. *store = '\0';
  76. *len = (size_t)(store - start);
  77. return 1;
  78. }
  79. /* Assumes ASCII */
  80. if(fmt[1] == '?')
  81. {
  82. fmt++;
  83. *store = '\177';
  84. }
  85. else if(fmt[1] >= 'a' && fmt[1] <= 'z')
  86. {
  87. fmt++;
  88. *store = (fmt[0] - 'a') + 1;
  89. }
  90. else if(fmt[1] >= '@' && fmt[1] <= '_')
  91. {
  92. fmt++;
  93. *store = (fmt[0] - '@');
  94. }
  95. else
  96. {
  97. *store = 'c';
  98. }
  99. break;
  100. case 'e': /* escape */
  101. *store = '\033';
  102. break;
  103. case 'f': /* form-feed; POSIX */
  104. *store = '\f';
  105. break;
  106. case 'n': /* newline; POSIX */
  107. *store = '\n';
  108. break;
  109. case 'r': /* carriage-return; POSIX */
  110. *store = '\r';
  111. break;
  112. case 't': /* horizontal tab; POSIX */
  113. *store = '\t';
  114. break;
  115. case 'v': /* vertical tab; POSIX */
  116. *store = '\v';
  117. break;
  118. case 'x': /* hex */
  119. c = 2;
  120. fmt++;
  121. for(value = 0; c-- && isxdigit(*fmt); ++fmt)
  122. {
  123. value <<= 4;
  124. if(*fmt <= '9')
  125. value += *fmt - '0';
  126. else if(*fmt <= 'F')
  127. value += *fmt - 'A' + 10;
  128. else
  129. value += *fmt - 'a' + 10;
  130. }
  131. --fmt;
  132. *store = (char)value;
  133. break;
  134. /* octal; POSIX */
  135. case '0':
  136. case '1':
  137. case '2':
  138. case '3':
  139. case '4':
  140. case '5':
  141. case '6':
  142. case '7':
  143. c = (!percent && *fmt == '0') ? 4 : 3;
  144. for(value = 0; c-- && *fmt >= '0' && *fmt <= '7'; ++fmt)
  145. {
  146. value <<= 3;
  147. value += *fmt - '0';
  148. }
  149. --fmt;
  150. *store = (percent && value == '%') ? '%' : (char)value;
  151. break;
  152. }
  153. }
  154. *store = '\0';
  155. *len = (size_t)(store - start);
  156. return 0;
  157. }
  158. static void
  159. usage(void)
  160. {
  161. (void)fputs("usage: printf format [arguments...]\n", stderr);
  162. }
  163. int
  164. main(int argc, char *argv[])
  165. {
  166. argc--;
  167. argv++;
  168. if(argc > 1 && strcmp("--", *argv) == 0)
  169. {
  170. argc--;
  171. argv++;
  172. }
  173. if(argc < 1)
  174. {
  175. usage();
  176. return 1;
  177. }
  178. char *fmt = argv[0];
  179. size_t fmtlen = strlen(fmt);
  180. if(unescape(fmt, &fmtlen, 1) != 0) return 1;
  181. argc--;
  182. argv++;
  183. // To keep argv intact for '%n$' format conversion specifiers
  184. char **fmt_argv = argv;
  185. if(!strchr(fmt, '%'))
  186. {
  187. fwrite(fmt, 1, fmtlen, stdout);
  188. return 0;
  189. }
  190. do
  191. {
  192. int fmt_argn = 0;
  193. for(char *fmt_idx = fmt; fmt_idx < (fmt + fmtlen); fmt_idx++)
  194. {
  195. // Field width provided for consistency with C printf
  196. int fwidth = 0;
  197. /* "negative precision is taken as if the precision were omitted." — POSIX.1-2008 fprintf() */
  198. int precision = -1;
  199. #define FMT_FLAGS "'-+ #0"
  200. #define FMT_BUF_SIZ sizeof("%" FMT_FLAGS "*.*d")
  201. size_t fmt_bufi = 0;
  202. static char fmt_buf[FMT_BUF_SIZ];
  203. fmt_buf[fmt_bufi++] = '%';
  204. char *fmt_arg = NULL;
  205. if(*fmt_idx != '%')
  206. {
  207. char *p = strchr(fmt_idx, '%');
  208. if(!p) p = fmt + fmtlen;
  209. fwrite(fmt_idx, 1, p - fmt_idx, stdout);
  210. fmt_idx = (p - 1);
  211. continue;
  212. }
  213. fmt_idx++;
  214. if(!(fmt_idx < (fmt + fmtlen))) return 0;
  215. // handle '%n$' if present
  216. if(isndigit(*fmt_idx) && argc > 0)
  217. {
  218. errno = 0;
  219. char *num_end = NULL;
  220. unsigned int num = strtoul(fmt_idx, &num_end, 10);
  221. if(errno == 0 && num != 0 && num_end && *num_end == '$')
  222. {
  223. if((int)num > argc)
  224. {
  225. fprintf(stderr,
  226. "printf: error: (format position %d) argument '%d' does not exists, only got "
  227. "%d arguments\n",
  228. (int)(fmt_idx - fmt),
  229. num,
  230. argc);
  231. return 1;
  232. }
  233. fmt_argn = MAX(fmt_argn, (int)num);
  234. fmt_arg = fmt_argv[num - 1];
  235. fmt_idx = num_end + 1;
  236. }
  237. }
  238. /* flags */
  239. while(fmt_bufi < sizeof(FMT_FLAGS) && fmt_idx + 1 < (fmt + fmtlen) &&
  240. strchr(FMT_FLAGS, *fmt_idx) != NULL)
  241. {
  242. if(memchr(fmt_buf, *fmt_idx, fmt_bufi + 1))
  243. {
  244. fprintf(stderr,
  245. "printf: error: (format position %d) flag '%c' already set\n",
  246. (int)(fmt_idx - fmt),
  247. *fmt_idx);
  248. return 1;
  249. }
  250. fmt_buf[fmt_bufi++] = *fmt_idx;
  251. fmt_idx++;
  252. }
  253. // Field width from argument
  254. fmt_buf[fmt_bufi++] = '*';
  255. if(fmt_idx[0] == '*')
  256. {
  257. if(argc <= 0)
  258. {
  259. fprintf(stderr,
  260. "printf: error: (format position %d) field-width argument without format "
  261. "arguments\n",
  262. (int)(fmt_idx - fmt));
  263. return 1;
  264. }
  265. char *fwidth_arg = NULL;
  266. fmt_idx++;
  267. if(isndigit(*fmt_idx))
  268. {
  269. if(!fmt_arg)
  270. {
  271. fprintf(stderr,
  272. "printf: error: (format position %d) field-width positional argument usage "
  273. "('*n$') also needs format data to be positional (via '%%n$')\n",
  274. (int)(fmt_idx - fmt));
  275. return 1;
  276. }
  277. errno = 0;
  278. char *num_end = NULL;
  279. unsigned int num = strtoul(fmt_idx, &num_end, 10);
  280. if(errno != 0)
  281. {
  282. fprintf(
  283. stderr,
  284. "printf: error: (format position %d) Failed parsing field-width as a number: %s\n",
  285. (int)(fmt_idx - fmt),
  286. strerror(errno));
  287. return 1;
  288. }
  289. if(!num_end || *num_end != '$')
  290. {
  291. fprintf(stderr,
  292. "printf: error: (format position %d) Expected to find '$' after field-width "
  293. "digits\n",
  294. (int)(fmt_idx - fmt));
  295. return 1;
  296. }
  297. if((int)num > argc)
  298. {
  299. fprintf(stderr,
  300. "printf: error: (format position %d) argument '%d' does not exists, only got "
  301. "%d arguments\n",
  302. (int)(fmt_idx - fmt),
  303. num,
  304. argc);
  305. return 1;
  306. }
  307. fmt_argn = MAX(fmt_argn, (int)num);
  308. fwidth_arg = fmt_argv[num - 1];
  309. fmt_idx = num_end + 1;
  310. }
  311. else
  312. {
  313. if(fmt_argn < argc) fwidth_arg = fmt_argv[fmt_argn++];
  314. if(fmt_argn < argc) fmt_arg = fmt_argv[fmt_argn++];
  315. }
  316. if(fwidth_arg)
  317. {
  318. errno = 0;
  319. fwidth = strtoul(fwidth_arg, NULL, 0);
  320. if(errno != 0)
  321. {
  322. fprintf(stderr,
  323. "printf: error: Failed parsing argument (%s) as a number for field width: %s\n",
  324. fwidth_arg,
  325. strerror(errno));
  326. return 1;
  327. }
  328. }
  329. }
  330. else if(isdigit(fmt_idx[0]))
  331. {
  332. errno = 0;
  333. char *num_end = NULL;
  334. fwidth = strtoul(fmt_idx, &num_end, 10);
  335. if(errno != 0)
  336. {
  337. fprintf(
  338. stderr,
  339. "printf: error: (format position %d) Failed parsing field-width as a number: %s\n",
  340. (int)(fmt_idx - fmt),
  341. strerror(errno));
  342. return 1;
  343. }
  344. if(!num_end)
  345. {
  346. fprintf(stderr,
  347. "printf: error: (format position %d) No remaining characters after field-width "
  348. "digits\n",
  349. (int)(fmt_idx - fmt));
  350. return 1;
  351. }
  352. if(*num_end == '$')
  353. {
  354. fprintf(stderr,
  355. "printf: error: (format position %d) Unexpectedly found '$' after '*'-less "
  356. "field-width digits\n",
  357. (int)(fmt_idx - fmt));
  358. return 1;
  359. }
  360. fmt_idx = num_end;
  361. }
  362. /* precision */
  363. fmt_buf[fmt_bufi++] = '.';
  364. fmt_buf[fmt_bufi++] = '*';
  365. if(*fmt_idx == '.')
  366. {
  367. fmt_idx++;
  368. if(*fmt_idx == '*')
  369. {
  370. fmt_idx++;
  371. if(argc <= 0)
  372. {
  373. fprintf(stderr,
  374. "printf: error: (format position %d) precision argument without format "
  375. "arguments\n",
  376. (int)(fmt_idx - fmt));
  377. return 1;
  378. }
  379. char *prec_arg = NULL;
  380. if(isndigit(*fmt_idx))
  381. {
  382. if(!fmt_arg)
  383. {
  384. fprintf(stderr,
  385. "printf: error: (format position %d) precision positional argument usage "
  386. "('.*n$') also needs format data to be positional (via '%%n$')\n",
  387. (int)(fmt_idx - fmt));
  388. return 1;
  389. }
  390. errno = 0;
  391. char *num_end = NULL;
  392. unsigned int num = strtoul(fmt_idx, &num_end, 10);
  393. if(errno != 0)
  394. {
  395. fprintf(
  396. stderr,
  397. "printf: error: (format position %d) Failed parsing precision as a number: %s\n",
  398. (int)(fmt_idx - fmt),
  399. strerror(errno));
  400. return 1;
  401. }
  402. if(!num_end || *num_end != '$')
  403. {
  404. fprintf(stderr,
  405. "printf: error: (format position %d) Expected to find '$' after precision "
  406. "digits\n",
  407. (int)(fmt_idx - fmt));
  408. return 1;
  409. }
  410. if((int)num > argc)
  411. {
  412. fprintf(stderr,
  413. "printf: error: (format position %d) argument '%d' does not exists, only got "
  414. "%d arguments\n",
  415. (int)(fmt_idx - fmt),
  416. num,
  417. argc);
  418. return 1;
  419. }
  420. fmt_argn = MAX(fmt_argn, (int)num);
  421. prec_arg = fmt_argv[num - 1];
  422. fmt_idx = num_end + 1;
  423. }
  424. else
  425. {
  426. if(fmt_argn < argc) prec_arg = fmt_argv[fmt_argn++];
  427. }
  428. if(prec_arg)
  429. {
  430. errno = 0;
  431. precision = strtoul(prec_arg, NULL, 0);
  432. if(errno != 0)
  433. {
  434. fprintf(stderr,
  435. "printf: error: Failed parsing argument (%s) as a number for precision: %s\n",
  436. prec_arg,
  437. strerror(errno));
  438. return 1;
  439. }
  440. }
  441. }
  442. else if(isdigit(fmt_idx[0]))
  443. {
  444. errno = 0;
  445. char *num_end = NULL;
  446. precision = strtoul(fmt_idx, &num_end, 10);
  447. if(errno != 0)
  448. {
  449. fprintf(
  450. stderr,
  451. "printf: error: (format position %d) Failed parsing precision as a number: %s\n",
  452. (int)(fmt_idx - fmt),
  453. strerror(errno));
  454. return 1;
  455. }
  456. if(!num_end)
  457. {
  458. fprintf(stderr,
  459. "printf: error: (format position %d) No remaining characters after precision's "
  460. "digits\n",
  461. (int)(fmt_idx - fmt));
  462. return 1;
  463. }
  464. if(*num_end == '$')
  465. {
  466. fprintf(stderr,
  467. "printf: error: (format position %d) Unexpectedly found '$' after '*'-less "
  468. "precision's digits\n",
  469. (int)(fmt_idx - fmt));
  470. return 1;
  471. }
  472. fmt_idx = num_end;
  473. }
  474. else
  475. {
  476. fprintf(stderr,
  477. "printf: error: (format position %d) Unknown precision format (char: '%c')\n",
  478. (int)(fmt_idx - fmt),
  479. *fmt_idx);
  480. return 1;
  481. }
  482. }
  483. /* BSD compatibility */
  484. if(*fmt_idx == 'L') fmt_idx++;
  485. fmt_buf[fmt_bufi++] = *fmt_idx;
  486. fmt_buf[fmt_bufi++] = '\0';
  487. if(!fmt_arg && fmt_argn < argc) fmt_arg = fmt_argv[fmt_argn++];
  488. switch(*fmt_idx)
  489. {
  490. case '%':
  491. putchar(*fmt_idx);
  492. break;
  493. /* strings */
  494. case 's':
  495. if(!fmt_arg) fmt_arg = (char *)"";
  496. printf(fmt_buf, fwidth, precision, fmt_arg);
  497. break;
  498. case 'b':
  499. {
  500. if(!fmt_arg) fmt_arg = (char *)"";
  501. size_t arglen = strlen(fmt_arg);
  502. int clear = unescape(fmt_arg, &arglen, 0);
  503. if(precision > 0 && arglen > (size_t)precision) arglen = precision;
  504. /* left-justify if there's a '-' flag */
  505. if(memchr(fmt_buf, '-', fmt_bufi + 1))
  506. {
  507. fwrite(fmt_arg, 1, arglen, stdout);
  508. for(int pad = fwidth - arglen; pad > 0; pad--)
  509. putchar(' ');
  510. }
  511. else
  512. {
  513. for(int pad = fwidth - arglen; pad > 0; pad--)
  514. putchar(' ');
  515. fwrite(fmt_arg, 1, arglen, stdout);
  516. }
  517. if(clear) return 0;
  518. break;
  519. }
  520. case 'q':
  521. {
  522. if(!fmt_arg) fmt_arg = (char *)"";
  523. if(fwidth != 0)
  524. {
  525. fprintf(
  526. stderr,
  527. "printf: error: (format position %d) field-width is unsupported with 'q' specifier\n",
  528. (int)(fmt_idx - fmt));
  529. return 1;
  530. }
  531. if(precision != -1)
  532. {
  533. fprintf(
  534. stderr,
  535. "printf: error: (format position %d) precision is unsupported with 'q' specifier\n",
  536. (int)(fmt_idx - fmt));
  537. return 1;
  538. }
  539. size_t arglen = strlen(fmt_arg);
  540. bool quoted = false;
  541. for(size_t i = 0; i < arglen; i++)
  542. {
  543. if(!(iscntrl(fmt_arg[i]) || fmt_arg[i] == '\'' || fmt_arg[i] == '"'))
  544. {
  545. putchar(fmt_arg[i]);
  546. continue;
  547. }
  548. if(!quoted)
  549. {
  550. quoted = true;
  551. fputs("$'", stdout);
  552. }
  553. switch(fmt_arg[i])
  554. {
  555. default:
  556. case 0x7F:
  557. /* for control chars */
  558. printf("\\c%c", fmt_arg[i] == 0x7F ? '?' : fmt_arg[i] + '@');
  559. break;
  560. case '\'':
  561. fputs("\\'", stdout);
  562. break;
  563. case '"':
  564. putchar(fmt_arg[i]);
  565. break;
  566. }
  567. }
  568. if(quoted) putchar('\'');
  569. break;
  570. }
  571. case 'c':
  572. if(!fmt_arg) fmt_arg = (char *)"";
  573. printf("%*c", fwidth, *fmt_arg);
  574. break;
  575. /* integers */
  576. case 'd':
  577. case 'i':
  578. case 'o':
  579. case 'u':
  580. case 'x':
  581. case 'X':
  582. {
  583. unsigned long int num = 0;
  584. if(fmt_arg)
  585. {
  586. errno = 0;
  587. num = strtoul(fmt_arg, NULL, 0);
  588. if(errno != 0)
  589. {
  590. fprintf(stderr,
  591. "printf: error: Failed parsing argument (%s) as a number for format conversion "
  592. "'%%%c': %s\n",
  593. fmt_arg,
  594. *fmt_idx,
  595. strerror(errno));
  596. return 1;
  597. }
  598. }
  599. printf(fmt_buf, fwidth, precision, num);
  600. break;
  601. }
  602. /* floats */
  603. case 'a':
  604. case 'A':
  605. case 'e':
  606. case 'E':
  607. case 'f':
  608. case 'F':
  609. case 'g':
  610. case 'G':
  611. {
  612. double num = 0;
  613. if(fmt_arg)
  614. {
  615. num = strtod(fmt_arg, NULL);
  616. if(errno != 0)
  617. {
  618. fprintf(stderr,
  619. "printf: error: Failed parsing argument (%s) as a number for format conversion "
  620. "'%%%c': %s\n",
  621. fmt_arg,
  622. *fmt_idx,
  623. strerror(errno));
  624. return 1;
  625. }
  626. }
  627. printf(fmt_buf, fwidth, precision, num);
  628. break;
  629. }
  630. default:
  631. fprintf(stderr, "printf: error: Unknown conversion specifier '%c'\n", *fmt_idx);
  632. return 1;
  633. }
  634. }
  635. fmt_argv += fmt_argn;
  636. argc -= fmt_argn;
  637. } while(argc > 0);
  638. }