]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
format-table: add new string cell type that accepts ANSI sequences
authorLennart Poettering <lennart@poettering.net>
Thu, 15 Jan 2026 07:51:05 +0000 (08:51 +0100)
committerLennart Poettering <lennart@poettering.net>
Wed, 21 Jan 2026 20:32:49 +0000 (21:32 +0100)
For various usecases it's useful that we can embed ANSI sequences in
cells of tables. For example, I hope we can eventually switch "systemctl
status" output to use the table formatter, and multiple of its fields
contain ANSI sequences (since they pack multiple different pieces
information into the same field, and highlight parts of it to
communicate relevance of distinct parts).

Add a distinct cell type for this, which gets special processing when we
output to a terminal that doesn't support ANSI sequences, and to JSON:
we strip the sequences.

src/shared/format-table.c
src/shared/format-table.h
src/test/test-format-table.c

index 9e366ce51348f8bcf0ca2b06bf1955b23ab3c3d6..9f6c46afb75d3602674ccbacf09cf0a03257f690 100644 (file)
@@ -283,6 +283,7 @@ static size_t table_data_size(TableDataType type, const void *data) {
                 return 0;
 
         case TABLE_STRING:
+        case TABLE_STRING_WITH_ANSI:
         case TABLE_PATH:
         case TABLE_PATH_BASENAME:
         case TABLE_FIELD:
@@ -529,7 +530,7 @@ int table_add_cell_stringf_full(Table *t, TableCell **ret_cell, TableDataType dt
         int r;
 
         assert(t);
-        assert(IN_SET(dt, TABLE_STRING, TABLE_PATH, TABLE_PATH_BASENAME, TABLE_FIELD, TABLE_HEADER, TABLE_VERSION));
+        assert(IN_SET(dt, TABLE_STRING, TABLE_STRING_WITH_ANSI, TABLE_PATH, TABLE_PATH_BASENAME, TABLE_FIELD, TABLE_HEADER, TABLE_VERSION));
 
         va_start(ap, format);
         r = vasprintf(&buffer, format, ap);
@@ -933,6 +934,7 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) {
                         break;
 
                 case TABLE_STRING:
+                case TABLE_STRING_WITH_ANSI:
                 case TABLE_PATH:
                 case TABLE_PATH_BASENAME:
                 case TABLE_FIELD:
@@ -1393,6 +1395,7 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t
                 switch (a->type) {
 
                 case TABLE_STRING:
+                case TABLE_STRING_WITH_ANSI:
                 case TABLE_FIELD:
                 case TABLE_HEADER:
                         return strcmp(a->string, b->string);
@@ -1574,7 +1577,13 @@ static char* format_strv_width(char **strv, size_t column_width) {
         return buf;
 }
 
-static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercasing, size_t column_width, bool *have_soft) {
+static const char *table_data_format(
+                Table *t,
+                TableData *d,
+                bool avoid_uppercasing,
+                size_t column_width,
+                bool *have_soft) {
+
         assert(d);
 
         if (d->formatted &&
@@ -1587,6 +1596,7 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas
                 return table_ersatz_string(t);
 
         case TABLE_STRING:
+        case TABLE_STRING_WITH_ANSI:
         case TABLE_PATH:
         case TABLE_PATH_BASENAME:
         case TABLE_FIELD:
@@ -2068,6 +2078,42 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas
         return d->formatted;
 }
 
+static const char *table_data_format_strip_ansi(
+                Table *t,
+                TableData *d,
+                bool avoid_uppercasing,
+                size_t column_width,
+                bool *have_soft,
+                char **ret_buffer) {
+
+        /* Just like table_data_format() but strips ANSI sequences for ANSI fields. */
+
+        assert(ret_buffer);
+
+        const char *c;
+
+        c = table_data_format(t, d, avoid_uppercasing, column_width, have_soft);
+        if (!c)
+                return NULL;
+
+        if (d->type != TABLE_STRING_WITH_ANSI) {
+                /* Shortcut: we do not consider ANSI sequences for all other column types, hence return the
+                 * original string as-is */
+                *ret_buffer = NULL;
+                return c;
+        }
+
+        _cleanup_free_ char *s = strdup(c);
+        if (!s)
+                return NULL;
+
+        if (!strip_tab_ansi(&s, /* isz= */ NULL, /* highlight= */ NULL))
+                return NULL;
+
+        *ret_buffer = TAKE_PTR(s);
+        return *ret_buffer;
+}
+
 static int console_width_height(
                 const char *s,
                 size_t *ret_width,
@@ -2122,14 +2168,20 @@ static int table_data_requested_width_height(
                 size_t *ret_height,
                 bool *have_soft) {
 
-        _cleanup_free_ char *truncated = NULL;
+        _cleanup_free_ char *truncated = NULL, *buffer = NULL;
         bool truncation_applied = false;
         size_t width, height;
+        bool soft = false;
         const char *t;
         int r;
-        bool soft = false;
 
-        t = table_data_format(table, d, false, available_width, &soft);
+        t = table_data_format_strip_ansi(
+                        table,
+                        d,
+                        /* avoid_uppercasing= */ false,
+                        available_width,
+                        &soft,
+                        &buffer);
         if (!t)
                 return -ENOMEM;
 
@@ -2352,7 +2404,7 @@ int table_print(Table *t, FILE *f) {
                                 if (r < 0)
                                         return r;
                                 if (r > 0) { /* Truncated because too many lines? */
-                                        _cleanup_free_ char *last = NULL;
+                                        _cleanup_free_ char *last = NULL, *buffer = NULL;
                                         const char *field;
 
                                         /* If we are going to show only the first few lines of a cell that has
@@ -2360,9 +2412,13 @@ int table_print(Table *t, FILE *f) {
                                          * ellipsis. Hence, let's figure out the last line, and account for its
                                          * length plus ellipsis. */
 
-                                        field = table_data_format(t, d, false,
-                                                                  width ? width[j] : SIZE_MAX,
-                                                                  &any_soft);
+                                        field = table_data_format_strip_ansi(
+                                                        t,
+                                                        d,
+                                                        /* avoid_uppercasing= */ false,
+                                                        width ? width[j] : SIZE_MAX,
+                                                        &any_soft,
+                                                        &buffer);
                                         if (!field)
                                                 return -ENOMEM;
 
@@ -2550,7 +2606,7 @@ int table_print(Table *t, FILE *f) {
                         more_sublines = false;
 
                         for (size_t j = 0; j < display_columns; j++) {
-                                _cleanup_free_ char *buffer = NULL, *extracted = NULL;
+                                _cleanup_free_ char *buffer = NULL, *stripped_ansi_buffer = NULL, *extracted = NULL;
                                 bool lines_truncated = false;
                                 const char *field, *color = NULL, *underline = NULL;
                                 TableData *d;
@@ -2558,7 +2614,21 @@ int table_print(Table *t, FILE *f) {
 
                                 assert_se(d = row[t->display_map ? t->display_map[j] : j]);
 
-                                field = table_data_format(t, d, false, width[j], NULL);
+                                if (colors_enabled())
+                                        field = table_data_format(
+                                                        t,
+                                                        d,
+                                                        /* avoid_uppercasing= */ false,
+                                                        width[j],
+                                                        /* have_soft= */ NULL);
+                                else
+                                        field = table_data_format_strip_ansi(
+                                                        t,
+                                                        d,
+                                                        /* avoid_uppercasing= */ false,
+                                                        width[j],
+                                                        /* have_soft= */ NULL,
+                                                        &stripped_ansi_buffer);
                                 if (!field)
                                         return -ENOMEM;
 
@@ -2665,7 +2735,8 @@ int table_print(Table *t, FILE *f) {
 
                                 fputs(field, f);
 
-                                if (color || underline)
+                                /* Reset color afterwards if colors was set or the string to output contained ANSI sequences. */
+                                if (color || underline || (d->type == TABLE_STRING_WITH_ANSI && colors_enabled()))
                                         fputs(ANSI_NORMAL, f);
 
                                 gap_color = d->rgap_color;
@@ -2920,6 +2991,18 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) {
                                                   SD_JSON_BUILD_UNSIGNED(major(d->devnum)),
                                                   SD_JSON_BUILD_UNSIGNED(minor(d->devnum))));
 
+        case TABLE_STRING_WITH_ANSI: {
+                _cleanup_free_ char *s = strdup(d->string);
+                if (!s)
+                        return -ENOMEM;
+
+                /* We strip the ANSI data when outputting to JSON */
+                if (!strip_tab_ansi(&s, /* isz= */ NULL, /* highlight= */ NULL))
+                        return -ENOMEM;
+
+                return sd_json_variant_new_string(ret, s);
+        }
+
         default:
                 return -EINVAL;
         }
@@ -2963,7 +3046,7 @@ char* table_mangle_to_json_field_name(const char *str) {
 }
 
 static int table_make_json_field_name(Table *t, TableData *d, char **ret) {
-        _cleanup_free_ char *mangled = NULL;
+        _cleanup_free_ char *mangled = NULL, *buffer = NULL;
         const char *n;
 
         assert(t);
@@ -2973,7 +3056,13 @@ static int table_make_json_field_name(Table *t, TableData *d, char **ret) {
         if (IN_SET(d->type, TABLE_HEADER, TABLE_FIELD))
                 n = d->string;
         else {
-                n = table_data_format(t, d, /* avoid_uppercasing= */ true, SIZE_MAX, NULL);
+                n = table_data_format_strip_ansi(
+                                t,
+                                d,
+                                /* avoid_uppercasing= */ true,
+                                /* column_width= */ SIZE_MAX,
+                                /* have_soft= */ NULL,
+                                &buffer);
                 if (!n)
                         return -ENOMEM;
         }
index 2147b823178372be5ba9b22986c3ce268b648620..56dd18fa66d353eb2cc7d49958e422b66419ccaf 100644 (file)
@@ -10,6 +10,7 @@
 typedef enum TableDataType {
         TABLE_EMPTY,
         TABLE_STRING,
+        TABLE_STRING_WITH_ANSI,    /* like the above, but contains ANSI sequences/TABs. They will be stripped when outputing to JSON */
         TABLE_HEADER,              /* in regular mode: the cells in the first row, that carry the column names */
         TABLE_FIELD,               /* in vertical mode: the cells in the first column, that carry the field names */
         TABLE_STRV,
index ff886a3d5effc3c486e7bc354f055545f8ed2707..a2e142791ff80851c0d6e1637a49d532f0b21bed 100644 (file)
@@ -4,6 +4,8 @@
 #include <unistd.h>
 
 #include "alloc-util.h"
+#include "ansi-color.h"
+#include "env-util.h"
 #include "format-table.h"
 #include "json-util.h"
 #include "terminal-util.h"
@@ -835,9 +837,80 @@ TEST(table_bps) {
                      "2500000000         2.3G           2.5Gbps\n");
 }
 
+TEST(table_ansi) {
+        _cleanup_(table_unrefp) Table *table = NULL;
+
+        ASSERT_NOT_NULL((table = table_new("foo", "bar", "baz", "kkk")));
+
+        ASSERT_OK(table_add_many(table,
+                                 TABLE_STRING, "hallo",
+                                 TABLE_STRING_WITH_ANSI, "knuerz" ANSI_HIGHLIGHT_RED "red" ANSI_HIGHLIGHT_GREEN "green",
+                                 TABLE_STRING_WITH_ANSI, "noansi",
+                                 TABLE_STRING_WITH_ANSI, ANSI_GREY "thisisgrey"));
+
+        unsigned saved_columns = columns();
+        bool saved_color = colors_enabled();
+        _cleanup_free_ char *saved_term = NULL;
+        const char *e = getenv("TERM");
+        if (e)
+                ASSERT_NOT_NULL((saved_term = strdup(e)));
+
+        ASSERT_OK_ERRNO(setenv("COLUMNS", "200", /* overwrite= */ true));
+        ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "1", /* overwrite= */ true));
+        ASSERT_OK_ERRNO(setenv("TERM", FALLBACK_TERM, /* overwrite= */ true));
+        reset_terminal_feature_caches();
+
+        _cleanup_free_ char *formatted = NULL;
+        ASSERT_OK(table_format(table, &formatted));
+
+        ASSERT_STREQ(formatted,
+                     ANSI_ADD_UNDERLINE "FOO  " ANSI_NORMAL
+                     ANSI_ADD_UNDERLINE " " ANSI_NORMAL
+                     ANSI_ADD_UNDERLINE "BAR           " ANSI_NORMAL
+                     ANSI_ADD_UNDERLINE " " ANSI_NORMAL
+                     ANSI_ADD_UNDERLINE "BAZ   " ANSI_NORMAL
+                     ANSI_ADD_UNDERLINE " " ANSI_NORMAL
+                     ANSI_ADD_UNDERLINE "KKK       " ANSI_NORMAL "\n"
+                     "hallo knuerz" ANSI_HIGHLIGHT_RED "red" ANSI_HIGHLIGHT_GREEN "green" ANSI_NORMAL
+                     " noansi" ANSI_NORMAL
+                     " " ANSI_GREY "thisisgrey" ANSI_NORMAL "\n");
+
+        /* Validate that color is correctly stripped */
+        ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "0", /* overwrite= */ true));
+        reset_terminal_feature_caches();
+
+        formatted = mfree(formatted);
+        ASSERT_OK(table_format(table, &formatted));
+
+        ASSERT_STREQ(formatted,
+                     "FOO   BAR            BAZ    KKK\n"
+                     "hallo knuerzredgreen noansi thisisgrey\n");
+
+        ASSERT_OK(table_print(table, /* f= */ NULL));
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL, *jj = NULL;
+
+        ASSERT_OK(table_to_json(table, &j));
+
+        ASSERT_OK(sd_json_build(&jj,
+                                SD_JSON_BUILD_ARRAY(
+                                                SD_JSON_BUILD_OBJECT(
+                                                                SD_JSON_BUILD_PAIR_STRING("foo", "hallo"),
+                                                                SD_JSON_BUILD_PAIR_STRING("bar", "knuerzredgreen"),
+                                                                SD_JSON_BUILD_PAIR_STRING("baz", "noansi"),
+                                                                SD_JSON_BUILD_PAIR_STRING("kkk", "thisisgrey")))));
+        ASSERT_TRUE(sd_json_variant_equal(j, jj));
+
+        ASSERT_OK(sd_json_variant_dump(j, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO, /* f= */ NULL, /* prefix= */ NULL));
+
+        ASSERT_OK(setenvf("COLUMNS", /* overwrite= */ true, "%u", saved_columns));
+        ASSERT_OK(setenvf("SYSTEMD_COLORS", /* overwrite= */ true, "%i", saved_color));
+        ASSERT_OK(set_unset_env("TERM", saved_term, /* overwrite= */ true));
+}
+
 static int intro(void) {
-        ASSERT_OK(setenv("SYSTEMD_COLORS", "0", 1));
-        ASSERT_OK(setenv("COLUMNS", "40", 1));
+        ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "0", /* overwrite= */ true));
+        ASSERT_OK_ERRNO(setenv("COLUMNS", "40", /* overwrite= */ true));
         return EXIT_SUCCESS;
 }