diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c index 3d2eeade74..f8ba743f73 100644 --- a/src/analyze/analyze-security.c +++ b/src/analyze/analyze-security.c @@ -1399,7 +1399,7 @@ static int assess(const struct security_info *info, Table *overview_table, Analy int r; if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) { - details_table = table_new("", "NAME", "DESCRIPTION", "WEIGHT", "BADNESS", "RANGE", "EXPOSURE"); + details_table = table_new(" ", "name", "description", "weight", "badness", "range", "exposure"); if (!details_table) return log_oom(); @@ -1973,7 +1973,7 @@ int analyze_security(sd_bus *bus, char **units, AnalyzeSecurityFlags flags) { assert(bus); if (strv_length(units) != 1) { - overview_table = table_new("UNIT", "EXPOSURE", "PREDICATE", "HAPPY"); + overview_table = table_new("unit", "exposure", "predicate", "happy"); if (!overview_table) return log_oom(); } diff --git a/src/busctl/busctl.c b/src/busctl/busctl.c index 6188216d28..0bcc89f6f2 100644 --- a/src/busctl/busctl.c +++ b/src/busctl/busctl.c @@ -1939,7 +1939,7 @@ static void json_dump_with_flags(JsonVariant *v, FILE *f) { json_variant_dump(v, (arg_json == JSON_PRETTY ? JSON_FORMAT_PRETTY : JSON_FORMAT_NEWLINE) | - colors_enabled() * JSON_FORMAT_COLOR, + JSON_FORMAT_COLOR_AUTO, f, NULL); } diff --git a/src/login/inhibit.c b/src/login/inhibit.c index 2394c5d937..03bbf3b826 100644 --- a/src/login/inhibit.c +++ b/src/login/inhibit.c @@ -83,7 +83,7 @@ static int print_inhibitors(sd_bus *bus) { if (r < 0) return log_error_errno(r, "Could not get active inhibitors: %s", bus_error_message(&error, r)); - table = table_new("WHO", "UID", "USER", "PID", "COMM", "WHAT", "WHY", "MODE"); + table = table_new("who", "uid", "user", "pid", "comm", "what", "why", "mode"); if (!table) return log_oom(); diff --git a/src/login/loginctl.c b/src/login/loginctl.c index fb5240d7d3..ab1b56201a 100644 --- a/src/login/loginctl.c +++ b/src/login/loginctl.c @@ -101,7 +101,10 @@ static int show_table(Table *table, const char *word) { table_set_header(table, arg_legend); - r = table_print(table, NULL); + if (OUTPUT_MODE_IS_JSON(arg_output)) + r = table_print_json(table, NULL, output_mode_to_json_format_flags(arg_output) | JSON_FORMAT_COLOR_AUTO); + else + r = table_print(table, NULL); if (r < 0) return log_error_errno(r, "Failed to show table: %m"); } @@ -143,7 +146,7 @@ static int list_sessions(int argc, char *argv[], void *userdata) { if (r < 0) return bus_log_parse_error(r); - table = table_new("SESSION", "UID", "USER", "SEAT", "TTY"); + table = table_new("session", "uid", "user", "seat", "tty"); if (!table) return log_oom(); @@ -224,7 +227,7 @@ static int list_users(int argc, char *argv[], void *userdata) { if (r < 0) return bus_log_parse_error(r); - table = table_new("UID", "USER"); + table = table_new("uid", "user"); if (!table) return log_oom(); @@ -281,7 +284,7 @@ static int list_seats(int argc, char *argv[], void *userdata) { if (r < 0) return bus_log_parse_error(r); - table = table_new("SEAT"); + table = table_new("seat"); if (!table) return log_oom(); @@ -1434,6 +1437,10 @@ static int parse_argv(int argc, char *argv[]) { if (arg_output < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown output '%s'.", optarg); + + if (OUTPUT_MODE_IS_JSON(arg_output)) + arg_legend = false; + break; case ARG_NO_PAGER: diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 5023acb5e2..c5fe01406e 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -259,7 +259,10 @@ static int show_table(Table *table, const char *word) { table_set_header(table, arg_legend); - r = table_print(table, NULL); + if (OUTPUT_MODE_IS_JSON(arg_output)) + r = table_print_json(table, NULL, output_mode_to_json_format_flags(arg_output) | JSON_FORMAT_COLOR_AUTO); + else + r = table_print(table, NULL); if (r < 0) return log_error_errno(r, "Failed to show table: %m"); } @@ -297,7 +300,7 @@ static int list_machines(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Could not get machines: %s", bus_error_message(&error, r)); - table = table_new("MACHINE", "CLASS", "SERVICE", "OS", "VERSION", "ADDRESSES"); + table = table_new("machine", "class", "service", "os", "version", "addresses"); if (!table) return log_oom(); @@ -377,7 +380,7 @@ static int list_images(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Could not get images: %s", bus_error_message(&error, r)); - table = table_new("NAME", "TYPE", "RO", "USAGE", "CREATED", "MODIFIED"); + table = table_new("name", "type", "ro", "usage", "created", "modified"); if (!table) return log_oom(); @@ -2921,6 +2924,9 @@ static int parse_argv(int argc, char *argv[]) { if (arg_output < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown output '%s'.", optarg); + + if (OUTPUT_MODE_IS_JSON(arg_output)) + arg_legend = false; break; case ARG_NO_PAGER: diff --git a/src/portable/portablectl.c b/src/portable/portablectl.c index 02d3954897..4763c0d7d0 100644 --- a/src/portable/portablectl.c +++ b/src/portable/portablectl.c @@ -500,7 +500,7 @@ static int list_images(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Failed to list images: %s", bus_error_message(&error, r)); - table = table_new("NAME", "TYPE", "RO", "CRTIME", "MTIME", "USAGE", "STATE"); + table = table_new("name", "type", "ro", "crtime", "mtime", "usage", "state"); if (!table) return log_oom(); diff --git a/src/shared/format-table.c b/src/shared/format-table.c index e685efea0e..7d529801a1 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ +#include #include #include "alloc-util.h" @@ -58,6 +59,8 @@ typedef struct TableData { unsigned ellipsize_percent; /* 0 … 100, where to place the ellipsis when compression is needed */ unsigned align_percent; /* 0 … 100, where to pad with spaces when expanding is needed. 0: left-aligned, 100: right-aligned */ + bool uppercase; /* Uppercase string on display */ + const char *color; /* ANSI color string to use for this cell. When written to terminal should not move cursor. Will automatically be reset after the cell */ char *url; /* A URL to use for a clickable hyperlink */ char *formatted; /* A cached textual representation of the cell data, before ellipsation/alignment */ @@ -132,6 +135,7 @@ Table *table_new_raw(size_t n_columns) { Table *table_new_internal(const char *first_header, ...) { _cleanup_(table_unrefp) Table *t = NULL; size_t n_columns = 1; + const char *h; va_list ap; int r; @@ -139,8 +143,6 @@ Table *table_new_internal(const char *first_header, ...) { va_start(ap, first_header); for (;;) { - const char *h; - h = va_arg(ap, const char*); if (!h) break; @@ -153,19 +155,18 @@ Table *table_new_internal(const char *first_header, ...) { if (!t) return NULL; - r = table_add_cell(t, NULL, TABLE_STRING, first_header); - if (r < 0) - return NULL; - va_start(ap, first_header); - for (;;) { - const char *h; + for (h = first_header; h; h = va_arg(ap, const char*)) { + TableCell *cell; - h = va_arg(ap, const char*); - if (!h) - break; + r = table_add_cell(t, &cell, TABLE_STRING, h); + if (r < 0) { + va_end(ap); + return NULL; + } - r = table_add_cell(t, NULL, TABLE_STRING, h); + /* Make the table header uppercase */ + r = table_set_uppercase(t, cell, true); if (r < 0) { va_end(ap); return NULL; @@ -269,6 +270,14 @@ static bool table_data_matches( if (d->ellipsize_percent != ellipsize_percent) return false; + /* If a color/url/uppercase flag is set, refuse to merge */ + if (d->color) + return false; + if (d->url) + return false; + if (d->uppercase) + return false; + k = table_data_size(type, data); l = table_data_size(d->type, d->data); @@ -427,6 +436,7 @@ static int table_dedup_cell(Table *t, TableCell *cell) { nd->color = od->color; nd->url = TAKE_PTR(curl); + nd->uppercase = od->uppercase; table_data_unref(od); t->data[i] = nd; @@ -574,6 +584,27 @@ int table_set_url(Table *t, TableCell *cell, const char *url) { return free_and_replace(table_get_data(t, cell)->url, copy); } +int table_set_uppercase(Table *t, TableCell *cell, bool b) { + TableData *d; + int r; + + assert(t); + assert(cell); + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + assert_se(d = table_get_data(t, cell)); + + if (d->uppercase == b) + return 0; + + d->formatted = mfree(d->formatted); + d->uppercase = b; + return 1; +} + int table_update(Table *t, TableCell *cell, TableDataType type, const void *data) { _cleanup_free_ char *curl = NULL; TableData *nd, *od; @@ -607,6 +638,7 @@ int table_update(Table *t, TableCell *cell, TableDataType type, const void *data nd->color = od->color; nd->url = TAKE_PTR(curl); + nd->uppercase = od->uppercase; table_data_unref(od); t->data[i] = nd; @@ -858,6 +890,20 @@ static const char *table_data_format(TableData *d) { return ""; case TABLE_STRING: + if (d->uppercase) { + char *p, *q; + + d->formatted = new(char, strlen(d->string) + 1); + if (!d->formatted) + return NULL; + + for (p = d->string, q = d->formatted; *p; p++, q++) + *q = (char) toupper((unsigned char) *p); + *q = 0; + + return d->formatted; + } + return d->string; case TABLE_BOOLEAN: @@ -884,7 +930,7 @@ static const char *table_data_format(TableData *d) { if (!p) return NULL; - if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timestamp, 0)) + if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timespan, 0)) return "n/a"; d->formatted = TAKE_PTR(p); @@ -1414,3 +1460,166 @@ const void* table_get_at(Table *t, size_t row, size_t column) { return table_get(t, cell); } + +static int table_data_to_json(TableData *d, JsonVariant **ret) { + + switch (d->type) { + + case TABLE_EMPTY: + return json_variant_new_null(ret); + + case TABLE_STRING: + return json_variant_new_string(ret, d->string); + + case TABLE_BOOLEAN: + return json_variant_new_boolean(ret, d->boolean); + + case TABLE_TIMESTAMP: + if (d->timestamp == USEC_INFINITY) + return json_variant_new_null(ret); + + return json_variant_new_unsigned(ret, d->timestamp); + + case TABLE_TIMESPAN: + if (d->timespan == USEC_INFINITY) + return json_variant_new_null(ret); + + return json_variant_new_unsigned(ret, d->timespan); + + case TABLE_SIZE: + if (d->size == (size_t) -1) + return json_variant_new_null(ret); + + return json_variant_new_unsigned(ret, d->size); + + case TABLE_UINT32: + return json_variant_new_unsigned(ret, d->uint32); + + case TABLE_UINT64: + return json_variant_new_unsigned(ret, d->uint64); + + case TABLE_PERCENT: + return json_variant_new_integer(ret, d->percent); + + default: + return -EINVAL; + } +} + +int table_to_json(Table *t, JsonVariant **ret) { + JsonVariant **rows = NULL, **elements = NULL; + _cleanup_free_ size_t *sorted = NULL; + size_t n_rows, i, j, display_columns; + int r; + + assert(t); + + /* Ensure we have no incomplete rows */ + assert(t->n_cells % t->n_columns == 0); + + n_rows = t->n_cells / t->n_columns; + assert(n_rows > 0); /* at least the header row must be complete */ + + if (t->sort_map) { + /* If sorting is requested, let's calculate an index table we use to lookup the actual index to display with. */ + + sorted = new(size_t, n_rows); + if (!sorted) { + r = -ENOMEM; + goto finish; + } + + for (i = 0; i < n_rows; i++) + sorted[i] = i * t->n_columns; + + typesafe_qsort_r(sorted, n_rows, table_data_compare, t); + } + + if (t->display_map) + display_columns = t->n_display_map; + else + display_columns = t->n_columns; + assert(display_columns > 0); + + elements = new0(JsonVariant*, display_columns * 2); + if (!elements) { + r = -ENOMEM; + goto finish; + } + + for (j = 0; j < display_columns; j++) { + TableData *d; + + assert_se(d = t->data[t->display_map ? t->display_map[j] : j]); + + r = table_data_to_json(d, elements + j*2); + if (r < 0) + goto finish; + } + + rows = new0(JsonVariant*, n_rows-1); + if (!rows) { + r = -ENOMEM; + goto finish; + } + + for (i = 1; i < n_rows; i++) { + TableData **row; + + if (sorted) + row = t->data + sorted[i]; + else + row = t->data + i * t->n_columns; + + for (j = 0; j < display_columns; j++) { + TableData *d; + size_t k; + + assert_se(d = row[t->display_map ? t->display_map[j] : j]); + + k = j*2+1; + elements[k] = json_variant_unref(elements[k]); + + r = table_data_to_json(d, elements + k); + if (r < 0) + goto finish; + } + + r = json_variant_new_object(rows + i - 1, elements, display_columns * 2); + if (r < 0) + goto finish; + } + + r = json_variant_new_array(ret, rows, n_rows - 1); + +finish: + if (rows) { + json_variant_unref_many(rows, n_rows-1); + free(rows); + } + + if (elements) { + json_variant_unref_many(elements, display_columns*2); + free(elements); + } + + return r; +} + +int table_print_json(Table *t, FILE *f, JsonFormatFlags flags) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(t); + + if (!f) + f = stdout; + + r = table_to_json(t, &v); + if (r < 0) + return r; + + json_variant_dump(v, flags, f, NULL); + + return fflush_and_check(f); +} diff --git a/src/shared/format-table.h b/src/shared/format-table.h index a2bb2e0846..5ff247953b 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -5,6 +5,7 @@ #include #include +#include "json.h" #include "macro.h" typedef enum TableDataType { @@ -45,6 +46,7 @@ int table_set_align_percent(Table *t, TableCell *cell, unsigned percent); int table_set_ellipsize_percent(Table *t, TableCell *cell, unsigned percent); int table_set_color(Table *t, TableCell *cell, const char *color); int table_set_url(Table *t, TableCell *cell, const char *color); +int table_set_uppercase(Table *t, TableCell *cell, bool b); int table_update(Table *t, TableCell *cell, TableDataType type, const void *data); @@ -71,3 +73,6 @@ TableCell *table_get_cell(Table *t, size_t row, size_t column); const void *table_get(Table *t, TableCell *cell); const void *table_get_at(Table *t, size_t row, size_t column); + +int table_to_json(Table *t, JsonVariant **ret); +int table_print_json(Table *t, FILE *f, unsigned json_flags); diff --git a/src/shared/json.c b/src/shared/json.c index 420555ae54..c410b62008 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1236,7 +1236,7 @@ int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *r return 0; } -static int print_source(FILE *f, JsonVariant *v, unsigned flags, bool whitespace) { +static int print_source(FILE *f, JsonVariant *v, JsonFormatFlags flags, bool whitespace) { size_t w, k; if (!FLAGS_SET(flags, JSON_FORMAT_SOURCE|JSON_FORMAT_PRETTY)) @@ -1289,7 +1289,7 @@ static int print_source(FILE *f, JsonVariant *v, unsigned flags, bool whitespace return 0; } -static int json_format(FILE *f, JsonVariant *v, unsigned flags, const char *prefix) { +static int json_format(FILE *f, JsonVariant *v, JsonFormatFlags flags, const char *prefix) { int r; assert(f); @@ -1546,7 +1546,7 @@ static int json_format(FILE *f, JsonVariant *v, unsigned flags, const char *pref return 0; } -int json_variant_format(JsonVariant *v, unsigned flags, char **ret) { +int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret) { _cleanup_free_ char *s = NULL; size_t sz = 0; int r; @@ -1576,7 +1576,7 @@ int json_variant_format(JsonVariant *v, unsigned flags, char **ret) { return (int) sz; } -void json_variant_dump(JsonVariant *v, unsigned flags, FILE *f, const char *prefix) { +void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const char *prefix) { if (!v) return; @@ -1585,6 +1585,9 @@ void json_variant_dump(JsonVariant *v, unsigned flags, FILE *f, const char *pref print_source(f, v, flags, false); + if (((flags & (JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_COLOR)) == JSON_FORMAT_COLOR_AUTO) && colors_enabled()) + flags |= JSON_FORMAT_COLOR; + if (flags & JSON_FORMAT_SSE) fputs("data: ", f); if (flags & JSON_FORMAT_SEQ) diff --git a/src/shared/json.h b/src/shared/json.h index 4a21a411ac..4eba91c272 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -150,17 +150,18 @@ struct json_variant_foreach_state { int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *ret_line, unsigned *ret_column); -enum { - JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */ - JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */ - JSON_FORMAT_COLOR = 1 << 2, /* insert ANSI color sequences */ - JSON_FORMAT_SOURCE = 1 << 3, /* prefix with source filename/line/column */ - JSON_FORMAT_SSE = 1 << 4, /* prefix/suffix with W3C server-sent events */ - JSON_FORMAT_SEQ = 1 << 5, /* prefix/suffix with RFC 7464 application/json-seq */ -}; +typedef enum JsonFormatFlags { + JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */ + JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */ + JSON_FORMAT_COLOR = 1 << 2, /* insert ANSI color sequences */ + JSON_FORMAT_COLOR_AUTO = 1 << 3, /* insetr ANSI color sequences if colors_enabled() says so */ + JSON_FORMAT_SOURCE = 1 << 4, /* prefix with source filename/line/column */ + JSON_FORMAT_SSE = 1 << 5, /* prefix/suffix with W3C server-sent events */ + JSON_FORMAT_SEQ = 1 << 6, /* prefix/suffix with RFC 7464 application/json-seq */ +} JsonFormatFlags; -int json_variant_format(JsonVariant *v, unsigned flags, char **ret); -void json_variant_dump(JsonVariant *v, unsigned flags, FILE *f, const char *prefix); +int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret); +void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const char *prefix); int json_parse(const char *string, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); diff --git a/src/shared/logs-show.c b/src/shared/logs-show.c index 41d484fa16..76457bb475 100644 --- a/src/shared/logs-show.c +++ b/src/shared/logs-show.c @@ -946,10 +946,7 @@ static int output_json( } json_variant_dump(object, - (mode == OUTPUT_JSON_SSE ? JSON_FORMAT_SSE : - mode == OUTPUT_JSON_SEQ ? JSON_FORMAT_SEQ : - mode == OUTPUT_JSON_PRETTY ? JSON_FORMAT_PRETTY : - JSON_FORMAT_NEWLINE) | + output_mode_to_json_format_flags(mode) | (FLAGS_SET(flags, OUTPUT_COLOR) ? JSON_FORMAT_COLOR : 0), f, NULL); diff --git a/src/shared/output-mode.c b/src/shared/output-mode.c index 9463d185f0..107b345538 100644 --- a/src/shared/output-mode.c +++ b/src/shared/output-mode.c @@ -3,6 +3,24 @@ #include "output-mode.h" #include "string-table.h" +JsonFormatFlags output_mode_to_json_format_flags(OutputMode m) { + + switch (m) { + + case OUTPUT_JSON_SSE: + return JSON_FORMAT_SSE; + + case OUTPUT_JSON_SEQ: + return JSON_FORMAT_SEQ; + + case OUTPUT_JSON_PRETTY: + return JSON_FORMAT_PRETTY; + + default: + return JSON_FORMAT_NEWLINE; + } +} + static const char *const output_mode_table[_OUTPUT_MODE_MAX] = { [OUTPUT_SHORT] = "short", [OUTPUT_SHORT_FULL] = "short-full", diff --git a/src/shared/output-mode.h b/src/shared/output-mode.h index 3cbaeadde6..00b6032056 100644 --- a/src/shared/output-mode.h +++ b/src/shared/output-mode.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #pragma once +#include "json.h" #include "macro.h" typedef enum OutputMode { @@ -23,6 +24,10 @@ typedef enum OutputMode { _OUTPUT_MODE_INVALID = -1 } OutputMode; +static inline bool OUTPUT_MODE_IS_JSON(OutputMode m) { + return IN_SET(m, OUTPUT_JSON, OUTPUT_JSON_PRETTY, OUTPUT_JSON_SSE, OUTPUT_JSON_SEQ); +} + /* The output flags definitions are shared by the logs and process tree output. Some apply to both, some only to the * logs output, others only to the process tree output. */ @@ -39,5 +44,7 @@ typedef enum OutputFlags { OUTPUT_NO_HOSTNAME = 1 << 9, } OutputFlags; +JsonFormatFlags output_mode_to_json_format_flags(OutputMode m); + const char* output_mode_to_string(OutputMode m) _const_; OutputMode output_mode_from_string(const char *s) _pure_; diff --git a/src/test/test-format-table.c b/src/test/test-format-table.c index 2527cacbbd..5bede5c75b 100644 --- a/src/test/test-format-table.c +++ b/src/test/test-format-table.c @@ -9,7 +9,7 @@ static void test_issue_9549(void) { _cleanup_(table_unrefp) Table *table = NULL; _cleanup_free_ char *formatted = NULL; - assert_se(table = table_new("NAME", "TYPE", "RO", "USAGE", "CREATED", "MODIFIED")); + assert_se(table = table_new("name", "type", "ro", "usage", "created", "modified")); assert_se(table_set_align_percent(table, TABLE_HEADER_CELL(3), 100) >= 0); assert_se(table_add_many(table, TABLE_STRING, "foooo", @@ -36,7 +36,7 @@ int main(int argc, char *argv[]) { assert_se(setenv("COLUMNS", "40", 1) >= 0); - assert_se(t = table_new("ONE", "TWO", "THREE")); + assert_se(t = table_new("one", "two", "three")); assert_se(table_set_align_percent(t, TABLE_HEADER_CELL(2), 100) >= 0);