From b77f5e2773f0ba12022315a0edde3d2c9f062923 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 19:49:50 +0200 Subject: [PATCH 01/11] locale: add ellipsis as special glyph --- src/basic/locale-util.c | 4 +++- src/basic/locale-util.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/basic/locale-util.c b/src/basic/locale-util.c index 01ab44d9ae..9c3f757da7 100644 --- a/src/basic/locale-util.c +++ b/src/basic/locale-util.c @@ -365,10 +365,11 @@ const char *special_glyph(SpecialGlyph code) { [BLACK_CIRCLE] = "*", [ARROW] = "->", [MDASH] = "-", + [ELLIPSIS] = "..." }, /* UTF-8 */ - [ true ] = { + [true] = { [TREE_VERTICAL] = "\342\224\202 ", /* │ */ [TREE_BRANCH] = "\342\224\234\342\224\200", /* ├─ */ [TREE_RIGHT] = "\342\224\224\342\224\200", /* └─ */ @@ -377,6 +378,7 @@ const char *special_glyph(SpecialGlyph code) { [BLACK_CIRCLE] = "\342\227\217", /* ● */ [ARROW] = "\342\206\222", /* → */ [MDASH] = "\342\200\223", /* – */ + [ELLIPSIS] = "\342\200\246", /* … */ }, }; diff --git a/src/basic/locale-util.h b/src/basic/locale-util.h index 510064232e..144e2bc630 100644 --- a/src/basic/locale-util.h +++ b/src/basic/locale-util.h @@ -53,6 +53,7 @@ typedef enum { BLACK_CIRCLE, ARROW, MDASH, + ELLIPSIS, _SPECIAL_GLYPH_MAX } SpecialGlyph; From 3f536d5baeb28ac96f4e18151c1452d9d2f76826 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 19:50:53 +0200 Subject: [PATCH 02/11] utf8: add helper call for counting display width of strings --- src/basic/utf8.c | 21 +++++++++++++++++++++ src/basic/utf8.h | 1 + 2 files changed, 22 insertions(+) diff --git a/src/basic/utf8.c b/src/basic/utf8.c index 0dc76eba21..670a98a6a9 100644 --- a/src/basic/utf8.c +++ b/src/basic/utf8.c @@ -35,6 +35,7 @@ #include #include "alloc-util.h" +#include "gunicode.h" #include "hexdecoct.h" #include "macro.h" #include "utf8.h" @@ -414,3 +415,23 @@ size_t utf8_n_codepoints(const char *str) { return n; } + +size_t utf8_console_width(const char *str) { + size_t n = 0; + + /* Returns the approximate width a string will take on screen when printed on a character cell + * terminal/console. */ + + while (*str != 0) { + char32_t c; + + if (utf8_encoded_to_unichar(str, &c) < 0) + return (size_t) -1; + + str = utf8_next_char(str); + + n += unichar_iswide(c) ? 2 : 1; + } + + return n; +} diff --git a/src/basic/utf8.h b/src/basic/utf8.h index c3a7d7b96c..7d68105a08 100644 --- a/src/basic/utf8.h +++ b/src/basic/utf8.h @@ -48,3 +48,4 @@ static inline char32_t utf16_surrogate_pair_to_unichar(char16_t lead, char16_t t } size_t utf8_n_codepoints(const char *str); +size_t utf8_console_width(const char *str); From adea407d111c04deef81e2e615de6102adb3956a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 19:51:39 +0200 Subject: [PATCH 03/11] util: add qsort_r_safe(), similar to qsort_safe() --- src/basic/util.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/basic/util.h b/src/basic/util.h index 0db3627e24..ad1931f7b6 100644 --- a/src/basic/util.h +++ b/src/basic/util.h @@ -111,6 +111,14 @@ static inline void qsort_safe(void *base, size_t nmemb, size_t size, comparison_ qsort_safe((p), (n), sizeof((p)[0]), (__compar_fn_t) _func_); \ }) +static inline void qsort_r_safe(void *base, size_t nmemb, size_t size, int (*compar)(const void*, const void*, void*), void *userdata) { + if (nmemb <= 1) + return; + + assert(base); + qsort_r(base, nmemb, size, compar, userdata); +} + /** * Normal memcpy requires src to be nonnull. We do nothing if n is 0. */ From c30a49b2d019d8657f5a2ca1a3bfdd8bc7ca673c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 19:52:25 +0200 Subject: [PATCH 04/11] string-util: tweak ellipsation a bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This primarily changes to things: 1. Ellipsation to 0, 1 or 2 characters is now supported. Previously we'd hit an assert if the new lengths was < 3, this is now permitted. The result strings won't show too much info still of course, but the code becomes a bit more generic and robust to use. 2. If a UTF-8 mode is disabled and the input string is pure ASCII, then "..." is used for ellipsation, otherwise (as before) "…". This means on a pure-ASCII system we should remain pure-ASCII, matching behaviour otherwise exposed with special_glyph() and friends. Note that we'll use "…" for ellipsiation as soon as either the locale settings indicate an UTF-8 mode or the input string already contains non-ASCII unicode characters. Testing for these special cases is improved. --- src/basic/string-util.c | 93 ++++++++++++++++++++++++++++----------- src/test/test-ellipsize.c | 24 ++++++++++ 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 7c75970d7b..4bcfee973c 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -15,6 +15,7 @@ #include "alloc-util.h" #include "gunicode.h" +#include "locale-util.h" #include "macro.h" #include "string-util.h" #include "terminal-util.h" @@ -452,62 +453,104 @@ bool string_has_cc(const char *p, const char *ok) { } static char *ascii_ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigned percent) { - size_t x; + size_t x, need_space; char *r; assert(s); assert(percent <= 100); - assert(new_length >= 3); + assert(new_length != (size_t) -1); - if (old_length <= 3 || old_length <= new_length) + if (old_length <= new_length) return strndup(s, old_length); - r = new0(char, new_length+3); + /* Special case short ellipsations */ + switch (new_length) { + + case 0: + return strdup(""); + + case 1: + if (is_locale_utf8()) + return strdup("…"); + else + return strdup("."); + + case 2: + if (!is_locale_utf8()) + return strdup(".."); + + break; + + default: + break; + } + + /* Calculate how much space the ellipsis will take up. If we are in UTF-8 mode we only need space for one + * character ("…"), otherwise for three characters ("..."). Note that in both cases we need 3 bytes of storage, + * either for the UTF-8 encoded character or for three ASCII characters. */ + need_space = is_locale_utf8() ? 1 : 3; + + r = new(char, new_length+3); if (!r) return NULL; - x = (new_length * percent) / 100; + assert(new_length >= need_space); - if (x > new_length - 3) - x = new_length - 3; + x = ((new_length - need_space) * percent + 50) / 100; + assert(x <= new_length - need_space); memcpy(r, s, x); - r[x] = 0xe2; /* tri-dot ellipsis: … */ - r[x+1] = 0x80; - r[x+2] = 0xa6; + + if (is_locale_utf8()) { + r[x+0] = 0xe2; /* tri-dot ellipsis: … */ + r[x+1] = 0x80; + r[x+2] = 0xa6; + } else { + r[x+0] = '.'; + r[x+1] = '.'; + r[x+2] = '.'; + } + memcpy(r + x + 3, - s + old_length - (new_length - x - 1), - new_length - x - 1); + s + old_length - (new_length - x - need_space), + new_length - x - need_space + 1); return r; } char *ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigned percent) { - size_t x; - char *e; + size_t x, k, len, len2; const char *i, *j; - unsigned k, len, len2; + char *e; int r; + /* Note that 'old_length' refers to bytes in the string, while 'new_length' refers to character cells taken up + * on screen. This distinction doesn't matter for ASCII strings, but it does matter for non-ASCII UTF-8 + * strings. + * + * Ellipsation is done in a locale-dependent way: + * 1. If the string passed in is fully ASCII and the current locale is not UTF-8, three dots are used ("...") + * 2. Otherwise, a unicode ellipsis is used ("…") + * + * In other words: you'll get a unicode ellipsis as soon as either the string contains non-ASCII characters or + * the current locale is UTF-8. + */ + assert(s); assert(percent <= 100); if (new_length == (size_t) -1) return strndup(s, old_length); - assert(new_length >= 3); + if (new_length == 0) + return strdup(""); - /* if no multibyte characters use ascii_ellipsize_mem for speed */ + /* If no multibyte characters use ascii_ellipsize_mem for speed */ if (ascii_is_valid(s)) return ascii_ellipsize_mem(s, old_length, new_length, percent); - if (old_length <= 3 || old_length <= new_length) - return strndup(s, old_length); - - x = (new_length * percent) / 100; - - if (x > new_length - 3) - x = new_length - 3; + x = ((new_length - 1) * percent) / 100; + assert(x <= new_length - 1); k = 0; for (i = s; k < x && i < s + old_length; i = utf8_next_char(i)) { @@ -552,7 +595,7 @@ char *ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigne */ memcpy(e, s, len); - e[len] = 0xe2; /* tri-dot ellipsis: … */ + e[len + 0] = 0xe2; /* tri-dot ellipsis: … */ e[len + 1] = 0x80; e[len + 2] = 0xa6; diff --git a/src/test/test-ellipsize.c b/src/test/test-ellipsize.c index ba4b043fc9..902bc3342f 100644 --- a/src/test/test-ellipsize.c +++ b/src/test/test-ellipsize.c @@ -17,6 +17,30 @@ static void test_one(const char *p) { _cleanup_free_ char *t; t = ellipsize(p, columns(), 70); puts(t); + free(t); + t = ellipsize(p, columns(), 0); + puts(t); + free(t); + t = ellipsize(p, columns(), 100); + puts(t); + free(t); + t = ellipsize(p, 0, 50); + puts(t); + free(t); + t = ellipsize(p, 1, 50); + puts(t); + free(t); + t = ellipsize(p, 2, 50); + puts(t); + free(t); + t = ellipsize(p, 3, 50); + puts(t); + free(t); + t = ellipsize(p, 4, 50); + puts(t); + free(t); + t = ellipsize(p, 5, 50); + puts(t); } int main(int argc, char *argv[]) { From a89e30ecb4c299e9b5df0d4afea9ecaeb4686d96 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 19:57:56 +0200 Subject: [PATCH 05/11] =?UTF-8?q?pager:=20move=20pager.[ch]=20src/shared/?= =?UTF-8?q?=20=E2=86=92=20src/basic/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pager.[ch] doesn't use any APIs from src/libsystemd/ or src/shared/ hence there's no reason for it to be in src/shared/, let's move it to src/basic/ instead. This enables us to use pager.[ch] APIs from other code in src/basic/, for example pager_have() and suchlike. --- src/basic/meson.build | 2 ++ src/{shared => basic}/pager.c | 0 src/{shared => basic}/pager.h | 0 src/shared/meson.build | 2 -- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/{shared => basic}/pager.c (100%) rename src/{shared => basic}/pager.h (100%) diff --git a/src/basic/meson.build b/src/basic/meson.build index 0175a45410..ed1609c5c8 100644 --- a/src/basic/meson.build +++ b/src/basic/meson.build @@ -127,6 +127,8 @@ basic_sources = files(''' nss-util.h ordered-set.c ordered-set.h + pager.c + pager.h parse-util.c parse-util.h path-util.c diff --git a/src/shared/pager.c b/src/basic/pager.c similarity index 100% rename from src/shared/pager.c rename to src/basic/pager.c diff --git a/src/shared/pager.h b/src/basic/pager.h similarity index 100% rename from src/shared/pager.h rename to src/basic/pager.h diff --git a/src/shared/meson.build b/src/shared/meson.build index 060b7f95a0..d0cb38650b 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -69,8 +69,6 @@ shared_sources = ''' nsflags.h output-mode.c output-mode.h - pager.c - pager.h path-lookup.c path-lookup.h ptyfwd.c From 1960e73611af42b8c0032d0fa8612931c80c3295 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 20:03:39 +0200 Subject: [PATCH 06/11] basic: add minimalistic table formatter We have plenty of code in our codebase that outputs tables to the console, and all is homegrown and awful. Let's replace it with a generic implementation that can do automatically what the old implementations did manually. Features: 1. Ellipsation (for fields overly long) and alignment (for fields overly short) 2. Sorting of rows 3. automatically copies formatting from the same cell in the row above 4. Heavy use of varargs to make putting together tables easy 5. can expand and compress tables, with weights 6. Has a minimal understanding of unicode wide characters in order to match unicode strings to character cell terminals. 7. Columns can be reordered and individually turned off. 8. pretty printing for various data types And more. --- src/basic/format-table.c | 1247 ++++++++++++++++++++++++++++++++++ src/basic/format-table.h | 62 ++ src/basic/meson.build | 2 + src/test/meson.build | 4 + src/test/test-format-table.c | 139 ++++ 5 files changed, 1454 insertions(+) create mode 100644 src/basic/format-table.c create mode 100644 src/basic/format-table.h create mode 100644 src/test/test-format-table.c diff --git a/src/basic/format-table.c b/src/basic/format-table.c new file mode 100644 index 0000000000..36873fd397 --- /dev/null +++ b/src/basic/format-table.c @@ -0,0 +1,1247 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "alloc-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "gunicode.h" +#include "pager.h" +#include "parse-util.h" +#include "string-util.h" +#include "terminal-util.h" +#include "time-util.h" +#include "utf8.h" +#include "util.h" + +#define DEFAULT_WEIGHT 100 + +/* + A few notes on implementation details: + + - TableCell is a 'fake' structure, it's just used as data type to pass references to specific cell positions in the + table. It can be easily converted to an index number and back. + + - TableData is where the actual data is stored: it encapsulates the data and formatting for a specific cell. It's + 'pseudo-immutable' and ref-counted. When a cell's data's formatting is to be changed, we duplicate the object if the + ref-counting is larger than 1. Note that TableData and its ref-counting is mostly not visible to the outside. The + outside only sees Table and TableCell. + + - The Table object stores a simple one-dimensional array of references to TableData objects, one row after the + previous one. + + - There's no special concept of a "row" or "column" in the table, and no special concept of the "header" row. It's all + derived from the cell index: we know how many cells are to be stored in a row, and can determine the rest from + that. The first row is always the header row. If header display is turned off we simply skip outputting the first + row. Also, when sorting rows we always leave the first row where it is, as the header shouldn't move. + + - Note because there's no row and no column object some properties that might be approproate as row/column properties + are exposed as cell properties instead. For example, the "weight" of a column (which is used to determine where to + add/remove space preferable when expanding/compressing tables horizontally) is actually made the "weight" of a + cell. Given that we usually need it per-column though we will calculate the average across every cell of the column + instead. + + - To make things easy, when cells are added without any explicit configured formatting, then we'll copy the formatting + from the same cell in the previous cell. This is particularly useful for the "weight" of the cell (see above), as + this means setting the weight of the cells of the header row will nicely propagate to all cells in the other rows. +*/ + +typedef struct TableData { + unsigned n_ref; + TableDataType type; + + size_t minimum_width; /* minimum width for the column */ + size_t maximum_width; /* maximum width for the column */ + unsigned weight; /* the horizontal weight for this column, in case the table is expanded/compressed */ + 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 */ + + 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 *formatted; /* A cached textual representation of the cell data, before ellipsation/alignment */ + + union { + uint8_t data[0]; /* data is generic array */ + bool boolean; + usec_t timestamp; + usec_t timespan; + uint64_t size; + char string[0]; + uint32_t uint32; + /* … add more here as we start supporting more cell data types … */ + }; +} TableData; + +static size_t TABLE_CELL_TO_INDEX(TableCell *cell) { + unsigned i; + + assert(cell); + + i = PTR_TO_UINT(cell); + assert(i > 0); + + return i-1; +} + +static TableCell* TABLE_INDEX_TO_CELL(size_t index) { + assert(index != (size_t) -1); + return UINT_TO_PTR((unsigned) (index + 1)); +} + +struct Table { + size_t n_columns; + size_t n_cells; + + bool header; /* Whether to show the header row? */ + size_t width; /* If != (size_t) -1 the width to format this table in */ + + TableData **data; + size_t n_allocated; + + size_t *display_map; /* List of columns to show (by their index). It's fine if columns are listed multiple times or not at all */ + size_t n_display_map; + + size_t *sort_map; /* The columns to order rows by, in order of preference. */ + size_t n_sort_map; +}; + +Table *table_new_raw(size_t n_columns) { + _cleanup_(table_unrefp) Table *t = NULL; + + assert(n_columns > 0); + + t = new(Table, 1); + if (!t) + return NULL; + + *t = (struct Table) { + .n_columns = n_columns, + .header = true, + .width = (size_t) -1, + }; + + return TAKE_PTR(t); +} + +Table *table_new_internal(const char *first_header, ...) { + _cleanup_(table_unrefp) Table *t = NULL; + size_t n_columns = 1; + va_list ap; + int r; + + assert(first_header); + + va_start(ap, first_header); + for (;;) { + const char *h; + + h = va_arg(ap, const char*); + if (!h) + break; + + n_columns++; + } + va_end(ap); + + t = table_new_raw(n_columns); + 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; + + h = va_arg(ap, const char*); + if (!h) + break; + + r = table_add_cell(t, NULL, TABLE_STRING, h); + if (r < 0) { + va_end(ap); + return NULL; + } + } + va_end(ap); + + assert(t->n_columns == t->n_cells); + return TAKE_PTR(t); +} + +static TableData *table_data_unref(TableData *d) { + if (!d) + return NULL; + + assert(d->n_ref > 0); + d->n_ref--; + + if (d->n_ref > 0) + return NULL; + + free(d->formatted); + return mfree(d); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(TableData*, table_data_unref); + +static TableData *table_data_ref(TableData *d) { + if (!d) + return NULL; + + assert(d->n_ref > 0); + d->n_ref++; + + return d; +} + +Table *table_unref(Table *t) { + size_t i; + + if (!t) + return NULL; + + for (i = 0; i < t->n_cells; i++) + table_data_unref(t->data[i]); + + free(t->data); + free(t->display_map); + free(t->sort_map); + + return mfree(t); +} + +static size_t table_data_size(TableDataType type, const void *data) { + + switch (type) { + + case TABLE_EMPTY: + return 0; + + case TABLE_STRING: + return strlen(data) + 1; + + case TABLE_BOOLEAN: + return sizeof(bool); + + case TABLE_TIMESTAMP: + case TABLE_TIMESPAN: + return sizeof(usec_t); + + case TABLE_SIZE: + return sizeof(uint64_t); + + case TABLE_UINT32: + return sizeof(uint32_t); + + default: + assert_not_reached("Uh? Unexpected cell type"); + } +} + +static bool table_data_matches( + TableData *d, + TableDataType type, + const void *data, + size_t minimum_width, + size_t maximum_width, + unsigned weight, + unsigned align_percent, + unsigned ellipsize_percent) { + + size_t k, l; + assert(d); + + if (d->type != type) + return false; + + if (d->minimum_width != minimum_width) + return false; + + if (d->maximum_width != maximum_width) + return false; + + if (d->weight != weight) + return false; + + if (d->align_percent != align_percent) + return false; + + if (d->ellipsize_percent != ellipsize_percent) + 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; +} + +static TableData *table_data_new( + TableDataType type, + const void *data, + size_t minimum_width, + size_t maximum_width, + unsigned weight, + unsigned align_percent, + unsigned ellipsize_percent) { + + size_t data_size; + TableData *d; + + data_size = table_data_size(type, data); + + d = malloc0(offsetof(TableData, data) + data_size); + if (!d) + return NULL; + + d->n_ref = 1; + d->type = type; + d->minimum_width = minimum_width; + d->maximum_width = maximum_width; + d->weight = weight; + d->align_percent = align_percent; + d->ellipsize_percent = ellipsize_percent; + memcpy_safe(d->data, data, data_size); + + return d; +} + +int table_add_cell_full( + Table *t, + TableCell **ret_cell, + TableDataType type, + const void *data, + size_t minimum_width, + size_t maximum_width, + unsigned weight, + unsigned align_percent, + unsigned ellipsize_percent) { + + _cleanup_(table_data_unrefp) TableData *d = NULL; + TableData *p; + + assert(t); + assert(type >= 0); + assert(type < _TABLE_DATA_TYPE_MAX); + + /* Determine the cell adjacent to the current one, but one row up */ + if (t->n_cells >= t->n_columns) + assert_se(p = t->data[t->n_cells - t->n_columns]); + else + p = NULL; + + /* If formatting parameters are left unspecified, copy from the previous row */ + if (minimum_width == (size_t) -1) + minimum_width = p ? p->minimum_width : 1; + + if (weight == (unsigned) -1) + weight = p ? p->weight : DEFAULT_WEIGHT; + + if (align_percent == (unsigned) -1) + align_percent = p ? p->align_percent : 0; + + if (ellipsize_percent == (unsigned) -1) + ellipsize_percent = p ? p->ellipsize_percent : 100; + + assert(align_percent <= 100); + assert(ellipsize_percent <= 100); + + /* Small optimization: Pretty often adjacent cells in two subsequent lines have the same data and + * formatting. Let's see if we can reuse the cell data and ref it once more. */ + + if (p && table_data_matches(p, type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent)) + d = table_data_ref(p); + else { + d = table_data_new(type, data, minimum_width, maximum_width, weight, align_percent, ellipsize_percent); + if (!d) + return -ENOMEM; + } + + if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns))) + return -ENOMEM; + + if (ret_cell) + *ret_cell = TABLE_INDEX_TO_CELL(t->n_cells); + + t->data[t->n_cells++] = TAKE_PTR(d); + + return 0; +} + +int table_dup_cell(Table *t, TableCell *cell) { + size_t i; + + assert(t); + + /* Add the data of the specified cell a second time as a new cell to the end. */ + + i = TABLE_CELL_TO_INDEX(cell); + if (i >= t->n_cells) + return -ENXIO; + + if (!GREEDY_REALLOC(t->data, t->n_allocated, MAX(t->n_cells + 1, t->n_columns))) + return -ENOMEM; + + t->data[t->n_cells++] = table_data_ref(t->data[i]); + return 0; +} + +static int table_dedup_cell(Table *t, TableCell *cell) { + TableData *nd, *od; + size_t i; + + assert(t); + + /* Helper call that ensures the specified cell's data object has a ref count of 1, which we can use before + * changing a cell's formatting without effecting every other cell's formatting that shares the same data */ + + i = TABLE_CELL_TO_INDEX(cell); + if (i >= t->n_cells) + return -ENXIO; + + assert_se(od = t->data[i]); + if (od->n_ref == 1) + return 0; + + assert(od->n_ref > 1); + + nd = table_data_new(od->type, od->data, od->minimum_width, od->maximum_width, od->weight, od->align_percent, od->ellipsize_percent); + if (!nd) + return -ENOMEM; + + table_data_unref(od); + t->data[i] = nd; + + assert(nd->n_ref == 1); + + return 1; +} + +static TableData *table_get_data(Table *t, TableCell *cell) { + size_t i; + + assert(t); + assert(cell); + + /* Get the data object of the specified cell, or NULL if it doesn't exist */ + + i = TABLE_CELL_TO_INDEX(cell); + if (i >= t->n_cells) + return NULL; + + assert(t->data[i]); + assert(t->data[i]->n_ref > 0); + + return t->data[i]; +} + +int table_set_minimum_width(Table *t, TableCell *cell, size_t minimum_width) { + int r; + + assert(t); + assert(cell); + + if (minimum_width == (size_t) -1) + minimum_width = 1; + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + table_get_data(t, cell)->minimum_width = minimum_width; + return 0; +} + +int table_set_maximum_width(Table *t, TableCell *cell, size_t maximum_width) { + int r; + + assert(t); + assert(cell); + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + table_get_data(t, cell)->maximum_width = maximum_width; + return 0; +} + +int table_set_weight(Table *t, TableCell *cell, unsigned weight) { + int r; + + assert(t); + assert(cell); + + if (weight == (unsigned) -1) + weight = DEFAULT_WEIGHT; + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + table_get_data(t, cell)->weight = weight; + return 0; +} + +int table_set_align_percent(Table *t, TableCell *cell, unsigned percent) { + int r; + + assert(t); + assert(cell); + + if (percent == (unsigned) -1) + percent = 0; + + assert(percent <= 100); + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + table_get_data(t, cell)->align_percent = percent; + return 0; +} + +int table_set_ellipsize_percent(Table *t, TableCell *cell, unsigned percent) { + int r; + + assert(t); + assert(cell); + + if (percent == (unsigned) -1) + percent = 100; + + assert(percent <= 100); + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + table_get_data(t, cell)->ellipsize_percent = percent; + return 0; +} + +int table_set_color(Table *t, TableCell *cell, const char *color) { + int r; + + assert(t); + assert(cell); + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + table_get_data(t, cell)->color = empty_to_null(color); + return 0; +} + +int table_add_many_internal(Table *t, TableDataType first_type, ...) { + TableDataType type; + va_list ap; + int r; + + assert(t); + assert(first_type >= 0); + assert(first_type < _TABLE_DATA_TYPE_MAX); + + type = first_type; + + va_start(ap, first_type); + for (;;) { + const void *data; + union { + uint64_t size; + usec_t usec; + uint32_t uint32; + bool b; + } buffer; + + switch (type) { + + case TABLE_EMPTY: + data = NULL; + break; + + case TABLE_STRING: + data = va_arg(ap, const char *); + break; + + case TABLE_BOOLEAN: + buffer.b = !!va_arg(ap, int); + data = &buffer.b; + break; + + case TABLE_TIMESTAMP: + case TABLE_TIMESPAN: + buffer.usec = va_arg(ap, usec_t); + data = &buffer.usec; + break; + + case TABLE_SIZE: + buffer.size = va_arg(ap, uint64_t); + data = &buffer.size; + break; + + case TABLE_UINT32: + buffer.uint32 = va_arg(ap, uint32_t); + data = &buffer.uint32; + break; + + case _TABLE_DATA_TYPE_MAX: + /* Used as end marker */ + va_end(ap); + return 0; + + default: + assert_not_reached("Uh? Unexpected data type."); + } + + r = table_add_cell(t, NULL, type, data); + if (r < 0) { + va_end(ap); + return r; + } + + type = va_arg(ap, TableDataType); + } +} + +void table_set_header(Table *t, bool b) { + assert(t); + + t->header = b; +} + +void table_set_width(Table *t, size_t width) { + assert(t); + + t->width = width; +} + +int table_set_display(Table *t, size_t first_column, ...) { + size_t allocated, column; + va_list ap; + + assert(t); + + allocated = t->n_display_map; + column = first_column; + + va_start(ap, first_column); + for (;;) { + assert(column < t->n_columns); + + if (!GREEDY_REALLOC(t->display_map, allocated, MAX(t->n_columns, t->n_display_map+1))) { + va_end(ap); + return -ENOMEM; + } + + t->display_map[t->n_display_map++] = column; + + column = va_arg(ap, size_t); + if (column == (size_t) -1) + break; + + } + + return 0; +} + +int table_set_sort(Table *t, size_t first_column, ...) { + size_t allocated, column; + va_list ap; + + assert(t); + + allocated = t->n_sort_map; + column = first_column; + + va_start(ap, first_column); + for (;;) { + assert(column < t->n_columns); + + if (!GREEDY_REALLOC(t->sort_map, allocated, MAX(t->n_columns, t->n_sort_map+1))) { + va_end(ap); + return -ENOMEM; + } + + t->sort_map[t->n_sort_map++] = column; + + column = va_arg(ap, size_t); + if (column == (size_t) -1) + break; + } + + return 0; +} + +static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t index_b) { + assert(a); + assert(b); + + if (a->type == b->type) { + + /* We only define ordering for cells of the same data type. If cells with different data types are + * compared we follow the order the cells were originally added in */ + + switch (a->type) { + + case TABLE_STRING: + return strcmp(a->string, b->string); + + case TABLE_BOOLEAN: + if (!a->boolean && b->boolean) + return -1; + if (a->boolean && !b->boolean) + return 1; + return 0; + + case TABLE_TIMESTAMP: + if (a->timestamp < b->timestamp) + return -1; + if (a->timestamp > b->timestamp) + return 1; + return 0; + + case TABLE_TIMESPAN: + if (a->timespan < b->timespan) + return -1; + if (a->timespan > b->timespan) + return 1; + return 0; + + case TABLE_SIZE: + if (a->size < b->size) + return -1; + if (a->size > b->size) + return 1; + return 0; + + case TABLE_UINT32: + if (a->uint32 < b->uint32) + return -1; + if (a->uint32 > b->uint32) + return 1; + return 0; + + default: + ; + } + } + + /* Generic fallback using the orginal order in which the cells where added. */ + if (index_a < index_b) + return -1; + if (index_a > index_b) + return 1; + + return 0; +} + +static int table_data_compare(const void *x, const void *y, void *userdata) { + const size_t *a = x, *b = y; + Table *t = userdata; + size_t i; + int r; + + assert(t); + assert(t->sort_map); + + /* Make sure the header stays at the beginning */ + if (*a < t->n_columns && *b < t->n_columns) + return 0; + if (*a < t->n_columns) + return -1; + if (*b < t->n_columns) + return 1; + + /* Order other lines by the sorting map */ + for (i = 0; i < t->n_sort_map; i++) { + TableData *d, *dd; + + d = t->data[*a + t->sort_map[i]]; + dd = t->data[*b + t->sort_map[i]]; + + r = cell_data_compare(d, *a, dd, *b); + if (r != 0) + return r; + } + + /* Order identical lines by the order there were originally added in */ + if (*a < *b) + return -1; + if (*a > *b) + return 1; + + return 0; +} + +static const char *table_data_format(TableData *d) { + assert(d); + + if (d->formatted) + return d->formatted; + + switch (d->type) { + case TABLE_EMPTY: + return ""; + + case TABLE_STRING: + return d->string; + + case TABLE_BOOLEAN: + return yes_no(d->boolean); + + case TABLE_TIMESTAMP: { + _cleanup_free_ char *p; + + p = new(char, FORMAT_TIMESTAMP_MAX); + if (!p) + return NULL; + + if (!format_timestamp(p, FORMAT_TIMESTAMP_MAX, d->timestamp)) + return "n/a"; + + d->formatted = TAKE_PTR(p); + break; + } + + case TABLE_TIMESPAN: { + _cleanup_free_ char *p; + + p = new(char, FORMAT_TIMESPAN_MAX); + if (!p) + return NULL; + + if (!format_timespan(p, FORMAT_TIMESPAN_MAX, d->timestamp, 0)) + return "n/a"; + + d->formatted = TAKE_PTR(p); + break; + } + + case TABLE_SIZE: { + _cleanup_free_ char *p; + + p = new(char, FORMAT_BYTES_MAX); + if (!p) + return NULL; + + if (!format_bytes(p, FORMAT_BYTES_MAX, d->size)) + return "n/a"; + + d->formatted = TAKE_PTR(p); + break; + } + + case TABLE_UINT32: { + _cleanup_free_ char *p; + + p = new(char, DECIMAL_STR_WIDTH(d->uint32) + 1); + if (!p) + return NULL; + + sprintf(p, "%" PRIu32, d->uint32); + d->formatted = TAKE_PTR(p); + break; + } + + default: + assert_not_reached("Unexpected type?"); + } + + + return d->formatted; +} + +static int table_data_requested_width(TableData *d, size_t *ret) { + const char *t; + size_t l; + + t = table_data_format(d); + if (!t) + return -ENOMEM; + + l = utf8_console_width(t); + if (l == (size_t) -1) + return -EINVAL; + + if (d->maximum_width != (size_t) -1 && l > d->maximum_width) + l = d->maximum_width; + + if (l < d->minimum_width) + l = d->minimum_width; + + *ret = l; + return 0; +} + +static char *align_string_mem(const char *str, size_t old_length, size_t new_length, unsigned percent) { + size_t w = 0, space, lspace; + const char *p; + char *ret; + size_t i; + + /* As with ellipsize_mem(), 'old_length' is a byte size while 'new_length' is a width in character cells */ + + assert(str); + assert(percent <= 100); + + if (old_length == (size_t) -1) + old_length = strlen(str); + + /* Determine current width on screen */ + p = str; + while (p < str + old_length) { + char32_t c; + + if (utf8_encoded_to_unichar(p, &c) < 0) { + p++, w++; /* count invalid chars as 1 */ + continue; + } + + p = utf8_next_char(p); + w += unichar_iswide(c) ? 2 : 1; + } + + /* Already wider than the target, if so, don't do anything */ + if (w >= new_length) + return strndup(str, old_length); + + /* How much spaces shall we add? An how much on the left side? */ + space = new_length - w; + lspace = space * percent / 100U; + + ret = new(char, space + old_length + 1); + if (!ret) + return NULL; + + for (i = 0; i < lspace; i++) + ret[i] = ' '; + memcpy(ret + lspace, str, old_length); + for (i = lspace + old_length; i < space + old_length; i++) + ret[i] = ' '; + + ret[space + old_length] = 0; + return ret; +} + +int table_print(Table *t, FILE *f) { + size_t n_rows, *minimum_width, *maximum_width, display_columns, *requested_width, + i, j, table_minimum_width, table_maximum_width, table_requested_width, table_effective_width, + *width; + _cleanup_free_ size_t *sorted = NULL; + uint64_t *column_weight, weight_sum; + int r; + + assert(t); + + if (!f) + f = stdout; + + /* 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) + return -ENOMEM; + + for (i = 0; i < n_rows; i++) + sorted[i] = i * t->n_columns; + + qsort_r_safe(sorted, n_rows, sizeof(size_t), table_data_compare, t); + } + + if (t->display_map) + display_columns = t->n_display_map; + else + display_columns = t->n_columns; + + assert(display_columns > 0); + + minimum_width = newa(size_t, display_columns); + maximum_width = newa(size_t, display_columns); + requested_width = newa(size_t, display_columns); + width = newa(size_t, display_columns); + column_weight = newa0(uint64_t, display_columns); + + for (j = 0; j < display_columns; j++) { + minimum_width[j] = 1; + maximum_width[j] = (size_t) -1; + requested_width[j] = (size_t) -1; + } + + /* First pass: determine column sizes */ + for (i = t->header ? 0 : 1; i < n_rows; i++) { + TableData **row; + + /* Note that we don't care about ordering at this time, as we just want to determine column sizes, + * hence we don't care for sorted[] during the first pass. */ + row = t->data + i * t->n_columns; + + for (j = 0; j < display_columns; j++) { + TableData *d; + size_t req; + + assert_se(d = row[t->display_map ? t->display_map[j] : j]); + + r = table_data_requested_width(d, &req); + if (r < 0) + return r; + + /* Determine the biggest width that any cell in this column would like to have */ + if (requested_width[j] == (size_t) -1 || + requested_width[j] < req) + requested_width[j] = req; + + /* Determine the minimum width any cell in this column needs */ + if (minimum_width[j] < d->minimum_width) + minimum_width[j] = d->minimum_width; + + /* Determine the maximum width any cell in this column needs */ + if (d->maximum_width != (size_t) -1 && + (maximum_width[j] == (size_t) -1 || + maximum_width[j] > d->maximum_width)) + maximum_width[j] = d->maximum_width; + + /* Determine the full columns weight */ + column_weight[j] += d->weight; + } + } + + /* One space between each column */ + table_requested_width = table_minimum_width = table_maximum_width = display_columns - 1; + + /* Calculate the total weight for all columns, plus the minimum, maximum and requested width for the table. */ + weight_sum = 0; + for (j = 0; j < display_columns; j++) { + weight_sum += column_weight[j]; + + table_minimum_width += minimum_width[j]; + + if (maximum_width[j] == (size_t) -1) + table_maximum_width = (size_t) -1; + else + table_maximum_width += maximum_width[j]; + + table_requested_width += requested_width[j]; + } + + /* Calculate effective table width */ + if (t->width == (size_t) -1) + table_effective_width = pager_have() ? table_requested_width : MIN(table_requested_width, columns()); + else + table_effective_width = t->width; + + if (table_maximum_width != (size_t) -1 && table_effective_width > table_maximum_width) + table_effective_width = table_maximum_width; + + if (table_effective_width < table_minimum_width) + table_effective_width = table_minimum_width; + + if (table_effective_width >= table_requested_width) { + size_t extra; + + /* We have extra room, let's distribute it among columns according to their weights. We first provide + * each column with what it asked for and the distribute the rest. */ + + extra = table_effective_width - table_requested_width; + + for (j = 0; j < display_columns; j++) { + size_t delta; + + if (weight_sum == 0) + width[j] = requested_width[j] + extra / (display_columns - j); /* Avoid division by zero */ + else + width[j] = requested_width[j] + (extra * column_weight[j]) / weight_sum; + + if (maximum_width[j] != (size_t) -1 && width[j] > maximum_width[j]) + width[j] = maximum_width[j]; + + if (width[j] < minimum_width[j]) + width[j] = minimum_width[j]; + + assert(width[j] >= requested_width[j]); + delta = width[j] - requested_width[j]; + + /* Subtract what we just added from the rest */ + if (extra > delta) + extra -= delta; + else + extra = 0; + + assert(weight_sum >= column_weight[j]); + weight_sum -= column_weight[j]; + } + + } else { + /* We need to compress the table, columns can't get what they asked for. We first provide each column + * with the minimum they need, and then distribute anything left. */ + bool finalize = false; + size_t extra; + + extra = table_effective_width - table_minimum_width; + + for (j = 0; j < display_columns; j++) + width[j] = (size_t) -1; + + for (;;) { + bool restart = false; + + for (j = 0; j < display_columns; j++) { + size_t delta, w; + + /* Did this column already get something assigned? If so, let's skip to the next */ + if (width[j] != (size_t) -1) + continue; + + if (weight_sum == 0) + w = minimum_width[j] + extra / (display_columns - j); /* avoid division by zero */ + else + w = minimum_width[j] + (extra * column_weight[j]) / weight_sum; + + if (w >= requested_width[j]) { + /* Never give more than requested. If we hit a column like this, there's more + * space to allocate to other columns which means we need to restart the + * iteration. However, if we hit a column like this, let's assign it the space + * it wanted for good early.*/ + + w = requested_width[j]; + restart = true; + + } else if (!finalize) + continue; + + width[j] = w; + + assert(w >= minimum_width[j]); + delta = w - minimum_width[j]; + + assert(delta <= extra); + extra -= delta; + + assert(weight_sum >= column_weight[j]); + weight_sum -= column_weight[j]; + + if (restart) + break; + } + + if (finalize) { + assert(!restart); + break; + } + + if (!restart) + finalize = true; + } + } + + /* Second pass: show output */ + for (i = t->header ? 0 : 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++) { + _cleanup_free_ char *buffer = NULL; + const char *field; + TableData *d; + size_t l; + + assert_se(d = row[t->display_map ? t->display_map[j] : j]); + + field = table_data_format(d); + if (!field) + return -ENOMEM; + + l = utf8_console_width(field); + if (l > width[j]) { + /* Field is wider than allocated space. Let's ellipsize */ + + buffer = ellipsize_mem(field, (size_t) -1, width[j], d->ellipsize_percent); + if (!buffer) + return -ENOMEM; + + field = buffer; + + } else if (l < width[j]) { + /* Field is shorter than allocated space. Let's align with spaces */ + + buffer = align_string_mem(field, (size_t) -1, width[j], d->align_percent); + if (!buffer) + return -ENOMEM; + + field = buffer; + } + + if (j > 0) + fputc(' ', f); /* column separator */ + + if (d->color) + fputs(d->color, f); + + fputs(field, f); + + if (d->color) + fputs(ansi_normal(), f); + } + + fputc('\n', f); + } + + return fflush_and_check(f); +} + +int table_format(Table *t, char **ret) { + _cleanup_fclose_ FILE *f = NULL; + char *buf = NULL; + size_t sz = 0; + int r; + + f = open_memstream(&buf, &sz); + if (!f) + return -ENOMEM; + + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + + r = table_print(t, f); + if (r < 0) + return r; + + f = safe_fclose(f); + + *ret = buf; + + return 0; +} + +size_t table_get_rows(Table *t) { + if (!t) + return 0; + + assert(t->n_columns > 0); + return t->n_cells / t->n_columns; +} + +size_t table_get_columns(Table *t) { + if (!t) + return 0; + + assert(t->n_columns > 0); + return t->n_columns; +} diff --git a/src/basic/format-table.h b/src/basic/format-table.h new file mode 100644 index 0000000000..6dc2d16052 --- /dev/null +++ b/src/basic/format-table.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include +#include + +#include "macro.h" + +typedef enum TableDataType { + TABLE_EMPTY, + TABLE_STRING, + TABLE_BOOLEAN, + TABLE_TIMESTAMP, + TABLE_TIMESPAN, + TABLE_SIZE, + TABLE_UINT32, + _TABLE_DATA_TYPE_MAX, + _TABLE_DATA_TYPE_INVALID = -1, +} TableDataType; + +typedef struct Table Table; +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_unref(Table *t); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Table*, table_unref); + +int table_add_cell_full(Table *t, TableCell **ret_cell, TableDataType type, const void *data, size_t minimum_width, size_t maximum_width, unsigned weight, unsigned align_percent, unsigned ellipsize_percent); +static inline int table_add_cell(Table *t, TableCell **ret_cell, TableDataType type, const void *data) { + return table_add_cell_full(t, ret_cell, type, data, (size_t) -1, (size_t) -1, (unsigned) -1, (unsigned) -1, (unsigned) -1); +} + +int table_dup_cell(Table *t, TableCell *cell); + +int table_set_minimum_width(Table *t, TableCell *cell, size_t minimum_width); +int table_set_maximum_width(Table *t, TableCell *cell, size_t maximum_width); +int table_set_weight(Table *t, TableCell *cell, unsigned weight); +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_add_many_internal(Table *t, TableDataType first_type, ...); +#define table_add_many(t, ...) table_add_many_internal(t, __VA_ARGS__, _TABLE_DATA_TYPE_MAX) + +void table_set_header(Table *table, bool b); +void table_set_width(Table *t, size_t width); +int table_set_display(Table *t, size_t first_column, ...); +int table_set_sort(Table *t, size_t first_column, ...); + +int table_print(Table *t, FILE *f); +int table_format(Table *t, char **ret); + +static inline TableCell* TABLE_HEADER_CELL(size_t i) { + return SIZE_TO_PTR(i + 1); +} + +size_t table_get_rows(Table *t); +size_t table_get_columns(Table *t); diff --git a/src/basic/meson.build b/src/basic/meson.build index ed1609c5c8..4f63fcef1c 100644 --- a/src/basic/meson.build +++ b/src/basic/meson.build @@ -77,6 +77,8 @@ basic_sources = files(''' fileio-label.h fileio.c fileio.h + format-table.c + format-table.h format-util.h fs-util.c fs-util.h diff --git a/src/test/meson.build b/src/test/meson.build index 61bb23ef7e..4a9982240b 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -178,6 +178,10 @@ tests += [ [], []], + [['src/test/test-format-table.c'], + [], + []], + [['src/test/test-ratelimit.c'], [], []], diff --git a/src/test/test-format-table.c b/src/test/test-format-table.c new file mode 100644 index 0000000000..adcc414161 --- /dev/null +++ b/src/test/test-format-table.c @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "alloc-util.h" +#include "format-table.h" +#include "string-util.h" +#include "time-util.h" + +int main(int argc, char *argv[]) { + + _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_free_ char *formatted = NULL; + + assert_se(setenv("COLUMNS", "40", 1) >= 0); + + assert_se(t = table_new("ONE", "TWO", "THREE")); + + assert_se(table_set_align_percent(t, TABLE_HEADER_CELL(2), 100) >= 0); + + assert_se(table_add_many(t, + TABLE_STRING, "xxx", + TABLE_STRING, "yyy", + TABLE_BOOLEAN, true) >= 0); + + assert_se(table_add_many(t, + TABLE_STRING, "a long field", + TABLE_STRING, "yyy", + TABLE_BOOLEAN, false) >= 0); + + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + "ONE TWO THREE\n" + "xxx yyy yes\n" + "a long field yyy no\n")); + + formatted = mfree(formatted); + + table_set_width(t, 40); + + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + "ONE TWO THREE\n" + "xxx yyy yes\n" + "a long field yyy no\n")); + + formatted = mfree(formatted); + + table_set_width(t, 12); + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + "ONE TWO THR…\n" + "xxx yyy yes\n" + "a … yyy no\n")); + + formatted = mfree(formatted); + + table_set_width(t, 5); + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + "… … …\n" + "… … …\n" + "… … …\n")); + + formatted = mfree(formatted); + + table_set_width(t, 3); + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + "… … …\n" + "… … …\n" + "… … …\n")); + + formatted = mfree(formatted); + + table_set_width(t, (size_t) -1); + assert_se(table_set_sort(t, (size_t) 0, (size_t) 2, (size_t) -1) >= 0); + + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + "ONE TWO THREE\n" + "a long field yyy no\n" + "xxx yyy yes\n")); + + formatted = mfree(formatted); + + table_set_header(t, false); + + assert_se(table_add_many(t, + TABLE_STRING, "fäää", + TABLE_STRING, "uuu", + TABLE_BOOLEAN, true) >= 0); + + assert_se(table_add_many(t, + TABLE_STRING, "fäää", + TABLE_STRING, "zzz", + TABLE_BOOLEAN, false) >= 0); + + assert_se(table_add_many(t, + TABLE_EMPTY, + TABLE_SIZE, (uint64_t) 4711, + TABLE_TIMESPAN, (usec_t) 5*USEC_PER_MINUTE) >= 0); + + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + "a long field yyy no\n" + "fäää zzz no\n" + "fäää uuu yes\n" + "xxx yyy yes\n" + " 4.6K 5min\n")); + + formatted = mfree(formatted); + + assert_se(table_set_display(t, (size_t) 2, (size_t) 0, (size_t) 2, (size_t) 0, (size_t) 0, (size_t) -1) >= 0); + + assert_se(table_format(t, &formatted) >= 0); + printf("%s\n", formatted); + + assert_se(streq(formatted, + " no a long f… no a long f… a long fi…\n" + " no fäää no fäää fäää \n" + " yes fäää yes fäää fäää \n" + " yes xxx yes xxx xxx \n" + "5min 5min \n")); + + return 0; +} From 930a08dabc2198d7d7be1e05db5f87827918e487 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 20:12:52 +0200 Subject: [PATCH 07/11] machinectl: port machinectl to format-table.[ch] --- src/machine/machinectl.c | 511 ++++++++++++++++----------------------- 1 file changed, 210 insertions(+), 301 deletions(-) diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index f79617ee06..b2909a76a2 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -29,8 +29,10 @@ #include "copy.h" #include "env-util.h" #include "fd-util.h" +#include "format-table.h" #include "hostname-util.h" #include "import-util.h" +#include "locale-util.h" #include "log.h" #include "logs-show.h" #include "macro.h" @@ -76,8 +78,6 @@ static const char *arg_uid = NULL; static char **arg_setenv = NULL; static int arg_addrs = 1; -static int print_addresses(sd_bus *bus, const char *name, int, const char *pr1, const char *pr2, int n_addr); - static OutputFlags get_output_flags(void) { return arg_all * OUTPUT_SHOW_ALL | @@ -86,34 +86,8 @@ static OutputFlags get_output_flags(void) { !arg_quiet * OUTPUT_WARN_CUTOFF; } -typedef struct MachineInfo { - const char *name; - const char *class; - const char *service; - char *os; - char *version_id; -} MachineInfo; - -static int compare_machine_info(const void *a, const void *b) { - const MachineInfo *x = a, *y = b; - - return strcmp(x->name, y->name); -} - -static void clean_machine_info(MachineInfo *machines, size_t n_machines) { - size_t i; - - if (!machines || n_machines == 0) - return; - - for (i = 0; i < n_machines; i++) { - free(machines[i].os); - free(machines[i].version_id); - } - free(machines); -} - static int call_get_os_release(sd_bus *bus, const char *method, const char *name, const char *query, ...) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; const char *k, *v, *iter, **query_res = NULL; size_t count = 0, awaited_args = 0; @@ -134,9 +108,10 @@ static int call_get_os_release(sd_bus *bus, const char *method, const char *name "/org/freedesktop/machine1", "org.freedesktop.machine1.Manager", method, - NULL, &reply, "s", name); + &error, + &reply, "s", name); if (r < 0) - return r; + return log_debug_errno(r, "Failed to call '%s()': %s", method, bus_error_message(&error, r)); r = sd_bus_message_enter_container(reply, 'a', "{ss}"); if (r < 0) @@ -179,16 +154,127 @@ static int call_get_os_release(sd_bus *bus, const char *method, const char *name return 0; } -static int list_machines(int argc, char *argv[], void *userdata) { +static int call_get_addresses(sd_bus *bus, const char *name, int ifi, const char *prefix, const char *prefix2, int n_addr, char **ret) { - size_t max_name = STRLEN("MACHINE"), max_class = STRLEN("CLASS"), - max_service = STRLEN("SERVICE"), max_os = STRLEN("OS"), max_version_id = STRLEN("VERSION"); _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - _cleanup_free_ char *prefix = NULL; - MachineInfo *machines = NULL; - const char *name, *class, *service, *object; - size_t n_machines = 0, n_allocated = 0, j; + _cleanup_free_ char *addresses = NULL; + bool truncate = false; + unsigned n = 0; + int r; + + assert(bus); + assert(name); + assert(prefix); + assert(prefix2); + + r = sd_bus_call_method(bus, + "org.freedesktop.machine1", + "/org/freedesktop/machine1", + "org.freedesktop.machine1.Manager", + "GetMachineAddresses", + NULL, + &reply, + "s", name); + if (r < 0) + return log_debug_errno(r, "Could not get addresses: %s", bus_error_message(&error, r)); + + addresses = strdup(prefix); + if (!addresses) + return log_oom(); + prefix = ""; + + r = sd_bus_message_enter_container(reply, 'a', "(iay)"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_enter_container(reply, 'r', "iay")) > 0) { + int family; + const void *a; + size_t sz; + char buf_ifi[DECIMAL_STR_MAX(int) + 2], buffer[MAX(INET6_ADDRSTRLEN, INET_ADDRSTRLEN)]; + + r = sd_bus_message_read(reply, "i", &family); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read_array(reply, 'y', &a, &sz); + if (r < 0) + return bus_log_parse_error(r); + + if (n_addr != 0) { + if (family == AF_INET6 && ifi > 0) + xsprintf(buf_ifi, "%%%i", ifi); + else + strcpy(buf_ifi, ""); + + if (!strextend(&addresses, prefix, inet_ntop(family, a, buffer, sizeof(buffer)), buf_ifi, NULL)) + return log_oom(); + } else + truncate = true; + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + prefix = prefix2; + + if (n_addr > 0) + n_addr --; + + n++; + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (truncate) { + + if (!strextend(&addresses, special_glyph(ELLIPSIS), NULL)) + return -ENOMEM; + + } + + *ret = TAKE_PTR(addresses); + return (int) n; +} + +static int show_table(Table *table, const char *word) { + int r; + + assert(table); + assert(word); + + if (table_get_rows(table) > 1) { + r = table_set_sort(table, (size_t) 0, (size_t) -1); + if (r < 0) + return log_error_errno(r, "Failed to sort table: %m"); + + table_set_header(table, arg_legend); + + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + if (arg_legend) { + if (table_get_rows(table) > 1) + printf("\n%zu %s listed.\n", table_get_rows(table) - 1, word); + else + printf("No %s.\n", word); + } + + return 0; +} + +static int list_machines(int argc, char *argv[], void *userdata) { + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(table_unrefp) Table *table = NULL; sd_bus *bus = userdata; int r; @@ -204,149 +290,73 @@ static int list_machines(int argc, char *argv[], void *userdata) { &error, &reply, NULL); - if (r < 0) { - log_error("Could not get machines: %s", bus_error_message(&error, -r)); - return r; - } + 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"); + if (!table) + return log_oom(); r = sd_bus_message_enter_container(reply, 'a', "(ssso)"); if (r < 0) return bus_log_parse_error(r); - while ((r = sd_bus_message_read(reply, "(ssso)", &name, &class, &service, &object)) > 0) { - size_t l; + + for (;;) { + _cleanup_free_ char *os = NULL, *version_id = NULL, *addresses = NULL; + const char *name, *class, *service, *object; + + r = sd_bus_message_read(reply, "(ssso)", &name, &class, &service, &object); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; if (name[0] == '.' && !arg_all) continue; - if (!GREEDY_REALLOC0(machines, n_allocated, n_machines + 1)) { - r = log_oom(); - goto out; - } - - machines[n_machines].name = name; - machines[n_machines].class = class; - machines[n_machines].service = service; - (void) call_get_os_release( bus, "GetMachineOSRelease", name, "ID\0" "VERSION_ID\0", - &machines[n_machines].os, - &machines[n_machines].version_id); + &os, + &version_id); - l = strlen(name); - if (l > max_name) - max_name = l; + (void) call_get_addresses( + bus, + name, + 0, + "", + "", + arg_addrs, + &addresses); - l = strlen(class); - if (l > max_class) - max_class = l; - - l = strlen(service); - if (l > max_service) - max_service = l; - - l = machines[n_machines].os ? strlen(machines[n_machines].os) : 1; - if (l > max_os) - max_os = l; - - l = machines[n_machines].version_id ? strlen(machines[n_machines].version_id) : 1; - if (l > max_version_id) - max_version_id = l; - - n_machines++; - } - if (r < 0) { - r = bus_log_parse_error(r); - goto out; + r = table_add_many(table, + TABLE_STRING, name, + TABLE_STRING, class, + TABLE_STRING, strdash_if_empty(service), + TABLE_STRING, strdash_if_empty(os), + TABLE_STRING, strdash_if_empty(version_id), + TABLE_STRING, strdash_if_empty(addresses)); + if (r < 0) + return log_error_errno(r, "Failed to add table row: %m"); } r = sd_bus_message_exit_container(reply); - if (r < 0) { - r = bus_log_parse_error(r); - goto out; - } + if (r < 0) + return bus_log_parse_error(r); - qsort_safe(machines, n_machines, sizeof(MachineInfo), compare_machine_info); - - /* Allocate for prefix max characters for all fields + spaces between them + STRLEN(",\n") */ - r = asprintf(&prefix, "%-*s", - (int) (max_name + - max_class + - max_service + - max_os + - max_version_id + 5 + STRLEN(",\n")), - ",\n"); - if (r < 0) { - r = log_oom(); - goto out; - } - - if (arg_legend && n_machines > 0) - printf("%-*s %-*s %-*s %-*s %-*s %s\n", - (int) max_name, "MACHINE", - (int) max_class, "CLASS", - (int) max_service, "SERVICE", - (int) max_os, "OS", - (int) max_version_id, "VERSION", - "ADDRESSES"); - - for (j = 0; j < n_machines; j++) { - printf("%-*s %-*s %-*s %-*s %-*s ", - (int) max_name, machines[j].name, - (int) max_class, machines[j].class, - (int) max_service, strdash_if_empty(machines[j].service), - (int) max_os, strdash_if_empty(machines[j].os), - (int) max_version_id, strdash_if_empty(machines[j].version_id)); - - r = print_addresses(bus, machines[j].name, 0, "", prefix, arg_addrs); - if (r <= 0) /* error or no addresses defined? */ - fputs("-\n", stdout); - else - fputc('\n', stdout); - } - - if (arg_legend) { - if (n_machines > 0) - printf("\n%zu machines listed.\n", n_machines); - else - printf("No machines.\n"); - } - - r = 0; -out: - clean_machine_info(machines, n_machines); - return r; -} - -typedef struct ImageInfo { - const char *name; - const char *type; - bool read_only; - usec_t crtime; - usec_t mtime; - uint64_t size; -} ImageInfo; - -static int compare_image_info(const void *a, const void *b) { - const ImageInfo *x = a, *y = b; - - return strcmp(x->name, y->name); + return show_table(table, "machines"); } static int list_images(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - size_t max_name = STRLEN("NAME"), max_type = STRLEN("TYPE"), max_size = STRLEN("USAGE"), max_crtime = STRLEN("CREATED"), max_mtime = STRLEN("MODIFIED"); _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - _cleanup_free_ ImageInfo *images = NULL; - size_t n_images = 0, n_allocated = 0, j; - const char *name, *type, *object; + _cleanup_(table_unrefp) Table *table = NULL; sd_bus *bus = userdata; - uint64_t crtime, mtime, size; - int read_only, r; + int r; assert(bus); @@ -359,99 +369,66 @@ static int list_images(int argc, char *argv[], void *userdata) { "ListImages", &error, &reply, - ""); - if (r < 0) { - log_error("Could not get images: %s", bus_error_message(&error, -r)); - return r; - } + NULL); + 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"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, TABLE_HEADER_CELL(3), 100); r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssbttto)"); if (r < 0) return bus_log_parse_error(r); - while ((r = sd_bus_message_read(reply, "(ssbttto)", &name, &type, &read_only, &crtime, &mtime, &size, &object)) > 0) { - char buf[MAX(FORMAT_TIMESTAMP_MAX, FORMAT_BYTES_MAX)]; - size_t l; + for (;;) { + const char *name, *type, *object; + uint64_t crtime, mtime, size; + TableCell *cell; + bool ro_bool; + int ro_int; + + r = sd_bus_message_read(reply, "(ssbttto)", &name, &type, &ro_int, &crtime, &mtime, &size, &object); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; if (name[0] == '.' && !arg_all) continue; - if (!GREEDY_REALLOC(images, n_allocated, n_images + 1)) - return log_oom(); + r = table_add_many(table, + TABLE_STRING, name, + TABLE_STRING, type); + if (r < 0) + return log_error_errno(r, "Failed to add table row: %m"); - images[n_images].name = name; - images[n_images].type = type; - images[n_images].read_only = read_only; - images[n_images].crtime = crtime; - images[n_images].mtime = mtime; - images[n_images].size = size; + ro_bool = ro_int; + r = table_add_cell(table, &cell, TABLE_BOOLEAN, &ro_bool); + if (r < 0) + return log_error_errno(r, "Failed to add table cell: %m"); - l = strlen(name); - if (l > max_name) - max_name = l; - - l = strlen(type); - if (l > max_type) - max_type = l; - - if (crtime != 0) { - l = strlen(strna(format_timestamp(buf, sizeof(buf), crtime))); - if (l > max_crtime) - max_crtime = l; + if (ro_bool) { + r = table_set_color(table, cell, ansi_highlight_red()); + if (r < 0) + return log_error_errno(r, "Failed to set table cell color: %m"); } - if (mtime != 0) { - l = strlen(strna(format_timestamp(buf, sizeof(buf), mtime))); - if (l > max_mtime) - max_mtime = l; - } - - if (size != (uint64_t) -1) { - l = strlen(strna(format_bytes(buf, sizeof(buf), size))); - if (l > max_size) - max_size = l; - } - - n_images++; + r = table_add_many(table, + TABLE_SIZE, size, + TABLE_TIMESTAMP, crtime, + TABLE_TIMESTAMP, mtime); + if (r < 0) + return log_error_errno(r, "Failed to add table row: %m"); } - if (r < 0) - return bus_log_parse_error(r); r = sd_bus_message_exit_container(reply); if (r < 0) return bus_log_parse_error(r); - qsort_safe(images, n_images, sizeof(ImageInfo), compare_image_info); - - if (arg_legend && n_images > 0) - printf("%-*s %-*s %-3s %-*s %-*s %-*s\n", - (int) max_name, "NAME", - (int) max_type, "TYPE", - "RO", - (int) max_size, "USAGE", - (int) max_crtime, "CREATED", - (int) max_mtime, "MODIFIED"); - - for (j = 0; j < n_images; j++) { - char crtime_buf[FORMAT_TIMESTAMP_MAX], mtime_buf[FORMAT_TIMESTAMP_MAX], size_buf[FORMAT_BYTES_MAX]; - - printf("%-*s %-*s %s%-3s%s %-*s %-*s %-*s\n", - (int) max_name, images[j].name, - (int) max_type, images[j].type, - images[j].read_only ? ansi_highlight_red() : "", yes_no(images[j].read_only), images[j].read_only ? ansi_normal() : "", - (int) max_size, strna(format_bytes(size_buf, sizeof(size_buf), images[j].size)), - (int) max_crtime, strna(format_timestamp(crtime_buf, sizeof(crtime_buf), images[j].crtime)), - (int) max_mtime, strna(format_timestamp(mtime_buf, sizeof(mtime_buf), images[j].mtime))); - } - - if (arg_legend) { - if (n_images > 0) - printf("\n%zu images listed.\n", n_images); - else - printf("No images.\n"); - } - - return 0; + return show_table(table, "images"); } static int show_unit_cgroup(sd_bus *bus, const char *unit, pid_t leader) { @@ -495,85 +472,17 @@ static int show_unit_cgroup(sd_bus *bus, const char *unit, pid_t leader) { } static int print_addresses(sd_bus *bus, const char *name, int ifi, const char *prefix, const char *prefix2, int n_addr) { - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - _cleanup_free_ char *addresses = NULL; - bool truncate = false; - unsigned n = 0; + _cleanup_free_ char *s = NULL; int r; - assert(bus); - assert(name); - assert(prefix); - assert(prefix2); - - r = sd_bus_call_method(bus, - "org.freedesktop.machine1", - "/org/freedesktop/machine1", - "org.freedesktop.machine1.Manager", - "GetMachineAddresses", - NULL, - &reply, - "s", name); + r = call_get_addresses(bus, name, ifi, prefix, prefix2, n_addr, &s); if (r < 0) return r; - addresses = strdup(prefix); - if (!addresses) - return log_oom(); - prefix = ""; + if (r > 0) + fputs(s, stdout); - r = sd_bus_message_enter_container(reply, 'a', "(iay)"); - if (r < 0) - return bus_log_parse_error(r); - - while ((r = sd_bus_message_enter_container(reply, 'r', "iay")) > 0) { - int family; - const void *a; - size_t sz; - char buf_ifi[DECIMAL_STR_MAX(int) + 2], buffer[MAX(INET6_ADDRSTRLEN, INET_ADDRSTRLEN)]; - - r = sd_bus_message_read(reply, "i", &family); - if (r < 0) - return bus_log_parse_error(r); - - r = sd_bus_message_read_array(reply, 'y', &a, &sz); - if (r < 0) - return bus_log_parse_error(r); - - if (n_addr != 0) { - if (family == AF_INET6 && ifi > 0) - xsprintf(buf_ifi, "%%%i", ifi); - else - strcpy(buf_ifi, ""); - - if (!strextend(&addresses, prefix, inet_ntop(family, a, buffer, sizeof(buffer)), buf_ifi, NULL)) - return log_oom(); - } else - truncate = true; - - r = sd_bus_message_exit_container(reply); - if (r < 0) - return bus_log_parse_error(r); - - if (prefix != prefix2) - prefix = prefix2; - - if (n_addr > 0) - n_addr -= 1; - - n++; - } - if (r < 0) - return bus_log_parse_error(r); - - r = sd_bus_message_exit_container(reply); - if (r < 0) - return bus_log_parse_error(r); - - if (n > 0) - fprintf(stdout, "%s%s", addresses, truncate ? "..." : ""); - - return (int) n; + return r; } static int print_os_release(sd_bus *bus, const char *method, const char *name, const char *prefix) { From 99f1229d76da4b805f8f6c6e5e4a878d17d42f93 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Apr 2018 21:37:38 +0200 Subject: [PATCH 08/11] loginctl: port loginctl to format-table.[ch] --- src/login/loginctl.c | 195 +++++++++++++++++++++++++++---------------- 1 file changed, 124 insertions(+), 71 deletions(-) diff --git a/src/login/loginctl.c b/src/login/loginctl.c index b166577062..665cef8cbd 100644 --- a/src/login/loginctl.c +++ b/src/login/loginctl.c @@ -19,6 +19,7 @@ #include "bus-util.h" #include "cgroup-show.h" #include "cgroup-util.h" +#include "format-table.h" #include "log.h" #include "logs-show.h" #include "macro.h" @@ -86,13 +87,39 @@ static int get_session_path(sd_bus *bus, const char *session_id, sd_bus_error *e return 0; } +static int show_table(Table *table, const char *word) { + int r; + + assert(table); + assert(word); + + if (table_get_rows(table) > 1) { + r = table_set_sort(table, (size_t) 0, (size_t) -1); + if (r < 0) + return log_error_errno(r, "Failed to sort table: %m"); + + table_set_header(table, arg_legend); + + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + if (arg_legend) { + if (table_get_rows(table) > 1) + printf("\n%zu %s listed.\n", table_get_rows(table) - 1, word); + else + printf("No %s.\n", word); + } + + return 0; +} + static int list_sessions(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - const char *id, *user, *seat, *object; + _cleanup_(table_unrefp) Table *table = NULL; sd_bus *bus = userdata; - unsigned k = 0; - uint32_t uid; int r; assert(bus); @@ -107,67 +134,74 @@ static int list_sessions(int argc, char *argv[], void *userdata) { "org.freedesktop.login1.Manager", "ListSessions", &error, &reply, - ""); - if (r < 0) { - log_error("Failed to list sessions: %s", bus_error_message(&error, r)); - return r; - } + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list sessions: %s", bus_error_message(&error, r)); r = sd_bus_message_enter_container(reply, 'a', "(susso)"); if (r < 0) return bus_log_parse_error(r); - if (arg_legend) - printf("%10s %10s %-16s %-16s %-16s\n", "SESSION", "UID", "USER", "SEAT", "TTY"); + table = table_new("SESSION", "UID", "USER", "SEAT", "TTY"); + if (!table) + return log_oom(); - while ((r = sd_bus_message_read(reply, "(susso)", &id, &uid, &user, &seat, &object)) > 0) { - _cleanup_(sd_bus_error_free) sd_bus_error error2 = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply2 = NULL; + /* Right-align the first two fields (since they are numeric) */ + (void) table_set_align_percent(table, TABLE_HEADER_CELL(0), 100); + (void) table_set_align_percent(table, TABLE_HEADER_CELL(1), 100); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error_tty = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply_tty = NULL; + const char *id, *user, *seat, *object, *tty = NULL; _cleanup_free_ char *path = NULL; - const char *tty = NULL; + uint32_t uid; - r = get_session_path(bus, id, &error2, &path); + r = sd_bus_message_read(reply, "(susso)", &id, &uid, &user, &seat, &object); if (r < 0) - log_warning("Failed to get session path: %s", bus_error_message(&error, r)); + return bus_log_parse_error(r); + if (r == 0) + break; + + r = sd_bus_get_property( + bus, + "org.freedesktop.login1", + object, + "org.freedesktop.login1.Session", + "TTY", + &error_tty, + &reply_tty, + "s"); + if (r < 0) + log_warning_errno(r, "Failed to get TTY for session %s: %s", id, bus_error_message(&error_tty, r)); else { - r = sd_bus_get_property( - bus, - "org.freedesktop.login1", - path, - "org.freedesktop.login1.Session", - "TTY", - &error2, - &reply2, - "s"); + r = sd_bus_message_read(reply_tty, "s", &tty); if (r < 0) - log_warning("Failed to get TTY for session %s: %s", - id, bus_error_message(&error2, r)); - else { - r = sd_bus_message_read(reply2, "s", &tty); - if (r < 0) - return bus_log_parse_error(r); - } + return bus_log_parse_error(r); } - printf("%10s %10"PRIu32" %-16s %-16s %-16s\n", id, uid, user, seat, strna(tty)); - k++; + r = table_add_many(table, + TABLE_STRING, id, + TABLE_UINT32, uid, + TABLE_STRING, user, + TABLE_STRING, seat, + TABLE_STRING, strna(tty)); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); } + + r = sd_bus_message_exit_container(reply); if (r < 0) return bus_log_parse_error(r); - if (arg_legend) - printf("\n%u sessions listed.\n", k); - - return 0; + return show_table(table, "sessions"); } static int list_users(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - const char *user, *object; + _cleanup_(table_unrefp) Table *table = NULL; sd_bus *bus = userdata; - unsigned k = 0; - uint32_t uid; int r; assert(bus); @@ -182,39 +216,51 @@ static int list_users(int argc, char *argv[], void *userdata) { "org.freedesktop.login1.Manager", "ListUsers", &error, &reply, - ""); - if (r < 0) { - log_error("Failed to list users: %s", bus_error_message(&error, r)); - return r; - } + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list users: %s", bus_error_message(&error, r)); r = sd_bus_message_enter_container(reply, 'a', "(uso)"); if (r < 0) return bus_log_parse_error(r); - if (arg_legend) - printf("%10s %-16s\n", "UID", "USER"); + table = table_new("UID", "USER"); + if (!table) + return log_oom(); - while ((r = sd_bus_message_read(reply, "(uso)", &uid, &user, &object)) > 0) { - printf("%10"PRIu32" %-16s\n", uid, user); - k++; + (void) table_set_align_percent(table, TABLE_HEADER_CELL(0), 100); + + for (;;) { + const char *user, *object; + uint32_t uid; + + r = sd_bus_message_read(reply, "(uso)", &uid, &user, &object); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = table_add_many(table, + TABLE_UINT32, uid, + TABLE_STRING, user); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); } + + r = sd_bus_message_exit_container(reply); if (r < 0) return bus_log_parse_error(r); - if (arg_legend) - printf("\n%u users listed.\n", k); - - return 0; + return show_table(table, "users"); } static int list_seats(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; - const char *seat, *object; + _cleanup_(table_unrefp) Table *table = NULL; sd_bus *bus = userdata; - unsigned k = 0; int r; + assert(bus); assert(argv); @@ -227,30 +273,37 @@ static int list_seats(int argc, char *argv[], void *userdata) { "org.freedesktop.login1.Manager", "ListSeats", &error, &reply, - ""); - if (r < 0) { - log_error("Failed to list seats: %s", bus_error_message(&error, r)); - return r; - } + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list seats: %s", bus_error_message(&error, r)); r = sd_bus_message_enter_container(reply, 'a', "(so)"); if (r < 0) return bus_log_parse_error(r); - if (arg_legend) - printf("%-16s\n", "SEAT"); + table = table_new("SEAT"); + if (!table) + return log_oom(); - while ((r = sd_bus_message_read(reply, "(so)", &seat, &object)) > 0) { - printf("%-16s\n", seat); - k++; + for (;;) { + const char *seat, *object; + + r = sd_bus_message_read(reply, "(so)", &seat, &object); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = table_add_cell(table, NULL, TABLE_STRING, seat); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); } + + r = sd_bus_message_exit_container(reply); if (r < 0) return bus_log_parse_error(r); - if (arg_legend) - printf("\n%u seats listed.\n", k); - - return 0; + return show_table(table, "seats"); } static int show_unit_cgroup(sd_bus *bus, const char *interface, const char *unit, pid_t leader) { From 7c6c2e07fcce75555ec093b2f78bb92bb0b66ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 18 Apr 2018 10:57:01 +0200 Subject: [PATCH 09/11] test-utf8: add a smoke test for utf8_console_width() --- src/test/test-utf8.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/test-utf8.c b/src/test/test-utf8.c index a7bbc9b426..ec963437b7 100644 --- a/src/test/test-utf8.c +++ b/src/test/test-utf8.c @@ -102,6 +102,15 @@ static void test_utf8_n_codepoints(void) { assert_se(utf8_n_codepoints("\xF1") == (size_t) -1); } +static void test_utf8_console_width(void) { + assert_se(utf8_console_width("abc") == 3); + assert_se(utf8_console_width("zażółcić gęślą jaźń") == 19); + assert_se(utf8_console_width("串") == 2); + assert_se(utf8_console_width("") == 0); + assert_se(utf8_console_width("…👊🔪💐…") == 8); + assert_se(utf8_console_width("\xF1") == (size_t) -1); +} + int main(int argc, char *argv[]) { test_utf8_is_valid(); test_utf8_is_printable(); @@ -111,6 +120,7 @@ int main(int argc, char *argv[]) { test_utf8_escaping_printable(); test_utf16_to_utf8(); test_utf8_n_codepoints(); + test_utf8_console_width(); return 0; } From e206fcc1644396d98bc37d505a2088cb33eb63dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 18 Apr 2018 10:53:25 +0200 Subject: [PATCH 10/11] test-locale-util: show special glyphs This is mostly useful as a sanity check. --- src/test/test-locale-util.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/test/test-locale-util.c b/src/test/test-locale-util.c index 01497b16fd..5008630e01 100644 --- a/src/test/test-locale-util.c +++ b/src/test/test-locale-util.c @@ -26,6 +26,8 @@ static void test_get_locales(void) { } static void test_locale_is_valid(void) { + log_info("/* %s */", __func__); + assert_se(locale_is_valid("en_EN.utf8")); assert_se(locale_is_valid("fr_FR.utf8")); assert_se(locale_is_valid("fr_FR@euro")); @@ -43,6 +45,8 @@ static void test_keymaps(void) { char **p; int r; + log_info("/* %s */", __func__); + assert_se(!keymap_is_valid("")); assert_se(!keymap_is_valid("/usr/bin/foo")); assert_se(!keymap_is_valid("\x01gar\x02 bage\x03")); @@ -65,11 +69,31 @@ static void test_keymaps(void) { assert_se(keymap_is_valid("unicode")); } +#define dump_glyph(x) log_info(STRINGIFY(x) ": %s", special_glyph(x)) +static void dump_special_glyphs(void) { + assert_cc(ELLIPSIS + 1 == _SPECIAL_GLYPH_MAX); + + log_info("/* %s */", __func__); + + log_info("is_locale_utf8: %s", yes_no(is_locale_utf8())); + + dump_glyph(TREE_VERTICAL); + dump_glyph(TREE_BRANCH); + dump_glyph(TREE_RIGHT); + dump_glyph(TREE_SPACE); + dump_glyph(TRIANGULAR_BULLET); + dump_glyph(BLACK_CIRCLE); + dump_glyph(ARROW); + dump_glyph(MDASH); + dump_glyph(ELLIPSIS); +} + int main(int argc, char *argv[]) { test_get_locales(); test_locale_is_valid(); - test_keymaps(); + dump_special_glyphs(); + return 0; } From 5da19043f11bb76f8d4f2e6f88de9cb08431eab9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Apr 2018 12:42:22 +0200 Subject: [PATCH 11/11] update TODO --- TODO | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/TODO b/TODO index 82d197ca3d..3a1b76e930 100644 --- a/TODO +++ b/TODO @@ -24,6 +24,13 @@ Janitorial Clean-ups: Features: +* Fix DECIMAL_STR_MAX or DECIMAL_STR_WIDTH. One includes a trailing NUL, the + other doesn't. What a desaster. Probably to exclude it. Also + DECIMAL_STR_WIDTH should probably add an extra "-" into account for negative + numbers. + +* port systemctl, systemd-inhibit, busctl, … over to format-table.[ch]'s table formatters + * pid1: lock image configured with RootDirectory=/RootImage= using the usual nspawn semantics while the unit is up * add --vacuum-xyz options to coredumpctl, matching those journalctl already has.