From: Lennart Poettering Date: Thu, 15 Jan 2026 07:51:05 +0000 (+0100) Subject: format-table: add new string cell type that accepts ANSI sequences X-Git-Tag: v260-rc1~336^2~4 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f42dc5ea18e920c70a8571309bfd66757f5e01cb;p=thirdparty%2Fsystemd.git format-table: add new string cell type that accepts ANSI sequences 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. --- diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 9e366ce5134..9f6c46afb75 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -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; } diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 2147b823178..56dd18fa66d 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -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, diff --git a/src/test/test-format-table.c b/src/test/test-format-table.c index ff886a3d5ef..a2e142791ff 100644 --- a/src/test/test-format-table.c +++ b/src/test/test-format-table.c @@ -4,6 +4,8 @@ #include #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; }