printf.c (13897B)
- // utils-std: Collection of commonly available Unix tools
- // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
- // SPDX-License-Identifier: MPL-2.0
- #define _POSIX_C_SOURCE 200809L
- #include <errno.h>
- #include <stdbool.h>
- #include <stdio.h> // printf
- #include <stdlib.h> // strtoul, strtod
- #include <string.h> // strlen, memchr
- // [1-9]
- static int
- isndigit(int c)
- {
- return c >= '1' && c <= '9';
- }
- // digits [0-9]
- static int
- isdigit(int c)
- {
- return c >= '0' && c <= '9';
- }
- // hex digits [0-9A-Fa-f]
- static int
- isxdigit(int c)
- {
- return isdigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
- }
- static int
- iscntrl(int c)
- {
- return (unsigned)c < 0x20 || c == 0x7f;
- }
- // len parameter needed because of NULL escapes
- // returns 1 for handling '\c' early ends
- static int
- unescape(char *fmt, size_t *len, int percent)
- {
- char *start = fmt;
- char *store;
- char c = '\0';
- int value;
- /*
- * Required by POSIX.1-2024 for printf(1): \\ \a \b \c \f \n \r \t \v \000
- *
- * As inspiration, required by POSIX.1-2024 for dollar-single-quote($'…'):
- * \" \' \\ \a \b \e \f \n\ r\ t\ \v \c0 \x00 \000
- * <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02_04>
- */
- for(store = fmt; ((c = *fmt) != '\0') && fmt < (start + *len); ++fmt, ++store)
- {
- if(c != '\\')
- {
- *store = c;
- continue;
- }
- switch(*++fmt)
- {
- case '\\': /* backslash; POSIX */
- case '\'': /* single quote */
- default:
- *store = *fmt;
- break;
- case 'a': /* bell/alert; POSIX */
- *store = '\a';
- break;
- case 'b': /* backspace; POSIX */
- *store = '\b';
- break;
- case 'c':
- if(!percent)
- {
- /* clear; POSIX */
- *store = '\0';
- *len = (size_t)(store - start);
- return 1;
- }
- /* Assumes ASCII */
- if(fmt[1] == '?')
- {
- fmt++;
- *store = '\177';
- }
- else if(fmt[1] >= 'a' && fmt[1] <= 'z')
- {
- fmt++;
- *store = (fmt[0] - 'a') + 1;
- }
- else if(fmt[1] >= '@' && fmt[1] <= '_')
- {
- fmt++;
- *store = (fmt[0] - '@');
- }
- else
- {
- *store = 'c';
- }
- break;
- case 'e': /* escape */
- *store = '\033';
- break;
- case 'f': /* form-feed; POSIX */
- *store = '\f';
- break;
- case 'n': /* newline; POSIX */
- *store = '\n';
- break;
- case 'r': /* carriage-return; POSIX */
- *store = '\r';
- break;
- case 't': /* horizontal tab; POSIX */
- *store = '\t';
- break;
- case 'v': /* vertical tab; POSIX */
- *store = '\v';
- break;
- case 'x': /* hex */
- c = 2;
- fmt++;
- for(value = 0; c-- && isxdigit(*fmt); ++fmt)
- {
- value <<= 4;
- if(*fmt <= '9')
- value += *fmt - '0';
- else if(*fmt <= 'F')
- value += *fmt - 'A' + 10;
- else
- value += *fmt - 'a' + 10;
- }
- --fmt;
- *store = (char)value;
- break;
- /* octal; POSIX */
- case '0':
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- case '7':
- c = (!percent && *fmt == '0') ? 4 : 3;
- for(value = 0; c-- && *fmt >= '0' && *fmt <= '7'; ++fmt)
- {
- value <<= 3;
- value += *fmt - '0';
- }
- --fmt;
- *store = (percent && value == '%') ? '%' : (char)value;
- break;
- }
- }
- *store = '\0';
- *len = (size_t)(store - start);
- return 0;
- }
- static void
- usage(void)
- {
- (void)fputs("usage: printf format [arguments...]\n", stderr);
- }
- int
- main(int argc, char *argv[])
- {
- argc--;
- argv++;
- if(argc > 1 && strcmp("--", *argv) == 0)
- {
- argc--;
- argv++;
- }
- if(argc < 1)
- {
- usage();
- return 1;
- }
- char *fmt = argv[0];
- size_t fmtlen = strlen(fmt);
- if(unescape(fmt, &fmtlen, 1) != 0) return 1;
- argc--;
- argv++;
- // To keep argv intact for '%n$' format conversion specifiers
- char **fmt_argv = argv;
- unsigned int fmt_argn = 0;
- if(!strchr(fmt, '%'))
- {
- fwrite(fmt, 1, fmtlen, stdout);
- return 0;
- }
- do
- {
- for(char *fmt_idx = fmt; fmt_idx < (fmt + fmtlen); fmt_idx++)
- {
- // Field width provided for consistency with C printf
- int fwidth = 0;
- /* "negative precision is taken as if the precision were omitted." — POSIX.1-2008 fprintf() */
- int precision = -1;
- #define FMT_FLAGS "'-+ #0"
- #define FMT_BUF_SIZ sizeof("%" FMT_FLAGS "*.*d")
- int fmt_bufi = 0;
- static char fmt_buf[FMT_BUF_SIZ];
- fmt_buf[fmt_bufi++] = '%';
- char *fmt_arg = NULL;
- if(*fmt_idx != '%')
- {
- char *p = strchr(fmt_idx, '%');
- if(!p) p = fmt + fmtlen;
- fwrite(fmt_idx, 1, p - fmt_idx, stdout);
- fmt_idx = (p - 1);
- continue;
- }
- fmt_idx++;
- if(!(fmt_idx < (fmt + fmtlen))) return 0;
- // handle '%n$' if present
- if(isndigit(*fmt_idx) && argc > 0)
- {
- errno = 0;
- char *num_end = NULL;
- unsigned int num = strtoul(fmt_idx, &num_end, 10);
- if(errno == 0 && num != 0 && num_end && *num_end == '$')
- {
- fmt_arg = argv[(num - 1) % argc];
- fmt_idx = num_end + 1;
- fmt_buf[fmt_bufi++] = '*';
- }
- }
- /* flags */
- while(fmt_bufi < sizeof(FMT_FLAGS) && fmt_idx + 1 < (fmt + fmtlen) &&
- strchr(FMT_FLAGS, *fmt_idx) != NULL)
- {
- if(memchr(fmt_buf, *fmt_idx, fmt_bufi + 1))
- {
- fprintf(stderr,
- "fprintf: error: (format position %d) flag '%c' already set\n",
- (int)(fmt_idx - fmt),
- *fmt_idx);
- return 1;
- }
- fmt_buf[fmt_bufi++] = *fmt_idx;
- fmt_idx++;
- }
- // Field width from argument
- fmt_buf[fmt_bufi++] = '*';
- if(fmt_idx[0] == '*')
- {
- if(argc <= 0)
- {
- fprintf(stderr,
- "fprintf: error: (format position %d) field-width argument without format "
- "arguments\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- char *fwidth_arg = NULL;
- fmt_idx++;
- if(isndigit(*fmt_idx))
- {
- if(!fmt_arg)
- {
- fprintf(stderr,
- "printf: error: (format position %d) field-width positional argument usage "
- "('*n$') also needs format data to be positional (via '%%n$')\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- errno = 0;
- char *num_end = NULL;
- unsigned int num = strtoul(fmt_idx, &num_end, 10);
- if(errno != 0)
- {
- fprintf(
- stderr,
- "printf: error: (format position %d) Failed parsing field-width as a number: %s\n",
- (int)(fmt_idx - fmt),
- strerror(errno));
- return 1;
- }
- if(!num_end || *num_end != '$')
- {
- fprintf(stderr,
- "printf: error: (format position %d) Expected to find '$' after field-width "
- "digits\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- fwidth_arg = fmt_argv[(num - 1) % argc];
- fmt_idx = num_end + 1;
- }
- else
- {
- fmt_arg = fmt_argv[fmt_argn++ % argc];
- fwidth_arg = fmt_argv[fmt_argn++ % argc];
- }
- errno = 0;
- fwidth = strtoul(fwidth_arg, NULL, 0);
- if(errno != 0)
- {
- fprintf(stderr,
- "printf: error: Failed parsing argument (%s) as a number for field width: %s\n",
- fwidth_arg,
- strerror(errno));
- return 1;
- }
- }
- else if(isdigit(fmt_idx[0]))
- {
- errno = 0;
- char *num_end = NULL;
- fwidth = strtoul(fmt_idx, &num_end, 10);
- if(errno != 0)
- {
- fprintf(
- stderr,
- "printf: error: (format position %d) Failed parsing field-width as a number: %s\n",
- (int)(fmt_idx - fmt),
- strerror(errno));
- return 1;
- }
- if(!num_end)
- {
- fprintf(stderr,
- "printf: error: (format position %d) No remaining characters after field-width "
- "digits\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- if(*num_end == '$')
- {
- fprintf(stderr,
- "printf: error: (format position %d) Unexpectedly found '$' after '*'-less "
- "field-width digits\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- fmt_idx = num_end;
- }
- /* precision */
- fmt_buf[fmt_bufi++] = '.';
- fmt_buf[fmt_bufi++] = '*';
- if(*fmt_idx == '.')
- {
- fmt_idx++;
- if(*fmt_idx == '*')
- {
- fmt_idx++;
- if(argc <= 0)
- {
- fprintf(stderr,
- "fprintf: error: (format position %d) precision argument without format "
- "arguments\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- char *prec_arg = NULL;
- fmt_idx++;
- if(isndigit(*fmt_idx))
- {
- if(!fmt_arg)
- {
- fprintf(stderr,
- "printf: error: (format position %d) precision positional argument usage "
- "('.*n$') also needs format data to be positional (via '%%n$')\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- errno = 0;
- char *num_end = NULL;
- unsigned int num = strtoul(fmt_idx, &num_end, 10);
- if(errno != 0)
- {
- fprintf(
- stderr,
- "printf: error: (format position %d) Failed parsing precision as a number: %s\n",
- (int)(fmt_idx - fmt),
- strerror(errno));
- return 1;
- }
- if(!num_end || *num_end != '$')
- {
- fprintf(stderr,
- "printf: error: (format position %d) Expected to find '$' after precision "
- "digits\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- prec_arg = fmt_argv[(num - 1) % argc];
- fmt_idx = num_end + 1;
- }
- else
- {
- prec_arg = fmt_argv[fmt_argn++ % argc];
- }
- errno = 0;
- precision = strtoul(prec_arg, NULL, 0);
- if(errno != 0)
- {
- fprintf(stderr,
- "printf: error: Failed parsing argument (%s) as a number for precision: %s\n",
- prec_arg,
- strerror(errno));
- return 1;
- }
- }
- else if(isdigit(fmt_idx[0]))
- {
- errno = 0;
- char *num_end = NULL;
- precision = strtoul(fmt_idx, &num_end, 10);
- if(errno != 0)
- {
- fprintf(
- stderr,
- "printf: error: (format position %d) Failed parsing precision as a number: %s\n",
- (int)(fmt_idx - fmt),
- strerror(errno));
- return 1;
- }
- if(!num_end)
- {
- fprintf(stderr,
- "printf: error: (format position %d) No remaining characters after precision's "
- "digits\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- if(*num_end == '$')
- {
- fprintf(stderr,
- "printf: error: (format position %d) Unexpectedly found '$' after '*'-less "
- "precision's digits\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- fmt_idx = num_end;
- }
- else
- {
- fprintf(stderr,
- "printf: error: (format position %d) Unknown precision format (char: '%c')\n",
- (int)(fmt_idx - fmt),
- *fmt_idx);
- return 1;
- }
- }
- /* BSD compatibility */
- if(*fmt_idx == 'L') fmt_idx++;
- fmt_buf[fmt_bufi++] = *fmt_idx;
- fmt_buf[fmt_bufi++] = '\0';
- if(!fmt_arg) fmt_arg = (argc == 0) ? (char *)"" : fmt_argv[fmt_argn++ % argc];
- switch(*fmt_idx)
- {
- case '%':
- putchar(*fmt_idx);
- break;
- /* strings */
- case 's':
- printf(fmt_buf, fwidth, precision, fmt_arg);
- break;
- case 'b':
- {
- size_t arglen = strlen(fmt_arg);
- int clear = unescape(fmt_arg, &arglen, 0);
- if(arglen > precision) arglen = precision;
- /* left-justify if there's a '-' flag */
- if(memchr(fmt_buf, '-', fmt_bufi + 1))
- {
- fwrite(fmt_arg, 1, arglen, stdout);
- for(int pad = fwidth - arglen; pad > 0; pad--)
- putchar(' ');
- }
- else
- {
- for(int pad = fwidth - arglen; pad > 0; pad--)
- putchar(' ');
- fwrite(fmt_arg, 1, arglen, stdout);
- }
- if(clear) return 0;
- break;
- }
- case 'q':
- {
- if(fwidth != 0)
- {
- fprintf(
- stderr,
- "printf: error: (format position %d) field-width is unsupported with 'q' specifier\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- if(precision != -1)
- {
- fprintf(
- stderr,
- "printf: error: (format position %d) precision is unsupported with 'q' specifier\n",
- (int)(fmt_idx - fmt));
- return 1;
- }
- size_t arglen = strlen(fmt_arg);
- bool quoted = false;
- for(size_t i = 0; i < arglen; i++)
- {
- if(!(iscntrl(fmt_arg[i]) || fmt_arg[i] == '\'' || fmt_arg[i] == '"'))
- {
- putchar(fmt_arg[i]);
- continue;
- }
- if(!quoted)
- {
- quoted = true;
- fputs("$'", stdout);
- }
- switch(fmt_arg[i])
- {
- default:
- case 0x7F:
- /* for control chars */
- printf("\\c%c", fmt_arg[i] == 0x7F ? '?' : fmt_arg[i] + '@');
- break;
- case '\'':
- fputs("\\'", stdout);
- break;
- case '"':
- putchar(fmt_arg[i]);
- break;
- }
- }
- if(quoted) putchar('\'');
- break;
- }
- case 'c':
- printf("%*c", fwidth, *fmt_arg);
- break;
- /* integers */
- case 'd':
- case 'i':
- case 'o':
- case 'u':
- case 'x':
- case 'X':
- {
- errno = 0;
- unsigned long int num = strtoul(fmt_arg, NULL, 0);
- if(errno != 0)
- {
- fprintf(stderr,
- "printf: error: Failed parsing argument (%s) as a number for format conversion "
- "'%%%c': %s\n",
- fmt_arg,
- *fmt_idx,
- strerror(errno));
- return 1;
- }
- printf(fmt_buf, fwidth, precision, num);
- break;
- }
- /* floats */
- case 'a':
- case 'A':
- case 'e':
- case 'E':
- case 'f':
- case 'F':
- case 'g':
- case 'G':
- {
- double num = strtod(fmt_arg, NULL);
- if(errno != 0)
- {
- fprintf(stderr,
- "printf: error: Failed parsing argument (%s) as a number for format conversion "
- "'%%%c': %s\n",
- fmt_arg,
- *fmt_idx,
- strerror(errno));
- return 1;
- }
- printf(fmt_buf, fwidth, precision, num);
- break;
- }
- default:
- fprintf(stderr, "printf: error: Unknown conversion specifier '%c'\n", *fmt_idx);
- return 1;
- }
- }
- } while(fmt_argn < argc);
- }