+ /* All done. */
+ *x_out = x; rc = 0;
+end:
+ dstr_destroy(&d);
+ return (rc);
+}
+
+/*----- String utilities --------------------------------------------------*/
+
+/* Special character name table. */
+static const struct chartab {
+ const char *name; /* character name */
+ int ch; /* character value */
+ unsigned f; /* flags: */
+#define CTF_PREFER 1u /* preferred name */
+#define CTF_SHORT 2u /* short name (compact style) */
+} chartab[] = {
+ { "#eof", EOF, CTF_PREFER | CTF_SHORT },
+ { "#nul", '\0', CTF_PREFER },
+ { "#bell", '\a', CTF_PREFER },
+ { "#ding", '\a', 0 },
+ { "#bel", '\a', CTF_SHORT },
+ { "#backspace", '\b', CTF_PREFER },
+ { "#bs", '\b', CTF_SHORT },
+ { "#escape", '\x1b', CTF_PREFER },
+ { "#esc", '\x1b', CTF_SHORT },
+ { "#formfeed", '\f', CTF_PREFER },
+ { "#ff", '\f', CTF_SHORT },
+ { "#newline", '\n', CTF_PREFER },
+ { "#linefeed", '\n', 0 },
+ { "#lf", '\n', CTF_SHORT },
+ { "#nl", '\n', 0 },
+ { "#return", '\r', CTF_PREFER },
+ { "#carriage-return", '\r', 0 },
+ { "#cr", '\r', CTF_SHORT },
+ { "#tab", '\t', CTF_PREFER | CTF_SHORT },
+ { "#horizontal-tab", '\t', 0 },
+ { "#ht", '\t', 0 },
+ { "#vertical-tab", '\v', CTF_PREFER },
+ { "#vt", '\v', CTF_SHORT },
+ { "#space", ' ', 0 },
+ { "#spc", ' ', CTF_SHORT },
+ { "#delete", '\x7f', CTF_PREFER },
+ { "#del", '\x7f', CTF_SHORT },
+ { 0, 0, 0 }
+};
+
+/* --- @find_charname@ --- *
+ *
+ * Arguments: @int ch@ = character to match
+ * @unsigned f@ = flags (@CTF_...@) to match
+ *
+ * Returns: The name of the character, or null if no match is found.
+ *
+ * Use: Looks up a name for a character. Specifically, it returns
+ * the first entry in the @chartab@ table which matches @ch@ and
+ * which has one of the flags @f@ set.
+ */
+
+static const char *find_charname(int ch, unsigned f)
+{
+ const struct chartab *ct;
+
+ for (ct = chartab; ct->name; ct++)
+ if (ct->ch == ch && (ct->f&f)) return (ct->name);
+ return (0);
+}
+
+/* --- @read_charname@ --- *
+ *
+ * Arguments: @int *ch_out@ = where to put the character
+ * @const char *p@ = character name
+ * @unsigned f@ = flags (@TCF_...@)
+ *
+ * Returns: Zero if a match was found, @-1@ if not.
+ *
+ * Use: Looks up a character by name. If @RCF_EOFOK@ is set in @f@,
+ * then the @EOF@ marker can be matched; otherwise it can't.
+ */
+
+#define RCF_EOFOK 1u
+static int read_charname(int *ch_out, const char *p, unsigned f)
+{
+ const struct chartab *ct;
+
+ for (ct = chartab; ct->name; ct++)
+ if (STRCMP(p, ==, ct->name) && ((f&RCF_EOFOK) || ct->ch >= 0))
+ { *ch_out = ct->ch; return (0); }
+ return (-1);
+}
+
+/* --- @format_charesc@ --- *
+ *
+ * Arguments: @const struct gprintf_ops *gops@ = print operations
+ * @void *go@ = print destination
+ * @int ch@ = character to format
+ * @unsigned f@ = flags (@FCF_...@)
+ *
+ * Returns: ---
+ *
+ * Use: Format a character as an escape sequence, possibly as part of
+ * a larger string. If @FCF_BRACE@ is set in @f@, then put
+ * braces around a `\x...' code, so that it's suitable for use
+ * in a longer string.
+ */
+
+#define FCF_BRACE 1u
+static void format_charesc(const struct gprintf_ops *gops, void *go,
+ int ch, unsigned f)
+{
+ switch (ch) {
+ case '\a': gprintf(gops, go, "\\a"); break;
+ case '\b': gprintf(gops, go, "\\b"); break;
+ case '\x1b': gprintf(gops, go, "\\e"); break;
+ case '\f': gprintf(gops, go, "\\f"); break;
+ case '\r': gprintf(gops, go, "\\r"); break;
+ case '\n': gprintf(gops, go, "\\n"); break;
+ case '\t': gprintf(gops, go, "\\t"); break;
+ case '\v': gprintf(gops, go, "\\v"); break;
+ case '\\': gprintf(gops, go, "\\\\"); break;
+ case '\'': gprintf(gops, go, "\\'"); break;
+ case '\0':
+ if (f&FCF_BRACE) gprintf(gops, go, "\\{0}");
+ else gprintf(gops, go, "\\0");
+ break;
+ default:
+ if (f&FCF_BRACE)
+ gprintf(gops, go, "\\x{%0*x}", hex_width(UCHAR_MAX), ch);
+ else
+ gprintf(gops, go, "\\x%0*x", hex_width(UCHAR_MAX), ch);
+ break;
+ }
+}
+
+/* --- @format_char@ --- *
+ *
+ * Arguments: @const struct gprintf_ops *gops@ = print operations
+ * @void *go@ = print destination
+ * @int ch@ = character to format
+ *
+ * Returns: ---
+ *
+ * Use: Format a single character.
+ */
+
+static void format_char(const struct gprintf_ops *gops, void *go, int ch)
+{
+ switch (ch) {
+ case '\\': case '\'': escape:
+ gprintf(gops, go, "'");
+ format_charesc(gops, go, ch, 0);
+ gprintf(gops, go, "'");
+ break;
+ default:
+ if (!isprint(ch)) goto escape;
+ gprintf(gops, go, "'%c'", ch);
+ break;
+ }
+}
+
+/* --- @maybe_format_unsigned_char@, @maybe_format_signed_char@ --- *
+ *
+ * Arguments: @const struct gprintf_ops *gops@ = print operations
+ * @void *go@ = print destination
+ * @unsigned long u@ or @long i@ = an integer
+ *
+ * Returns: ---
+ *
+ * Use: Format a (signed or unsigned) integer as a character, if it's
+ * in range, printing something like `= 'q''. It's assumed that
+ * a comment marker has already been output.
+ */
+
+static void maybe_format_unsigned_char
+ (const struct gprintf_ops *gops, void *go, unsigned long u)
+{
+ const char *p;
+
+ p = find_charname(u, CTF_PREFER);
+ if (p) gprintf(gops, go, " = %s", p);
+ if (u < UCHAR_MAX)
+ { gprintf(gops, go, " = "); format_char(gops, go, u); }