]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
format-table: add an explicit "vertical" mode
authorLennart Poettering <lennart@poettering.net>
Thu, 10 Nov 2022 11:52:08 +0000 (12:52 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 10 Nov 2022 22:09:18 +0000 (23:09 +0100)
Originally, the table formatting code was written to display a number of
records, one per line, and within each line multiple fields of the same
record. The first line contains the column names.

It was then started to be used in a "vertical" mode however,
i.e. with field names on the left instead of the top. Let's support such
a mode explicitly, so that we can provide systematic styling, and can
properly convert this mode to JSON.

A new constructor "table_new_vertical()" is added creating such
"vertical" tables. Internally, this is a table with two columns: "key"
and "value". When outputting this as JSON we'll output a single JSON
object, with key/value as fields. (Which is different from the
traditional output where we'd use the first line as JSON field names,
and output an array of objects).

A new cell type TABLE_FIELD is added for specifically marking the
"field" cells, i.e. the cells in the first column. We'll automatically
suffic ":" to these fields on output.

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

index 0549c9f12ef1153693954af5eecd71cbcfcdbbf8..d333bcf90b2829f447b58d3efc5e30bff74ea9ea 100644 (file)
@@ -132,6 +132,8 @@ struct Table {
         size_t n_cells;
 
         bool header;   /* Whether to show the header row? */
+        bool vertical; /* Whether to field names are on the left rather than the first line */
+
         TableErsatz ersatz; /* What to show when we have an empty cell or an invalid value that cannot be rendered. */
 
         size_t width;  /* If == 0 format this as wide as necessary. If SIZE_MAX format this to console
@@ -216,6 +218,38 @@ Table *table_new_internal(const char *first_header, ...) {
         return TAKE_PTR(t);
 }
 
+Table *table_new_vertical(void) {
+        _cleanup_(table_unrefp) Table *t = NULL;
+        TableCell *cell;
+
+        t = table_new_raw(2);
+        if (!t)
+                return NULL;
+
+        t->vertical = true;
+        t->header = false;
+
+        if (table_add_cell(t, &cell, TABLE_STRING, "key") < 0)
+                return NULL;
+
+        if (table_set_uppercase(t, cell, true) < 0)
+                return NULL;
+
+        if (table_set_align_percent(t, cell, 100) < 0)
+                return NULL;
+
+        if (table_add_cell(t, &cell, TABLE_STRING, "value") < 0)
+                return NULL;
+
+        if (table_set_uppercase(t, cell, true) < 0)
+                return NULL;
+
+        if (table_set_align_percent(t, cell, 0) < 0)
+                return NULL;
+
+        return TAKE_PTR(t);
+}
+
 static TableData *table_data_free(TableData *d) {
         assert(d);
 
@@ -260,6 +294,7 @@ static size_t table_data_size(TableDataType type, const void *data) {
 
         case TABLE_STRING:
         case TABLE_PATH:
+        case TABLE_FIELD:
                 return strlen(data) + 1;
 
         case TABLE_STRV:
@@ -840,6 +875,7 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) {
 
                 case TABLE_STRING:
                 case TABLE_PATH:
+                case TABLE_FIELD:
                         data = va_arg(ap, const char *);
                         break;
 
@@ -1241,6 +1277,7 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t
                 switch (a->type) {
 
                 case TABLE_STRING:
+                case TABLE_FIELD:
                         return strcmp(a->string, b->string);
 
                 case TABLE_PATH:
@@ -1416,20 +1453,33 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas
 
         case TABLE_STRING:
         case TABLE_PATH:
+        case TABLE_FIELD:
                 if (d->uppercase && !avoid_uppercasing) {
-                        d->formatted = new(char, strlen(d->string) + 1);
+                        d->formatted = new(char, strlen(d->string) + (d->type == TABLE_FIELD) + 1);
                         if (!d->formatted)
                                 return NULL;
 
                         char *q = d->formatted;
-                        for (char *p = d->string; *p; p++, q++)
-                                *q = (char) toupper((unsigned char) *p);
-                        *q = 0;
+                        for (char *p = d->string; *p; p++)
+                                *(q++) = (char) toupper((unsigned char) *p);
+
+                        if (d->type == TABLE_FIELD)
+                                *(q++) = ':';
 
+                        *q = 0;
                         return d->formatted;
-                }
+                } else {
+                        if (d->type == TABLE_FIELD) {
+                                d->formatted = strjoin(d->string, ":");
+                                if (!d->formatted)
+                                        return NULL;
+
+                                return d->formatted;
+                        }
 
-                return d->string;
+                        return d->string;
+                }
+                break;
 
         case TABLE_STRV:
                 if (strv_isempty(d->strv))
@@ -1982,6 +2032,9 @@ static const char* table_data_color(TableData *d) {
         if (table_data_isempty(d))
                 return ansi_grey();
 
+        if (d->type == TABLE_FIELD)
+                return ansi_bright_blue();
+
         return NULL;
 }
 
@@ -2494,6 +2547,7 @@ static int table_data_to_json(TableData *d, JsonVariant **ret) {
 
         case TABLE_STRING:
         case TABLE_PATH:
+        case TABLE_FIELD:
                 return json_variant_new_string(ret, d->string);
 
         case TABLE_STRV:
@@ -2629,21 +2683,23 @@ static char* string_to_json_field_name(const char *f) {
         return c;
 }
 
-static const char *table_get_json_field_name(Table *t, size_t column) {
+static const char *table_get_json_field_name(Table *t, size_t idx) {
         assert(t);
 
-        return column < t->n_json_fields ? t->json_fields[column] : NULL;
+        return idx < t->n_json_fields ? t->json_fields[idx] : NULL;
 }
 
-int table_to_json(Table *t, JsonVariant **ret) {
+static int table_to_json_regular(Table *t, JsonVariant **ret) {
         JsonVariant **rows = NULL, **elements = NULL;
         _cleanup_free_ size_t *sorted = NULL;
         size_t n_rows, display_columns;
         int r;
 
         assert(t);
+        assert(!t->vertical);
 
         /* Ensure we have no incomplete rows */
+        assert(t->n_columns > 0);
         assert(t->n_cells % t->n_columns == 0);
 
         n_rows = t->n_cells / t->n_columns;
@@ -2761,6 +2817,84 @@ finish:
         return r;
 }
 
+static int table_to_json_vertical(Table *t, JsonVariant **ret) {
+        JsonVariant **elements = NULL;
+        size_t n_elements = 0;
+        int r;
+
+        assert(t);
+        assert(t->vertical);
+
+        if (t->n_columns != 2)
+                return -EINVAL;
+
+        /* Ensure we have no incomplete rows */
+        assert(t->n_cells % t->n_columns == 0);
+
+        elements = new0(JsonVariant *, t->n_cells);
+        if (!elements) {
+                r = -ENOMEM;
+                goto finish;
+        }
+
+        for (size_t i = t->n_columns; i < t->n_cells; i++) {
+
+                if (i % t->n_columns == 0) {
+                        _cleanup_free_ char *mangled = NULL;
+                        const char *n;
+
+                        n = table_get_json_field_name(t, i / t->n_columns - 1);
+                        if (!n) {
+                                TableData *d = ASSERT_PTR(t->data[i]);
+
+                                if (d->type == TABLE_FIELD)
+                                        n = d->string;
+                                else {
+                                        n = table_data_format(t, d, /* avoid_uppercasing= */ true, SIZE_MAX, NULL);
+                                        if (!n) {
+                                                r = -ENOMEM;
+                                                goto finish;
+                                        }
+                                }
+
+                                mangled = string_to_json_field_name(n);
+                                if (!mangled) {
+                                        r = -ENOMEM;
+                                        goto finish;
+                                }
+
+                                n = mangled;
+                        }
+
+                        r = json_variant_new_string(elements + n_elements, n);
+                } else
+                        r = table_data_to_json(t->data[i], elements + n_elements);
+                if (r < 0)
+                        goto finish;
+
+                n_elements++;
+        }
+
+        r = json_variant_new_object(ret, elements, n_elements);
+
+finish:
+        if (elements) {
+                json_variant_unref_many(elements, n_elements);
+                free(elements);
+        }
+
+        return r;
+}
+
+int table_to_json(Table *t, JsonVariant **ret) {
+        assert(t);
+
+        if (t->vertical)
+                return table_to_json_vertical(t, ret);
+
+        return table_to_json_regular(t, ret);
+}
+
 int table_print_json(Table *t, FILE *f, JsonFormatFlags flags) {
         _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
         int r;
@@ -2809,7 +2943,7 @@ int table_print_with_pager(
         return 0;
 }
 
-int table_set_json_field_name(Table *t, size_t column, const char *name) {
+int table_set_json_field_name(Table *t, size_t idx, const char *name) {
         int r;
 
         assert(t);
@@ -2817,21 +2951,21 @@ int table_set_json_field_name(Table *t, size_t column, const char *name) {
         if (name) {
                 size_t m;
 
-                m = MAX(column + 1, t->n_json_fields);
+                m = MAX(idx + 1, t->n_json_fields);
                 if (!GREEDY_REALLOC0(t->json_fields, m))
                         return -ENOMEM;
 
-                r = free_and_strdup(t->json_fields + column, name);
+                r = free_and_strdup(t->json_fields + idx, name);
                 if (r < 0)
                         return r;
 
                 t->n_json_fields = m;
                 return r;
         } else {
-                if (column >= t->n_json_fields)
+                if (idx >= t->n_json_fields)
                         return 0;
 
-                t->json_fields[column] = mfree(t->json_fields[column]);
+                t->json_fields[idx] = mfree(t->json_fields[idx]);
                 return 1;
         }
 }
index c88a060dbea0ac0e18b07cafd0f73be21678c9d2..e90a7b7a6962e1ebefcbef4545f6fe9a93f604f3 100644 (file)
@@ -12,6 +12,7 @@
 typedef enum TableDataType {
         TABLE_EMPTY,
         TABLE_STRING,
+        TABLE_FIELD, /* used in vertical mode */
         TABLE_STRV,
         TABLE_STRV_WRAPPED,
         TABLE_PATH,
@@ -78,6 +79,7 @@ typedef struct TableCell TableCell;
 Table *table_new_internal(const char *first_header, ...) _sentinel_;
 #define table_new(...) table_new_internal(__VA_ARGS__, NULL)
 Table *table_new_raw(size_t n_columns);
+Table *table_new_vertical(void);
 Table *table_unref(Table *t);
 
 DEFINE_TRIVIAL_CLEANUP_FUNC(Table*, table_unref);
@@ -139,7 +141,7 @@ int table_print_json(Table *t, FILE *f, JsonFormatFlags json_flags);
 
 int table_print_with_pager(Table *t, JsonFormatFlags json_format_flags, PagerFlags pager_flags, bool show_header);
 
-int table_set_json_field_name(Table *t, size_t column, const char *name);
+int table_set_json_field_name(Table *t, size_t idx, const char *name);
 
 #define table_log_add_error(r) \
         log_error_errno(r, "Failed to add cells to table: %m")
index 60d5fee71fc2b452c2917ca79809ca3ff8d344ea..14341b97b4ea3960f43692efa2132863303025d8 100644 (file)
@@ -534,6 +534,37 @@ TEST(table) {
                                 "5min              5min              \n"));
 }
 
+TEST(vertical) {
+        _cleanup_(table_unrefp) Table *t = NULL;
+        _cleanup_free_ char *formatted = NULL;
+
+        assert_se(t = table_new_vertical());
+
+        assert_se(table_add_many(t,
+                                 TABLE_FIELD, "pfft aa", TABLE_STRING, "foo",
+                                 TABLE_FIELD, "uuu o", TABLE_SIZE, UINT64_C(1024),
+                                 TABLE_FIELD, "lllllllllllo", TABLE_STRING, "jjjjjjjjjjjjjjjjj") >= 0);
+
+        assert_se(table_set_json_field_name(t, 1, "dimpfelmoser") >= 0);
+
+        assert_se(table_format(t, &formatted) >= 0);
+
+        assert_se(streq(formatted,
+                        "     pfft aa: foo\n"
+                        "       uuu o: 1.0K\n"
+                        "lllllllllllo: jjjjjjjjjjjjjjjjj\n"));
+
+        _cleanup_(json_variant_unrefp) JsonVariant *a = NULL, *b = NULL;
+        assert_se(table_to_json(t, &a) >= 0);
+
+        assert_se(json_build(&b, JSON_BUILD_OBJECT(
+                                             JSON_BUILD_PAIR("pfft_aa", JSON_BUILD_STRING("foo")),
+                                             JSON_BUILD_PAIR("dimpfelmoser", JSON_BUILD_UNSIGNED(1024)),
+                                             JSON_BUILD_PAIR("lllllllllllo", JSON_BUILD_STRING("jjjjjjjjjjjjjjjjj")))) >= 0);
+
+        assert_se(json_variant_equal(a, b));
+}
+
 static int intro(void) {
         assert_se(setenv("SYSTEMD_COLORS", "0", 1) >= 0);
         assert_se(setenv("COLUMNS", "40", 1) >= 0);