logo

utils-std

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

printf.c (13897B)


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