diff --git a/man/machinectl.xml b/man/machinectl.xml index 5d7a14d2c7..c211ca02a9 100644 --- a/man/machinectl.xml +++ b/man/machinectl.xml @@ -812,15 +812,11 @@ - When used with the - command, limits the number of ip addresses output for every machine. - Defaults to 1. All addresses can be requested with all - as argument to . If the argument to - is less than the actual number - of addresses, ...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 - , if another address will be output afterwards. + When used with the command, limits the number of ip + addresses output for every machine. Defaults to 1. All addresses can be requested with + all as argument to . If the argument to + is less than the actual number of addresses, + follows the last address. diff --git a/src/basic/string-util.c b/src/basic/string-util.c index b477a51534..61809ab065 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1074,3 +1074,121 @@ char* string_erase(char *x) { explicit_bzero_safe(x, strlen(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++; + } +} diff --git a/src/basic/string-util.h b/src/basic/string-util.h index f10af9ad2f..f98fbdddda 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -280,3 +280,6 @@ static inline char* str_realloc(char **p) { } 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); diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 95d88bc543..2377416b44 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -56,7 +56,7 @@ #include "verbs.h" #include "web-util.h" -#define ALL_IP_ADDRESSES -1 +#define ALL_ADDRESSES -1 static char **arg_property = NULL; 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_uid = 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_setenv, strv_freep); @@ -160,12 +160,17 @@ static int call_get_os_release(sd_bus *bus, const char *method, const char *name 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_message_unrefp) sd_bus_message *reply = NULL; _cleanup_free_ char *addresses = NULL; - bool truncate = false; unsigned n = 0; int r; @@ -208,16 +213,13 @@ static int call_get_addresses(sd_bus *bus, const char *name, int ifi, const char 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 (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; + if (!strextend(&addresses, prefix, inet_ntop(family, a, buffer, sizeof(buffer)), buf_ifi, NULL)) + return log_oom(); r = sd_bus_message_exit_container(reply); if (r < 0) @@ -225,9 +227,6 @@ static int call_get_addresses(sd_bus *bus, const char *name, int ifi, const char prefix = prefix2; - if (n_addr > 0) - n_addr --; - n++; } 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) return bus_log_parse_error(r); - if (truncate) { - - if (!strextend(&addresses, special_glyph(SPECIAL_GLYPH_ELLIPSIS), NULL)) - return -ENOMEM; - - } - *ret = TAKE_PTR(addresses); return (int) n; } @@ -306,6 +298,10 @@ static int list_machines(int argc, char *argv[], void *userdata) { if (!table) 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) table_set_width(table, 0); @@ -340,17 +336,16 @@ static int list_machines(int argc, char *argv[], void *userdata) { name, 0, "", - " ", - arg_full ? ALL_IP_ADDRESSES : arg_addrs, + "\n", &addresses); r = table_add_many(table, - TABLE_STRING, name, - TABLE_STRING, class, - TABLE_STRING, empty_to_dash(service), - TABLE_STRING, empty_to_dash(os), - TABLE_STRING, empty_to_dash(version_id), - TABLE_STRING, empty_to_dash(addresses)); + TABLE_STRING, empty_to_null(name), + TABLE_STRING, empty_to_null(class), + TABLE_STRING, empty_to_null(service), + TABLE_STRING, empty_to_null(os), + TABLE_STRING, empty_to_null(version_id), + TABLE_STRING, empty_to_null(addresses)); if (r < 0) 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, - "\t Address: ", "\n\t ", ALL_IP_ADDRESSES, + "\t Address: ", "\n\t ", &addresses) > 0) { fputs(addresses, stdout); fputc('\n', stdout); @@ -2777,7 +2772,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_FORCE, ARG_FORMAT, ARG_UID, - ARG_NUMBER_IPS, + ARG_MAX_ADDRESSES, }; static const struct option options[] = { @@ -2804,7 +2799,7 @@ static int parse_argv(int argc, char *argv[]) { { "format", required_argument, NULL, ARG_FORMAT }, { "uid", required_argument, NULL, ARG_UID }, { "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(); break; - case ARG_NUMBER_IPS: + case ARG_MAX_ADDRESSES: if (streq(optarg, "all")) - arg_addrs = ALL_IP_ADDRESSES; - else if (safe_atoi(optarg, &arg_addrs) < 0) + arg_max_addresses = ALL_ADDRESSES; + else if (safe_atoi(optarg, &arg_max_addresses) < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid number of IPs"); - else if (arg_addrs < 0) + "Invalid number of addresses: %s", optarg); + else if (arg_max_addresses <= 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Number of IPs cannot be negative"); + "Number of IPs cannot be negative or zero: %s", optarg); break; case '?': diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 62ba0c5df3..178bc78cc0 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -11,6 +11,7 @@ #include "format-util.h" #include "gunicode.h" #include "in-addr-util.h" +#include "locale-util.h" #include "memory-util.h" #include "pager.h" #include "parse-util.h" @@ -119,6 +120,7 @@ struct Table { 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 * 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; size_t n_allocated; @@ -147,6 +149,7 @@ Table *table_new_raw(size_t n_columns) { .n_columns = n_columns, .header = true, .width = (size_t) -1, + .cell_height_max = (size_t) -1, }; return TAKE_PTR(t); @@ -963,6 +966,13 @@ void table_set_width(Table *t, size_t 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) { assert(t); @@ -1417,26 +1427,94 @@ static const char *table_data_format(Table *t, TableData *d) { 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; - size_t l; + int r; t = table_data_format(table, d); if (!t) return -ENOMEM; - l = utf8_console_width(t); - if (l == (size_t) -1) - return -EINVAL; + if (table->cell_height_max != (size_t) -1) { + r = string_truncate_lines(t, table->cell_height_max, &truncated); + if (r < 0) + return r; + if (r > 0) + truncation_applied = true; - if (d->maximum_width != (size_t) -1 && l > d->maximum_width) - l = d->maximum_width; + t = truncated; + } - if (l < d->minimum_width) - l = d->minimum_width; + r = console_width_height(t, &width, &height); + if (r < 0) + return r; - *ret = l; - return 0; + if (d->maximum_width != (size_t) -1 && width > d->maximum_width) + 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) { @@ -1573,18 +1651,40 @@ int table_print(Table *t, FILE *f) { for (j = 0; j < display_columns; j++) { TableData *d; - size_t req; + size_t req_width, req_height; 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) 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 */ if (requested_width[j] == (size_t) -1 || - requested_width[j] < req) - requested_width[j] = req; + requested_width[j] < req_width) + requested_width[j] = req_width; /* Determine the minimum width any cell in this column needs */ if (minimum_width[j] < d->minimum_width) @@ -1731,6 +1831,8 @@ int table_print(Table *t, FILE *f) { /* Second pass: show output */ for (i = t->header ? 0 : 1; i < n_rows; i++) { + size_t n_subline = 0; + bool more_sublines; TableData **row; if (sorted) @@ -1738,69 +1840,113 @@ int table_print(Table *t, FILE *f) { 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; + do { + more_sublines = false; - 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); - if (!field) - return -ENOMEM; + assert_se(d = row[t->display_map ? t->display_map[j] : j]); - l = utf8_console_width(field); - if (l > width[j]) { - /* Field is wider than allocated space. Let's ellipsize */ - - buffer = ellipsize(field, width[j], d->ellipsize_percent); - if (!buffer) + field = table_data_format(t, d); + if (!field) 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, 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); + r = string_extract_line(field, n_subline, &extracted); if (r < 0) 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); - field = buffer; - } + l = utf8_console_width(field); + 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 */ - fputs(ansi_underline(), f); + buffer = ellipsize(field, width[j], /* ellipsize at the end if we truncated coming lines, otherwise honour configuration */ + lines_truncated ? 100 : d->ellipsize_percent); + if (!buffer) + return -ENOMEM; - if (j > 0) - fputc(' ', f); /* column separator */ + field = buffer; + } else { + if (lines_truncated) { + _cleanup_free_ char *padded = NULL; - if (table_data_color(d) && colors_enabled()) { - if (row == t->data) /* first undo header underliner */ + /* We truncated more lines of this cell, let's add an + * 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(table_data_color(d), f); } - fputs(field, f); - - if (colors_enabled() && (table_data_color(d) || row == t->data)) - fputs(ANSI_NORMAL, f); - } - - fputc('\n', f); + fputc('\n', f); + n_subline ++; + } while (more_sublines); } return fflush_and_check(f); diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 60af0a96d4..cb9232b47a 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -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_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_display(Table *t, size_t first_column, ...); int table_set_sort(Table *t, size_t first_column, ...); diff --git a/src/test/test-format-table.c b/src/test/test-format-table.c index 96b1d77701..7bf62e48e2 100644 --- a/src/test/test-format-table.c +++ b/src/test/test-format-table.c @@ -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[]) { _cleanup_(table_unrefp) Table *t = NULL; @@ -172,6 +284,7 @@ int main(int argc, char *argv[]) { "5min 5min \n")); test_issue_9549(); + test_multiline(); return 0; } diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index 7a05afb4ac..13936f6d25 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -563,6 +563,153 @@ static void test_memory_startswith_no_case(void) { 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[]) { test_setup_logging(LOG_DEBUG); @@ -595,6 +742,8 @@ int main(int argc, char *argv[]) { test_strlen_ptr(); test_memory_startswith(); test_memory_startswith_no_case(); + test_string_truncate_lines(); + test_string_extract_line(); return 0; }