Compare commits

...

6 Commits

Author SHA1 Message Date
Yu Watanabe ab1b472062
Merge pull request #14555 from poettering/table-multine
format-table: proper multi-line support
2020-01-14 06:48:57 +09:00
Lennart Poettering 03f9228e7c man: suffix parameter with = in our documentation, if it expects an argument
Also, don't claim we'd insert a "," between addresses, because we
actually don't do that.
2020-01-13 16:38:44 +01:00
Lennart Poettering fc6eb08e74 machinectl: modernize address table handling
Primarily, use the new multi-line support in table formatting.

Also, stream-line naming of the "max-addresses" options. We used three
names for the concept internall, let's just unify on the name we use for
this for external users, i.e. "max-addresses".
2020-01-13 16:38:40 +01:00
Lennart Poettering d91614e717 format-table: natively support multiline cells
This adds native support for multiline cells.
2020-01-13 16:38:28 +01:00
Lennart Poettering f6857fa601 string-util: add helper for extracting n'th line of a string 2020-01-13 16:37:42 +01:00
Lennart Poettering 8dd6491ef9 string-util: let's add helper for truncating string after a specified number of lines 2020-01-13 16:36:47 +01:00
8 changed files with 634 additions and 113 deletions

View File

@ -812,15 +812,11 @@
<varlistentry> <varlistentry>
<term><option>--max-addresses=</option></term> <term><option>--max-addresses=</option></term>
<listitem><para>When used with the <option>list-machines</option> <listitem><para>When used with the <option>list-machines</option> command, limits the number of ip
command, limits the number of ip addresses output for every machine. addresses output for every machine. Defaults to 1. All addresses can be requested with
Defaults to 1. All addresses can be requested with <literal>all</literal> <literal>all</literal> as argument to <option>--max-addresses=</option>. If the argument to
as argument to <option>--max-addresses</option> . If the argument to <option>--max-addresses=</option> is less than the actual number of addresses,
<option>--max-addresses</option> is less than the actual number <literal></literal>follows the last address.</para></listitem>
of addresses, <literal>...</literal>follows the last address.
If multiple addresses are to be written for a given machine, every
address except the first one is on a new line and is followed by
<literal>,</literal> if another address will be output afterwards. </para></listitem>
</varlistentry> </varlistentry>
<varlistentry> <varlistentry>

View File

@ -1074,3 +1074,121 @@ char* string_erase(char *x) {
explicit_bzero_safe(x, strlen(x)); explicit_bzero_safe(x, strlen(x));
return x; return x;
} }
int string_truncate_lines(const char *s, size_t n_lines, char **ret) {
const char *p = s, *e = s;
bool truncation_applied = false;
char *copy;
size_t n = 0;
assert(s);
/* Truncate after the specified number of lines. Returns > 0 if a truncation was applied or == 0 if
* there were fewer lines in the string anyway. Trailing newlines on input are ignored, and not
* generated either. */
for (;;) {
size_t k;
k = strcspn(p, "\n");
if (p[k] == 0) {
if (k == 0) /* final empty line */
break;
if (n >= n_lines) /* above threshold */
break;
e = p + k; /* last line to include */
break;
}
assert(p[k] == '\n');
if (n >= n_lines)
break;
if (k > 0)
e = p + k;
p += k + 1;
n++;
}
/* e points after the last character we want to keep */
if (isempty(e))
copy = strdup(s);
else {
if (!in_charset(e, "\n")) /* We only consider things truncated if we remove something that
* isn't a new-line or a series of them */
truncation_applied = true;
copy = strndup(s, e - s);
}
if (!copy)
return -ENOMEM;
*ret = copy;
return truncation_applied;
}
int string_extract_line(const char *s, size_t i, char **ret) {
const char *p = s;
size_t c = 0;
/* Extract the i'nth line from the specified string. Returns > 0 if there are more lines after that,
* and == 0 if we are looking at the last line or already beyond the last line. As special
* optimization, if the first line is requested and the string only consists of one line we return
* NULL, indicating the input string should be used as is, and avoid a memory allocation for a very
* common case. */
for (;;) {
const char *q;
q = strchr(p, '\n');
if (i == c) {
/* The line we are looking for! */
if (q) {
char *m;
m = strndup(p, q - p);
if (!m)
return -ENOMEM;
*ret = m;
return !isempty(q + 1); /* more coming? */
} else {
if (p == s)
*ret = NULL; /* Just use the input string */
else {
char *m;
m = strdup(p);
if (!m)
return -ENOMEM;
*ret = m;
}
return 0; /* The end */
}
}
if (!q) {
char *m;
/* No more lines, return empty line */
m = strdup("");
if (!m)
return -ENOMEM;
*ret = m;
return 0; /* The end */
}
p = q + 1;
c++;
}
}

View File

@ -280,3 +280,6 @@ static inline char* str_realloc(char **p) {
} }
char* string_erase(char *x); char* string_erase(char *x);
int string_truncate_lines(const char *s, size_t n_lines, char **ret);
int string_extract_line(const char *s, size_t i, char **ret);

View File

@ -56,7 +56,7 @@
#include "verbs.h" #include "verbs.h"
#include "web-util.h" #include "web-util.h"
#define ALL_IP_ADDRESSES -1 #define ALL_ADDRESSES -1
static char **arg_property = NULL; static char **arg_property = NULL;
static bool arg_all = false; static bool arg_all = false;
@ -79,7 +79,7 @@ static ImportVerify arg_verify = IMPORT_VERIFY_SIGNATURE;
static const char* arg_format = NULL; static const char* arg_format = NULL;
static const char *arg_uid = NULL; static const char *arg_uid = NULL;
static char **arg_setenv = NULL; static char **arg_setenv = NULL;
static int arg_addrs = 1; static int arg_max_addresses = 1;
STATIC_DESTRUCTOR_REGISTER(arg_property, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_property, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_setenv, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_setenv, strv_freep);
@ -160,12 +160,17 @@ static int call_get_os_release(sd_bus *bus, const char *method, const char *name
return 0; return 0;
} }
static int call_get_addresses(sd_bus *bus, const char *name, int ifi, const char *prefix, const char *prefix2, int n_addr, char **ret) { static int call_get_addresses(
sd_bus *bus,
const char *name,
int ifi,
const char *prefix,
const char *prefix2,
char **ret) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_free_ char *addresses = NULL; _cleanup_free_ char *addresses = NULL;
bool truncate = false;
unsigned n = 0; unsigned n = 0;
int r; int r;
@ -208,16 +213,13 @@ static int call_get_addresses(sd_bus *bus, const char *name, int ifi, const char
if (r < 0) if (r < 0)
return bus_log_parse_error(r); return bus_log_parse_error(r);
if (n_addr != 0) { if (family == AF_INET6 && ifi > 0)
if (family == AF_INET6 && ifi > 0) xsprintf(buf_ifi, "%%%i", ifi);
xsprintf(buf_ifi, "%%%i", ifi); else
else strcpy(buf_ifi, "");
strcpy(buf_ifi, "");
if (!strextend(&addresses, prefix, inet_ntop(family, a, buffer, sizeof(buffer)), buf_ifi, NULL)) if (!strextend(&addresses, prefix, inet_ntop(family, a, buffer, sizeof(buffer)), buf_ifi, NULL))
return log_oom(); return log_oom();
} else
truncate = true;
r = sd_bus_message_exit_container(reply); r = sd_bus_message_exit_container(reply);
if (r < 0) if (r < 0)
@ -225,9 +227,6 @@ static int call_get_addresses(sd_bus *bus, const char *name, int ifi, const char
prefix = prefix2; prefix = prefix2;
if (n_addr > 0)
n_addr --;
n++; n++;
} }
if (r < 0) if (r < 0)
@ -237,13 +236,6 @@ static int call_get_addresses(sd_bus *bus, const char *name, int ifi, const char
if (r < 0) if (r < 0)
return bus_log_parse_error(r); return bus_log_parse_error(r);
if (truncate) {
if (!strextend(&addresses, special_glyph(SPECIAL_GLYPH_ELLIPSIS), NULL))
return -ENOMEM;
}
*ret = TAKE_PTR(addresses); *ret = TAKE_PTR(addresses);
return (int) n; return (int) n;
} }
@ -306,6 +298,10 @@ static int list_machines(int argc, char *argv[], void *userdata) {
if (!table) if (!table)
return log_oom(); return log_oom();
table_set_empty_string(table, "-");
if (!arg_full && arg_max_addresses != ALL_ADDRESSES)
table_set_cell_height_max(table, arg_max_addresses);
if (arg_full) if (arg_full)
table_set_width(table, 0); table_set_width(table, 0);
@ -340,17 +336,16 @@ static int list_machines(int argc, char *argv[], void *userdata) {
name, name,
0, 0,
"", "",
" ", "\n",
arg_full ? ALL_IP_ADDRESSES : arg_addrs,
&addresses); &addresses);
r = table_add_many(table, r = table_add_many(table,
TABLE_STRING, name, TABLE_STRING, empty_to_null(name),
TABLE_STRING, class, TABLE_STRING, empty_to_null(class),
TABLE_STRING, empty_to_dash(service), TABLE_STRING, empty_to_null(service),
TABLE_STRING, empty_to_dash(os), TABLE_STRING, empty_to_null(os),
TABLE_STRING, empty_to_dash(version_id), TABLE_STRING, empty_to_null(version_id),
TABLE_STRING, empty_to_dash(addresses)); TABLE_STRING, empty_to_null(addresses));
if (r < 0) if (r < 0)
return table_log_add_error(r); return table_log_add_error(r);
} }
@ -612,7 +607,7 @@ static void print_machine_status_info(sd_bus *bus, MachineStatusInfo *i) {
} }
if (call_get_addresses(bus, i->name, ifi, if (call_get_addresses(bus, i->name, ifi,
"\t Address: ", "\n\t ", ALL_IP_ADDRESSES, "\t Address: ", "\n\t ",
&addresses) > 0) { &addresses) > 0) {
fputs(addresses, stdout); fputs(addresses, stdout);
fputc('\n', stdout); fputc('\n', stdout);
@ -2777,7 +2772,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_FORCE, ARG_FORCE,
ARG_FORMAT, ARG_FORMAT,
ARG_UID, ARG_UID,
ARG_NUMBER_IPS, ARG_MAX_ADDRESSES,
}; };
static const struct option options[] = { static const struct option options[] = {
@ -2804,7 +2799,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "format", required_argument, NULL, ARG_FORMAT }, { "format", required_argument, NULL, ARG_FORMAT },
{ "uid", required_argument, NULL, ARG_UID }, { "uid", required_argument, NULL, ARG_UID },
{ "setenv", required_argument, NULL, 'E' }, { "setenv", required_argument, NULL, 'E' },
{ "max-addresses", required_argument, NULL, ARG_NUMBER_IPS }, { "max-addresses", required_argument, NULL, ARG_MAX_ADDRESSES },
{} {}
}; };
@ -3007,15 +3002,15 @@ static int parse_argv(int argc, char *argv[]) {
return log_oom(); return log_oom();
break; break;
case ARG_NUMBER_IPS: case ARG_MAX_ADDRESSES:
if (streq(optarg, "all")) if (streq(optarg, "all"))
arg_addrs = ALL_IP_ADDRESSES; arg_max_addresses = ALL_ADDRESSES;
else if (safe_atoi(optarg, &arg_addrs) < 0) else if (safe_atoi(optarg, &arg_max_addresses) < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Invalid number of IPs"); "Invalid number of addresses: %s", optarg);
else if (arg_addrs < 0) else if (arg_max_addresses <= 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Number of IPs cannot be negative"); "Number of IPs cannot be negative or zero: %s", optarg);
break; break;
case '?': case '?':

View File

@ -11,6 +11,7 @@
#include "format-util.h" #include "format-util.h"
#include "gunicode.h" #include "gunicode.h"
#include "in-addr-util.h" #include "in-addr-util.h"
#include "locale-util.h"
#include "memory-util.h" #include "memory-util.h"
#include "pager.h" #include "pager.h"
#include "parse-util.h" #include "parse-util.h"
@ -119,6 +120,7 @@ struct Table {
bool header; /* Whether to show the header row? */ bool header; /* Whether to show the header row? */
size_t width; /* If == 0 format this as wide as necessary. If (size_t) -1 format this to console size_t width; /* If == 0 format this as wide as necessary. If (size_t) -1 format this to console
* width or less wide, but not wider. Otherwise the width to format this table in. */ * width or less wide, but not wider. Otherwise the width to format this table in. */
size_t cell_height_max; /* Maximum number of lines per cell. (If there are more, ellipsis is shown. If (size_t) -1 then no limit is set, the default. == 0 is not allowed.) */
TableData **data; TableData **data;
size_t n_allocated; size_t n_allocated;
@ -147,6 +149,7 @@ Table *table_new_raw(size_t n_columns) {
.n_columns = n_columns, .n_columns = n_columns,
.header = true, .header = true,
.width = (size_t) -1, .width = (size_t) -1,
.cell_height_max = (size_t) -1,
}; };
return TAKE_PTR(t); return TAKE_PTR(t);
@ -963,6 +966,13 @@ void table_set_width(Table *t, size_t width) {
t->width = width; t->width = width;
} }
void table_set_cell_height_max(Table *t, size_t height) {
assert(t);
assert(height >= 1 || height == (size_t) -1);
t->cell_height_max = height;
}
int table_set_empty_string(Table *t, const char *empty) { int table_set_empty_string(Table *t, const char *empty) {
assert(t); assert(t);
@ -1417,26 +1427,94 @@ static const char *table_data_format(Table *t, TableData *d) {
return d->formatted; return d->formatted;
} }
static int table_data_requested_width(Table *table, TableData *d, size_t *ret) { static int console_width_height(
const char *s,
size_t *ret_width,
size_t *ret_height) {
size_t max_width = 0, height = 0;
const char *p;
assert(s);
/* Determine the width and height in console character cells the specified string needs. */
do {
size_t k;
p = strchr(s, '\n');
if (p) {
_cleanup_free_ char *c = NULL;
c = strndup(s, p - s);
if (!c)
return -ENOMEM;
k = utf8_console_width(c);
s = p + 1;
} else {
k = utf8_console_width(s);
s = NULL;
}
if (k == (size_t) -1)
return -EINVAL;
if (k > max_width)
max_width = k;
height++;
} while (!isempty(s));
if (ret_width)
*ret_width = max_width;
if (ret_height)
*ret_height = height;
return 0;
}
static int table_data_requested_width_height(
Table *table,
TableData *d,
size_t *ret_width,
size_t *ret_height) {
_cleanup_free_ char *truncated = NULL;
bool truncation_applied = false;
size_t width, height;
const char *t; const char *t;
size_t l; int r;
t = table_data_format(table, d); t = table_data_format(table, d);
if (!t) if (!t)
return -ENOMEM; return -ENOMEM;
l = utf8_console_width(t); if (table->cell_height_max != (size_t) -1) {
if (l == (size_t) -1) r = string_truncate_lines(t, table->cell_height_max, &truncated);
return -EINVAL; if (r < 0)
return r;
if (r > 0)
truncation_applied = true;
if (d->maximum_width != (size_t) -1 && l > d->maximum_width) t = truncated;
l = d->maximum_width; }
if (l < d->minimum_width) r = console_width_height(t, &width, &height);
l = d->minimum_width; if (r < 0)
return r;
*ret = l; if (d->maximum_width != (size_t) -1 && width > d->maximum_width)
return 0; width = d->maximum_width;
if (width < d->minimum_width)
width = d->minimum_width;
if (ret_width)
*ret_width = width;
if (ret_height)
*ret_height = height;
return truncation_applied;
} }
static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) { static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) {
@ -1573,18 +1651,40 @@ int table_print(Table *t, FILE *f) {
for (j = 0; j < display_columns; j++) { for (j = 0; j < display_columns; j++) {
TableData *d; TableData *d;
size_t req; size_t req_width, req_height;
assert_se(d = row[t->display_map ? t->display_map[j] : j]); assert_se(d = row[t->display_map ? t->display_map[j] : j]);
r = table_data_requested_width(t, d, &req); r = table_data_requested_width_height(t, d, &req_width, &req_height);
if (r < 0) if (r < 0)
return r; return r;
if (r > 0) { /* Truncated because too many lines? */
_cleanup_free_ char *last = NULL;
const char *field;
/* If we are going to show only the first few lines of a cell that has
* multiple make sure that we have enough space horizontally to show an
* ellipsis. Hence, let's figure out the last line, and account for its
* length plus ellipsis. */
field = table_data_format(t, d);
if (!field)
return -ENOMEM;
assert_se(t->cell_height_max > 0);
r = string_extract_line(field, t->cell_height_max-1, &last);
if (r < 0)
return r;
req_width = MAX(req_width,
utf8_console_width(last) +
utf8_console_width(special_glyph(SPECIAL_GLYPH_ELLIPSIS)));
}
/* Determine the biggest width that any cell in this column would like to have */ /* Determine the biggest width that any cell in this column would like to have */
if (requested_width[j] == (size_t) -1 || if (requested_width[j] == (size_t) -1 ||
requested_width[j] < req) requested_width[j] < req_width)
requested_width[j] = req; requested_width[j] = req_width;
/* Determine the minimum width any cell in this column needs */ /* Determine the minimum width any cell in this column needs */
if (minimum_width[j] < d->minimum_width) if (minimum_width[j] < d->minimum_width)
@ -1731,6 +1831,8 @@ int table_print(Table *t, FILE *f) {
/* Second pass: show output */ /* Second pass: show output */
for (i = t->header ? 0 : 1; i < n_rows; i++) { for (i = t->header ? 0 : 1; i < n_rows; i++) {
size_t n_subline = 0;
bool more_sublines;
TableData **row; TableData **row;
if (sorted) if (sorted)
@ -1738,69 +1840,113 @@ int table_print(Table *t, FILE *f) {
else else
row = t->data + i * t->n_columns; row = t->data + i * t->n_columns;
for (j = 0; j < display_columns; j++) { do {
_cleanup_free_ char *buffer = NULL; more_sublines = false;
const char *field;
TableData *d;
size_t l;
assert_se(d = row[t->display_map ? t->display_map[j] : j]); for (j = 0; j < display_columns; j++) {
_cleanup_free_ char *buffer = NULL, *extracted = NULL;
bool lines_truncated = false;
const char *field;
TableData *d;
size_t l;
field = table_data_format(t, d); assert_se(d = row[t->display_map ? t->display_map[j] : j]);
if (!field)
return -ENOMEM;
l = utf8_console_width(field); field = table_data_format(t, d);
if (l > width[j]) { if (!field)
/* Field is wider than allocated space. Let's ellipsize */
buffer = ellipsize(field, width[j], d->ellipsize_percent);
if (!buffer)
return -ENOMEM; return -ENOMEM;
field = buffer; r = string_extract_line(field, n_subline, &extracted);
} else if (l < width[j]) {
/* Field is shorter than allocated space. Let's align with spaces */
buffer = align_string_mem(field, d->url, width[j], d->align_percent);
if (!buffer)
return -ENOMEM;
field = buffer;
}
if (l >= width[j] && d->url) {
_cleanup_free_ char *clickable = NULL;
r = terminal_urlify(d->url, field, &clickable);
if (r < 0) if (r < 0)
return r; return r;
if (r > 0) {
/* There are more lines to come */
if ((t->cell_height_max == (size_t) -1 || n_subline + 1 < t->cell_height_max))
more_sublines = true; /* There are more lines to come */
else
lines_truncated = true;
}
if (extracted)
field = extracted;
free_and_replace(buffer, clickable); l = utf8_console_width(field);
field = buffer; if (l > width[j]) {
} /* Field is wider than allocated space. Let's ellipsize */
if (row == t->data) /* underline header line fully, including the column separator */ buffer = ellipsize(field, width[j], /* ellipsize at the end if we truncated coming lines, otherwise honour configuration */
fputs(ansi_underline(), f); lines_truncated ? 100 : d->ellipsize_percent);
if (!buffer)
return -ENOMEM;
if (j > 0) field = buffer;
fputc(' ', f); /* column separator */ } else {
if (lines_truncated) {
_cleanup_free_ char *padded = NULL;
if (table_data_color(d) && colors_enabled()) { /* We truncated more lines of this cell, let's add an
if (row == t->data) /* first undo header underliner */ * ellipsis. We first append it, but thta might make our
* string grow above what we have space for, hence ellipsize
* right after. This will truncate the ellipsis and add a new
* one. */
padded = strjoin(field, special_glyph(SPECIAL_GLYPH_ELLIPSIS));
if (!padded)
return -ENOMEM;
buffer = ellipsize(padded, width[j], 100);
if (!buffer)
return -ENOMEM;
field = buffer;
l = utf8_console_width(field);
}
if (l < width[j]) {
_cleanup_free_ char *aligned = NULL;
/* Field is shorter than allocated space. Let's align with spaces */
aligned = align_string_mem(field, d->url, width[j], d->align_percent);
if (!aligned)
return -ENOMEM;
free_and_replace(buffer, aligned);
field = buffer;
}
}
if (l >= width[j] && d->url) {
_cleanup_free_ char *clickable = NULL;
r = terminal_urlify(d->url, field, &clickable);
if (r < 0)
return r;
free_and_replace(buffer, clickable);
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 (table_data_color(d) && colors_enabled()) {
if (row == t->data) /* first undo header underliner */
fputs(ANSI_NORMAL, f);
fputs(table_data_color(d), f);
}
fputs(field, f);
if (colors_enabled() && (table_data_color(d) || row == t->data))
fputs(ANSI_NORMAL, f); fputs(ANSI_NORMAL, f);
fputs(table_data_color(d), f);
} }
fputs(field, f); fputc('\n', f);
n_subline ++;
if (colors_enabled() && (table_data_color(d) || row == t->data)) } while (more_sublines);
fputs(ANSI_NORMAL, f);
}
fputc('\n', f);
} }
return fflush_and_check(f); return fflush_and_check(f);

View File

@ -96,6 +96,7 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...);
void table_set_header(Table *table, bool b); void table_set_header(Table *table, bool b);
void table_set_width(Table *t, size_t width); void table_set_width(Table *t, size_t width);
void table_set_cell_height_max(Table *t, size_t height);
int table_set_empty_string(Table *t, const char *empty); int table_set_empty_string(Table *t, const char *empty);
int table_set_display(Table *t, size_t first_column, ...); int table_set_display(Table *t, size_t first_column, ...);
int table_set_sort(Table *t, size_t first_column, ...); int table_set_sort(Table *t, size_t first_column, ...);

View File

@ -31,6 +31,118 @@ static void test_issue_9549(void) {
)); ));
} }
static void test_multiline(void) {
_cleanup_(table_unrefp) Table *table = NULL;
_cleanup_free_ char *formatted = NULL;
assert_se(table = table_new("foo", "bar"));
assert_se(table_set_align_percent(table, TABLE_HEADER_CELL(1), 100) >= 0);
assert_se(table_add_many(table,
TABLE_STRING, "three\ndifferent\nlines",
TABLE_STRING, "two\nlines\n") >= 0);
table_set_cell_height_max(table, 1);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three… two…\n"));
formatted = mfree(formatted);
table_set_cell_height_max(table, 2);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three two\n"
"different… lines\n"));
formatted = mfree(formatted);
table_set_cell_height_max(table, 3);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three two\n"
"different lines\n"
"lines \n"));
formatted = mfree(formatted);
table_set_cell_height_max(table, (size_t) -1);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three two\n"
"different lines\n"
"lines \n"));
formatted = mfree(formatted);
assert_se(table_add_many(table,
TABLE_STRING, "short",
TABLE_STRING, "a\npair") >= 0);
assert_se(table_add_many(table,
TABLE_STRING, "short2\n",
TABLE_STRING, "a\nfour\nline\ncell") >= 0);
table_set_cell_height_max(table, 1);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three… two…\n"
"short a…\n"
"short2 a…\n"));
formatted = mfree(formatted);
table_set_cell_height_max(table, 2);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three two\n"
"different… lines\n"
"short a\n"
" pair\n"
"short2 a\n"
" four…\n"));
formatted = mfree(formatted);
table_set_cell_height_max(table, 3);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three two\n"
"different lines\n"
"lines \n"
"short a\n"
" pair\n"
"short2 a\n"
" four\n"
" line…\n"));
formatted = mfree(formatted);
table_set_cell_height_max(table, (size_t) -1);
assert_se(table_format(table, &formatted) >= 0);
fputs(formatted, stdout);
assert_se(streq(formatted,
"FOO BAR\n"
"three two\n"
"different lines\n"
"lines \n"
"short a\n"
" pair\n"
"short2 a\n"
" four\n"
" line\n"
" cell\n"));
formatted = mfree(formatted);
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
_cleanup_(table_unrefp) Table *t = NULL; _cleanup_(table_unrefp) Table *t = NULL;
@ -172,6 +284,7 @@ int main(int argc, char *argv[]) {
"5min 5min \n")); "5min 5min \n"));
test_issue_9549(); test_issue_9549();
test_multiline();
return 0; return 0;
} }

View File

@ -563,6 +563,153 @@ static void test_memory_startswith_no_case(void) {
assert_se(memory_startswith_no_case((char[2]){'X', 'X'}, 2, "XX")); assert_se(memory_startswith_no_case((char[2]){'X', 'X'}, 2, "XX"));
} }
static void test_string_truncate_lines_one(const char *input, size_t n_lines, const char *output, bool truncation) {
_cleanup_free_ char *b = NULL;
int k;
assert_se((k = string_truncate_lines(input, n_lines, &b)) >= 0);
assert_se(streq(b, output));
assert_se(!!k == truncation);
}
static void test_string_truncate_lines(void) {
test_string_truncate_lines_one("", 0, "", false);
test_string_truncate_lines_one("", 1, "", false);
test_string_truncate_lines_one("", 2, "", false);
test_string_truncate_lines_one("", 3, "", false);
test_string_truncate_lines_one("x", 0, "", true);
test_string_truncate_lines_one("x", 1, "x", false);
test_string_truncate_lines_one("x", 2, "x", false);
test_string_truncate_lines_one("x", 3, "x", false);
test_string_truncate_lines_one("x\n", 0, "", true);
test_string_truncate_lines_one("x\n", 1, "x", false);
test_string_truncate_lines_one("x\n", 2, "x", false);
test_string_truncate_lines_one("x\n", 3, "x", false);
test_string_truncate_lines_one("x\ny", 0, "", true);
test_string_truncate_lines_one("x\ny", 1, "x", true);
test_string_truncate_lines_one("x\ny", 2, "x\ny", false);
test_string_truncate_lines_one("x\ny", 3, "x\ny", false);
test_string_truncate_lines_one("x\ny\n", 0, "", true);
test_string_truncate_lines_one("x\ny\n", 1, "x", true);
test_string_truncate_lines_one("x\ny\n", 2, "x\ny", false);
test_string_truncate_lines_one("x\ny\n", 3, "x\ny", false);
test_string_truncate_lines_one("x\ny\nz", 0, "", true);
test_string_truncate_lines_one("x\ny\nz", 1, "x", true);
test_string_truncate_lines_one("x\ny\nz", 2, "x\ny", true);
test_string_truncate_lines_one("x\ny\nz", 3, "x\ny\nz", false);
test_string_truncate_lines_one("x\ny\nz\n", 0, "", true);
test_string_truncate_lines_one("x\ny\nz\n", 1, "x", true);
test_string_truncate_lines_one("x\ny\nz\n", 2, "x\ny", true);
test_string_truncate_lines_one("x\ny\nz\n", 3, "x\ny\nz", false);
test_string_truncate_lines_one("\n", 0, "", false);
test_string_truncate_lines_one("\n", 1, "", false);
test_string_truncate_lines_one("\n", 2, "", false);
test_string_truncate_lines_one("\n", 3, "", false);
test_string_truncate_lines_one("\n\n", 0, "", false);
test_string_truncate_lines_one("\n\n", 1, "", false);
test_string_truncate_lines_one("\n\n", 2, "", false);
test_string_truncate_lines_one("\n\n", 3, "", false);
test_string_truncate_lines_one("\n\n\n", 0, "", false);
test_string_truncate_lines_one("\n\n\n", 1, "", false);
test_string_truncate_lines_one("\n\n\n", 2, "", false);
test_string_truncate_lines_one("\n\n\n", 3, "", false);
test_string_truncate_lines_one("\nx\n\n", 0, "", true);
test_string_truncate_lines_one("\nx\n\n", 1, "", true);
test_string_truncate_lines_one("\nx\n\n", 2, "\nx", false);
test_string_truncate_lines_one("\nx\n\n", 3, "\nx", false);
test_string_truncate_lines_one("\n\nx\n", 0, "", true);
test_string_truncate_lines_one("\n\nx\n", 1, "", true);
test_string_truncate_lines_one("\n\nx\n", 2, "", true);
test_string_truncate_lines_one("\n\nx\n", 3, "\n\nx", false);
}
static void test_string_extract_lines_one(const char *input, size_t i, const char *output, bool more) {
_cleanup_free_ char *b = NULL;
int k;
assert_se((k = string_extract_line(input, i, &b)) >= 0);
assert_se(streq(b ?: input, output));
assert_se(!!k == more);
}
static void test_string_extract_line(void) {
test_string_extract_lines_one("", 0, "", false);
test_string_extract_lines_one("", 1, "", false);
test_string_extract_lines_one("", 2, "", false);
test_string_extract_lines_one("", 3, "", false);
test_string_extract_lines_one("x", 0, "x", false);
test_string_extract_lines_one("x", 1, "", false);
test_string_extract_lines_one("x", 2, "", false);
test_string_extract_lines_one("x", 3, "", false);
test_string_extract_lines_one("x\n", 0, "x", false);
test_string_extract_lines_one("x\n", 1, "", false);
test_string_extract_lines_one("x\n", 2, "", false);
test_string_extract_lines_one("x\n", 3, "", false);
test_string_extract_lines_one("x\ny", 0, "x", true);
test_string_extract_lines_one("x\ny", 1, "y", false);
test_string_extract_lines_one("x\ny", 2, "", false);
test_string_extract_lines_one("x\ny", 3, "", false);
test_string_extract_lines_one("x\ny\n", 0, "x", true);
test_string_extract_lines_one("x\ny\n", 1, "y", false);
test_string_extract_lines_one("x\ny\n", 2, "", false);
test_string_extract_lines_one("x\ny\n", 3, "", false);
test_string_extract_lines_one("x\ny\nz", 0, "x", true);
test_string_extract_lines_one("x\ny\nz", 1, "y", true);
test_string_extract_lines_one("x\ny\nz", 2, "z", false);
test_string_extract_lines_one("x\ny\nz", 3, "", false);
test_string_extract_lines_one("\n", 0, "", false);
test_string_extract_lines_one("\n", 1, "", false);
test_string_extract_lines_one("\n", 2, "", false);
test_string_extract_lines_one("\n", 3, "", false);
test_string_extract_lines_one("\n\n", 0, "", true);
test_string_extract_lines_one("\n\n", 1, "", false);
test_string_extract_lines_one("\n\n", 2, "", false);
test_string_extract_lines_one("\n\n", 3, "", false);
test_string_extract_lines_one("\n\n\n", 0, "", true);
test_string_extract_lines_one("\n\n\n", 1, "", true);
test_string_extract_lines_one("\n\n\n", 2, "", false);
test_string_extract_lines_one("\n\n\n", 3, "", false);
test_string_extract_lines_one("\n\n\n\n", 0, "", true);
test_string_extract_lines_one("\n\n\n\n", 1, "", true);
test_string_extract_lines_one("\n\n\n\n", 2, "", true);
test_string_extract_lines_one("\n\n\n\n", 3, "", false);
test_string_extract_lines_one("\nx\n\n\n", 0, "", true);
test_string_extract_lines_one("\nx\n\n\n", 1, "x", true);
test_string_extract_lines_one("\nx\n\n\n", 2, "", true);
test_string_extract_lines_one("\nx\n\n\n", 3, "", false);
test_string_extract_lines_one("\n\nx\n\n", 0, "", true);
test_string_extract_lines_one("\n\nx\n\n", 1, "", true);
test_string_extract_lines_one("\n\nx\n\n", 2, "x", true);
test_string_extract_lines_one("\n\nx\n\n", 3, "", false);
test_string_extract_lines_one("\n\n\nx\n", 0, "", true);
test_string_extract_lines_one("\n\n\nx\n", 1, "", true);
test_string_extract_lines_one("\n\n\nx\n", 2, "", true);
test_string_extract_lines_one("\n\n\nx\n", 3, "x", false);
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
test_setup_logging(LOG_DEBUG); test_setup_logging(LOG_DEBUG);
@ -595,6 +742,8 @@ int main(int argc, char *argv[]) {
test_strlen_ptr(); test_strlen_ptr();
test_memory_startswith(); test_memory_startswith();
test_memory_startswith_no_case(); test_memory_startswith_no_case();
test_string_truncate_lines();
test_string_extract_line();
return 0; return 0;
} }