diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 9e366ce513..9f6c46afb7 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 2147b82317..56dd18fa66 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 ff886a3d5e..a2e142791f 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; }