commit: 1c81a03d121a969d0dfe9c52f480a66554efd00d
parent 2cb594ab23dbc1cee684ee04401be431444bda69
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Fri, 30 May 2025 08:42:17 +0200
cmd/printf: Add support for %q specifier
Diffstat:
3 files changed, 81 insertions(+), 1 deletion(-)
diff --git a/cmd/printf.1 b/cmd/printf.1
@@ -338,6 +338,13 @@ and that an additional escape sequence
stops further output from this
.Nm
invocation.
+.It Cm q
+Print
+.Ar argument
+so it can be reused for shell input,
+escaping control characters and single-quote with POSIX.1-2024
+.Cm $''
+(dollar-single-quote) syntax.
.It Cm n$
Allows reordering of the output according to
.Ar argument .
@@ -420,6 +427,10 @@ backslash-escapes are extensions
inspired by
.Xr sh 1 Ns 's
dollar-single-quote($'…') escapes.
+.Pp
+The
+.Cm %q
+format specifier is an extension inspired by GNU coreutils.
.Sh HISTORY
The
.Nm
diff --git a/cmd/printf.c b/cmd/printf.c
@@ -4,6 +4,7 @@
#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
@@ -29,6 +30,12 @@ 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
@@ -520,6 +527,60 @@ main(int argc, char *argv[])
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;
diff --git a/test-cmd/printf.sh b/test-cmd/printf.sh
@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
# SPDX-License-Identifier: MPL-2.0
-plans=33
+plans=37
WD="$(dirname "$0")/../"
target="${WD}/cmd/printf"
. "${WD}/test-cmd/tap.sh"
@@ -29,6 +29,14 @@ t_args fmt_b '
t_args fmt_b_rightpad '!{} .' '%-6b%c' '\041\x7B\x7d' .
t_args fmt_b_leftpad ' !{}.' '%6b%c' '\041\x7B\x7d' .
+t_args fmt_q_print 'foo
+baré
+' '%q\n' foo baré
+t_args fmt_q_cntrl "back$'\cHdel\c?nl\cJ'" '%q' 'backdelnl
+'
+t_args fmt_q_sq "single$'\\'quote'" %q "single'quote"
+t_args fmt_q_dq "double$'"'"'"quote'" %q 'double"quote'
+
t_args fmt_c 'foo' %c f oo oooo
t_args fmt_d 10, %d, 10