/* SPDX-License-Identifier: LGPL-2.1+ */
-#include <stdio_ext.h>
+#include <ctype.h>
+#include <net/if.h>
#include "alloc-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-table.h"
+#include "format-util.h"
#include "gunicode.h"
+#include "in-addr-util.h"
+#include "memory-util.h"
#include "pager.h"
#include "parse-util.h"
#include "pretty-print.h"
+#include "sort-util.h"
#include "string-util.h"
+#include "strxcpyx.h"
#include "terminal-util.h"
#include "time-util.h"
#include "utf8.h"
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 */
usec_t timespan;
uint64_t size;
char string[0];
+ int int_val;
+ int32_t int32;
+ int64_t int64;
+ unsigned uint_val;
uint32_t uint32;
+ uint64_t uint64;
+ int percent; /* we use 'int' as datatype for percent values in order to match the result of parse_percent() */
+ int ifindex;
+ union in_addr_union address;
/* … add more here as we start supporting more cell data types … */
};
} TableData;
size_t *sort_map; /* The columns to order rows by, in order of preference. */
size_t n_sort_map;
+
+ bool *reverse_map;
};
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;
va_start(ap, first_header);
for (;;) {
- const char *h;
-
h = va_arg(ap, const char*);
if (!h)
break;
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;
free(t->data);
free(t->display_map);
free(t->sort_map);
+ free(t->reverse_map);
return mfree(t);
}
return sizeof(bool);
case TABLE_TIMESTAMP:
+ case TABLE_TIMESTAMP_UTC:
+ case TABLE_TIMESTAMP_RELATIVE:
case TABLE_TIMESPAN:
+ case TABLE_TIMESPAN_MSEC:
return sizeof(usec_t);
case TABLE_SIZE:
+ case TABLE_INT64:
+ case TABLE_UINT64:
+ case TABLE_BPS:
return sizeof(uint64_t);
+ case TABLE_INT32:
case TABLE_UINT32:
return sizeof(uint32_t);
+ case TABLE_INT:
+ case TABLE_UINT:
+ case TABLE_PERCENT:
+ case TABLE_IFINDEX:
+ return sizeof(int);
+
+ case TABLE_IN_ADDR:
+ return sizeof(struct in_addr);
+
+ case TABLE_IN6_ADDR:
+ return sizeof(struct in6_addr);
+
default:
assert_not_reached("Uh? Unexpected cell type");
}
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);
if (k != l)
return false;
- return memcmp(data, d->data, l) == 0;
+ return memcmp_safe(data, d->data, l) == 0;
}
static TableData *table_data_new(
return 0;
}
+int table_add_cell_stringf(Table *t, TableCell **ret_cell, const char *format, ...) {
+ _cleanup_free_ char *buffer = NULL;
+ va_list ap;
+ int r;
+
+ va_start(ap, format);
+ r = vasprintf(&buffer, format, ap);
+ va_end(ap);
+ if (r < 0)
+ return -ENOMEM;
+
+ return table_add_cell(t, ret_cell, TABLE_STRING, buffer);
+}
+
int table_dup_cell(Table *t, TableCell *cell) {
size_t i;
nd->color = od->color;
nd->url = TAKE_PTR(curl);
+ nd->uppercase = od->uppercase;
table_data_unref(od);
t->data[i] = nd;
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;
+ size_t i;
+
+ assert(t);
+ assert(cell);
+
+ i = TABLE_CELL_TO_INDEX(cell);
+ if (i >= t->n_cells)
+ return -ENXIO;
+
+ assert_se(od = t->data[i]);
+
+ if (od->url) {
+ curl = strdup(od->url);
+ if (!curl)
+ return -ENOMEM;
+ }
+
+ nd = table_data_new(
+ type,
+ data,
+ od->minimum_width,
+ od->maximum_width,
+ od->weight,
+ od->align_percent,
+ od->ellipsize_percent);
+ if (!nd)
+ return -ENOMEM;
+
+ nd->color = od->color;
+ nd->url = TAKE_PTR(curl);
+ nd->uppercase = od->uppercase;
+
+ table_data_unref(od);
+ t->data[i] = nd;
+
+ return 0;
+}
+
int table_add_many_internal(Table *t, TableDataType first_type, ...) {
TableDataType type;
va_list ap;
+ TableCell *last_cell = NULL;
int r;
assert(t);
union {
uint64_t size;
usec_t usec;
+ int int_val;
+ int32_t int32;
+ int64_t int64;
+ unsigned uint_val;
uint32_t uint32;
+ uint64_t uint64;
+ int percent;
+ int ifindex;
bool b;
+ union in_addr_union address;
} buffer;
switch (type) {
break;
case TABLE_TIMESTAMP:
+ case TABLE_TIMESTAMP_UTC:
+ case TABLE_TIMESTAMP_RELATIVE:
case TABLE_TIMESPAN:
+ case TABLE_TIMESPAN_MSEC:
buffer.usec = va_arg(ap, usec_t);
data = &buffer.usec;
break;
case TABLE_SIZE:
+ case TABLE_BPS:
buffer.size = va_arg(ap, uint64_t);
data = &buffer.size;
break;
+ case TABLE_INT:
+ buffer.int_val = va_arg(ap, int);
+ data = &buffer.int_val;
+ break;
+
+ case TABLE_INT32:
+ buffer.int32 = va_arg(ap, int32_t);
+ data = &buffer.int32;
+ break;
+
+ case TABLE_INT64:
+ buffer.int64 = va_arg(ap, int64_t);
+ data = &buffer.int64;
+ break;
+
+ case TABLE_UINT:
+ buffer.uint_val = va_arg(ap, unsigned);
+ data = &buffer.uint_val;
+ break;
+
case TABLE_UINT32:
buffer.uint32 = va_arg(ap, uint32_t);
data = &buffer.uint32;
break;
+ case TABLE_UINT64:
+ buffer.uint64 = va_arg(ap, uint64_t);
+ data = &buffer.uint64;
+ break;
+
+ case TABLE_PERCENT:
+ buffer.percent = va_arg(ap, int);
+ data = &buffer.percent;
+ break;
+
+ case TABLE_IFINDEX:
+ buffer.ifindex = va_arg(ap, int);
+ data = &buffer.ifindex;
+ break;
+
+ case TABLE_IN_ADDR:
+ buffer.address = *va_arg(ap, union in_addr_union *);
+ data = &buffer.address.in;
+ break;
+
+ case TABLE_IN6_ADDR:
+ buffer.address = *va_arg(ap, union in_addr_union *);
+ data = &buffer.address.in6;
+ break;
+
+ case TABLE_SET_MINIMUM_WIDTH: {
+ size_t w = va_arg(ap, size_t);
+
+ r = table_set_minimum_width(t, last_cell, w);
+ break;
+ }
+
+ case TABLE_SET_MAXIMUM_WIDTH: {
+ size_t w = va_arg(ap, size_t);
+ r = table_set_maximum_width(t, last_cell, w);
+ break;
+ }
+
+ case TABLE_SET_WEIGHT: {
+ unsigned w = va_arg(ap, unsigned);
+ r = table_set_weight(t, last_cell, w);
+ break;
+ }
+
+ case TABLE_SET_ALIGN_PERCENT: {
+ unsigned p = va_arg(ap, unsigned);
+ r = table_set_align_percent(t, last_cell, p);
+ break;
+ }
+
+ case TABLE_SET_ELLIPSIZE_PERCENT: {
+ unsigned p = va_arg(ap, unsigned);
+ r = table_set_ellipsize_percent(t, last_cell, p);
+ break;
+ }
+
+ case TABLE_SET_COLOR: {
+ const char *c = va_arg(ap, const char*);
+ r = table_set_color(t, last_cell, c);
+ break;
+ }
+
+ case TABLE_SET_URL: {
+ const char *u = va_arg(ap, const char*);
+ r = table_set_url(t, last_cell, u);
+ break;
+ }
+
+ case TABLE_SET_UPPERCASE: {
+ int u = va_arg(ap, int);
+ r = table_set_uppercase(t, last_cell, u);
+ break;
+ }
+
case _TABLE_DATA_TYPE_MAX:
/* Used as end marker */
va_end(ap);
assert_not_reached("Uh? Unexpected data type.");
}
- r = table_add_cell(t, NULL, type, data);
+ if (type < _TABLE_DATA_TYPE_MAX)
+ r = table_add_cell(t, &last_cell, type, data);
+
if (r < 0) {
va_end(ap);
return r;
return 0;
case TABLE_TIMESTAMP:
+ case TABLE_TIMESTAMP_UTC:
+ case TABLE_TIMESTAMP_RELATIVE:
return CMP(a->timestamp, b->timestamp);
case TABLE_TIMESPAN:
+ case TABLE_TIMESPAN_MSEC:
return CMP(a->timespan, b->timespan);
case TABLE_SIZE:
+ case TABLE_BPS:
return CMP(a->size, b->size);
+ case TABLE_INT:
+ return CMP(a->int_val, b->int_val);
+
+ case TABLE_INT32:
+ return CMP(a->int32, b->int32);
+
+ case TABLE_INT64:
+ return CMP(a->int64, b->int64);
+
+ case TABLE_UINT:
+ return CMP(a->uint_val, b->uint_val);
+
case TABLE_UINT32:
return CMP(a->uint32, b->uint32);
+ case TABLE_UINT64:
+ return CMP(a->uint64, b->uint64);
+
+ case TABLE_PERCENT:
+ return CMP(a->percent, b->percent);
+
+ case TABLE_IFINDEX:
+ return CMP(a->ifindex, b->ifindex);
+
+ case TABLE_IN_ADDR:
+ return CMP(a->address.in.s_addr, b->address.in.s_addr);
+
+ case TABLE_IN6_ADDR:
+ return memcmp(&a->address.in6, &b->address.in6, FAMILY_ADDRESS_SIZE(AF_INET6));
+
default:
;
}
}
- /* Generic fallback using the orginal order in which the cells where added. */
+ /* Generic fallback using the original order in which the cells where added. */
return CMP(index_a, index_b);
}
r = cell_data_compare(d, *a, dd, *b);
if (r != 0)
- return r;
+ return t->reverse_map && t->reverse_map[t->sort_map[i]] ? -r : r;
}
/* Order identical lines by the order there were originally added in */
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:
return yes_no(d->boolean);
- case TABLE_TIMESTAMP: {
+ case TABLE_TIMESTAMP:
+ case TABLE_TIMESTAMP_UTC:
+ case TABLE_TIMESTAMP_RELATIVE: {
_cleanup_free_ char *p;
+ char *ret;
p = new(char, FORMAT_TIMESTAMP_MAX);
if (!p)
return NULL;
- if (!format_timestamp(p, FORMAT_TIMESTAMP_MAX, d->timestamp))
+ if (d->type == TABLE_TIMESTAMP)
+ ret = format_timestamp(p, FORMAT_TIMESTAMP_MAX, d->timestamp);
+ else if (d->type == TABLE_TIMESTAMP_UTC)
+ ret = format_timestamp_utc(p, FORMAT_TIMESTAMP_MAX, d->timestamp);
+ else
+ ret = format_timestamp_relative(p, FORMAT_TIMESTAMP_MAX, d->timestamp);
+ if (!ret)
return "n/a";
d->formatted = TAKE_PTR(p);
break;
}
- case TABLE_TIMESPAN: {
+ case TABLE_TIMESPAN:
+ case TABLE_TIMESPAN_MSEC: {
_cleanup_free_ char *p;
p = new(char, FORMAT_TIMESPAN_MAX);
if (!p)
return NULL;
- if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timestamp, 0))
+ if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timespan,
+ d->type == TABLE_TIMESPAN ? 0 : USEC_PER_MSEC))
return "n/a";
d->formatted = TAKE_PTR(p);
break;
}
+ case TABLE_BPS: {
+ _cleanup_free_ char *p;
+ size_t n;
+
+ p = new(char, FORMAT_BYTES_MAX+2);
+ if (!p)
+ return NULL;
+
+ if (!format_bytes_full(p, FORMAT_BYTES_MAX, d->size, 0))
+ return "n/a";
+
+ n = strlen(p);
+ strscpy(p + n, FORMAT_BYTES_MAX + 2 - n, "bps");
+
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
+ case TABLE_INT: {
+ _cleanup_free_ char *p;
+
+ p = new(char, DECIMAL_STR_WIDTH(d->int_val) + 1);
+ if (!p)
+ return NULL;
+
+ sprintf(p, "%i", d->int_val);
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
+ case TABLE_INT32: {
+ _cleanup_free_ char *p;
+
+ p = new(char, DECIMAL_STR_WIDTH(d->int32) + 1);
+ if (!p)
+ return NULL;
+
+ sprintf(p, "%" PRIi32, d->int32);
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
+ case TABLE_INT64: {
+ _cleanup_free_ char *p;
+
+ p = new(char, DECIMAL_STR_WIDTH(d->int64) + 1);
+ if (!p)
+ return NULL;
+
+ sprintf(p, "%" PRIi64, d->int64);
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
+ case TABLE_UINT: {
+ _cleanup_free_ char *p;
+
+ p = new(char, DECIMAL_STR_WIDTH(d->uint_val) + 1);
+ if (!p)
+ return NULL;
+
+ sprintf(p, "%u", d->uint_val);
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
case TABLE_UINT32: {
_cleanup_free_ char *p;
break;
}
+ case TABLE_UINT64: {
+ _cleanup_free_ char *p;
+
+ p = new(char, DECIMAL_STR_WIDTH(d->uint64) + 1);
+ if (!p)
+ return NULL;
+
+ sprintf(p, "%" PRIu64, d->uint64);
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
+ case TABLE_PERCENT: {
+ _cleanup_free_ char *p;
+
+ p = new(char, DECIMAL_STR_WIDTH(d->percent) + 2);
+ if (!p)
+ return NULL;
+
+ sprintf(p, "%i%%" , d->percent);
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
+ case TABLE_IFINDEX: {
+ _cleanup_free_ char *p;
+ char name[IF_NAMESIZE + 1];
+
+ if (format_ifname(d->ifindex, name)) {
+ p = strdup(name);
+ if (!p)
+ return NULL;
+ } else {
+ if (asprintf(&p, "%i" , d->ifindex) < 0)
+ return NULL;
+ }
+
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
+ case TABLE_IN_ADDR:
+ case TABLE_IN6_ADDR: {
+ _cleanup_free_ char *p = NULL;
+
+ if (in_addr_to_string(d->type == TABLE_IN_ADDR ? AF_INET : AF_INET6,
+ &d->address, &p) < 0)
+ return NULL;
+
+ d->formatted = TAKE_PTR(p);
+ break;
+ }
+
default:
assert_not_reached("Unexpected type?");
}
field = buffer;
}
+ if (row == t->data) /* underline header line fully, including the column separator */
+ fputs(ansi_underline(), f);
+
if (j > 0)
fputc(' ', f); /* column separator */
- if (d->color && colors_enabled())
+ if (d->color && colors_enabled()) {
+ if (row == t->data) /* first undo header underliner */
+ fputs(ANSI_NORMAL, f);
+
fputs(d->color, f);
+ }
fputs(field, f);
- if (d->color && colors_enabled())
+ if (colors_enabled() && (d->color || row == t->data))
fputs(ANSI_NORMAL, f);
}
size_t sz = 0;
int r;
- f = open_memstream(&buf, &sz);
+ f = open_memstream_unlocked(&buf, &sz);
if (!f)
return -ENOMEM;
- (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
-
r = table_print(t, f);
if (r < 0)
return r;
assert(t->n_columns > 0);
return t->n_columns;
}
+
+int table_set_reverse(Table *t, size_t column, bool b) {
+ assert(t);
+ assert(column < t->n_columns);
+
+ if (!t->reverse_map) {
+ if (!b)
+ return 0;
+
+ t->reverse_map = new0(bool, t->n_columns);
+ if (!t->reverse_map)
+ return -ENOMEM;
+ }
+
+ t->reverse_map[column] = b;
+ return 0;
+}
+
+TableCell *table_get_cell(Table *t, size_t row, size_t column) {
+ size_t i;
+
+ assert(t);
+
+ if (column >= t->n_columns)
+ return NULL;
+
+ i = row * t->n_columns + column;
+ if (i >= t->n_cells)
+ return NULL;
+
+ return TABLE_INDEX_TO_CELL(i);
+}
+
+const void *table_get(Table *t, TableCell *cell) {
+ TableData *d;
+
+ assert(t);
+
+ d = table_get_data(t, cell);
+ if (!d)
+ return NULL;
+
+ return d->data;
+}
+
+const void* table_get_at(Table *t, size_t row, size_t column) {
+ TableCell *cell;
+
+ cell = table_get_cell(t, row, column);
+ if (!cell)
+ return NULL;
+
+ 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:
+ case TABLE_TIMESTAMP_UTC:
+ case TABLE_TIMESTAMP_RELATIVE:
+ if (d->timestamp == USEC_INFINITY)
+ return json_variant_new_null(ret);
+
+ return json_variant_new_unsigned(ret, d->timestamp);
+
+ case TABLE_TIMESPAN:
+ case TABLE_TIMESPAN_MSEC:
+ if (d->timespan == USEC_INFINITY)
+ return json_variant_new_null(ret);
+
+ return json_variant_new_unsigned(ret, d->timespan);
+
+ case TABLE_SIZE:
+ case TABLE_BPS:
+ if (d->size == (size_t) -1)
+ return json_variant_new_null(ret);
+
+ return json_variant_new_unsigned(ret, d->size);
+
+ case TABLE_INT:
+ return json_variant_new_integer(ret, d->int_val);
+
+ case TABLE_INT32:
+ return json_variant_new_integer(ret, d->int32);
+
+ case TABLE_INT64:
+ return json_variant_new_integer(ret, d->int64);
+
+ case TABLE_UINT:
+ return json_variant_new_unsigned(ret, d->uint_val);
+
+ 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);
+
+ case TABLE_IFINDEX:
+ return json_variant_new_integer(ret, d->ifindex);
+
+ case TABLE_IN_ADDR:
+ return json_variant_new_array_bytes(ret, &d->address, FAMILY_ADDRESS_SIZE(AF_INET));
+
+ case TABLE_IN6_ADDR:
+ return json_variant_new_array_bytes(ret, &d->address, FAMILY_ADDRESS_SIZE(AF_INET6));
+
+ 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);
+}