From: Karel Zak Date: Mon, 15 Sep 2025 14:14:08 +0000 (+0200) Subject: column: add --wrap-separator option for custom text wrapping X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5fd9b8c95ca67e1c14002c6c2b04315049ef14ba;p=thirdparty%2Futil-linux.git column: add --wrap-separator option for custom text wrapping Add a new --wrap-separator option that allows users to specify custom separator characters for text wrapping within table columns. When used with --table-wrap, the separator is replaced with newlines to enable wrapping at specific points rather than just by column width. The implementation: - Processes wrap separator replacement in modify_table() after column wrap flags are set - Uses scols_cell_refer_memory() for data containing embedded nulls - Enables scols_wrapzero_nextchunk for columns with wrapping enabled - Only applies to columns that have the wrap flag set via --table-wrap Example usage: $ echo -e 'Name:Desc\nJohn:A|software|dev' | \ column --table --separator ':' \ --table-wrap 2 --wrap-separator '|' Name Desc John A software dev Fixes: https://github.com/util-linux/util-linux/issues/3739 Signed-off-by: Karel Zak --- diff --git a/tests/expected/column/table-wrap-separator-all-columns b/tests/expected/column/table-wrap-separator-all-columns new file mode 100644 index 000000000..cb5395517 --- /dev/null +++ b/tests/expected/column/table-wrap-separator-all-columns @@ -0,0 +1,4 @@ +0 1 2 +a b + c +x y z diff --git a/tests/expected/column/table-wrap-separator-basic b/tests/expected/column/table-wrap-separator-basic new file mode 100644 index 000000000..cb5395517 --- /dev/null +++ b/tests/expected/column/table-wrap-separator-basic @@ -0,0 +1,4 @@ +0 1 2 +a b + c +x y z diff --git a/tests/expected/column/table-wrap-separator-multichar b/tests/expected/column/table-wrap-separator-multichar new file mode 100644 index 000000000..e59801aa4 --- /dev/null +++ b/tests/expected/column/table-wrap-separator-multichar @@ -0,0 +1,7 @@ +Name Description +John A + software + developer +Jane A + data + scientist diff --git a/tests/expected/column/table-wrap-separator-multiple-separators b/tests/expected/column/table-wrap-separator-multiple-separators new file mode 100644 index 000000000..d50724c3e --- /dev/null +++ b/tests/expected/column/table-wrap-separator-multiple-separators @@ -0,0 +1,6 @@ +A B C +aa b1 cc + b2 + b3 +xx y1 zz + y2 diff --git a/tests/expected/column/table-wrap-separator-without-wrap b/tests/expected/column/table-wrap-separator-without-wrap new file mode 100644 index 000000000..69a685a4b --- /dev/null +++ b/tests/expected/column/table-wrap-separator-without-wrap @@ -0,0 +1,3 @@ +0 1 2 +a b|c +x y z diff --git a/tests/ts/column/table b/tests/ts/column/table index 5d5e1311d..1cb3804be 100755 --- a/tests/ts/column/table +++ b/tests/ts/column/table @@ -152,4 +152,24 @@ echo "A B C D" | $TS_CMD_COLUMN --output-separator '|' --table --table-maxout \ --table-right 2-3 --output-width=80 >> $TS_OUTPUT 2>> $TS_ERRLOG ts_finalize_subtest +ts_init_subtest "wrap-separator-basic" +echo -e '0:1:2\na::b|c\nx:y:z' | $TS_CMD_COLUMN --table --separator ':' --table-wrap 3 --wrap-separator '|' >> $TS_OUTPUT 2>> $TS_ERRLOG +ts_finalize_subtest + +ts_init_subtest "wrap-separator-all-columns" +echo -e '0:1:2\na::b|c\nx:y:z' | $TS_CMD_COLUMN --table --separator ':' --table-wrap 0 --wrap-separator '|' >> $TS_OUTPUT 2>> $TS_ERRLOG +ts_finalize_subtest + +ts_init_subtest "wrap-separator-without-wrap" +echo -e '0:1:2\na::b|c\nx:y:z' | $TS_CMD_COLUMN --table --separator ':' --wrap-separator '|' >> $TS_OUTPUT 2>> $TS_ERRLOG +ts_finalize_subtest + +ts_init_subtest "wrap-separator-multichar" +echo -e 'Name:Description\nJohn:A||software||developer\nJane:A||data||scientist' | $TS_CMD_COLUMN --table --separator ':' --table-wrap 2 --wrap-separator '||' >> $TS_OUTPUT 2>> $TS_ERRLOG +ts_finalize_subtest + +ts_init_subtest "wrap-separator-multiple-separators" +echo -e 'A:B:C\naa:b1|b2|b3:cc\nxx:y1|y2:zz' | $TS_CMD_COLUMN --table --separator ':' --table-wrap 2 --wrap-separator '|' >> $TS_OUTPUT 2>> $TS_ERRLOG +ts_finalize_subtest + ts_finalize diff --git a/text-utils/column.1.adoc b/text-utils/column.1.adoc index cb1f77a7a..69f2f794f 100644 --- a/text-utils/column.1.adoc +++ b/text-utils/column.1.adoc @@ -176,7 +176,10 @@ This option is active by default for the last visible column. Print header line for each page. *-W, --table-wrap* _columns_:: -Specify the columns where multi-line cells can be used for long text. +Specify the columns where multi-line cells can be used for long text. By default, text is wrapped according to column width. Use *--wrap-separator* to wrap at custom separator characters instead. + +*--wrap-separator* _string_:: +Use _string_ as a separator for wrapping text within columns that have wrapping enabled. The separator is replaced with newlines when the text is displayed. This option requires table mode and columns with wrapping enabled (see *--table-wrap*). For example, use `|` to allow wrapping at pipe characters within column data. *-H, --table-hide* _columns_:: Don't print the specified columns. The special placeholder '*-*' may be used to hide all unnamed columns (see *--table-columns*). @@ -294,6 +297,19 @@ echo -e '1 0 A\n2 1 AA\n3 1 AB\n4 2 AAA\n5 2 AAB' | column --tree-id 1 --tree-pa 3 1 `-AB .... +Print table with custom wrap separator: + +.... +echo -e 'Name:Description\nJohn:A|software|developer\nJane:A|data|scientist' | column --table --separator ':' --table-wrap 2 --wrap-separator '|' +Name Description +John A + software + developer +Jane A + data + scientist +.... + == SEE ALSO *colrm*(1), diff --git a/text-utils/column.c b/text-utils/column.c index 4158c15bb..656d4f14a 100644 --- a/text-utils/column.c +++ b/text-utils/column.c @@ -92,6 +92,7 @@ struct column_control { wchar_t *input_separator; const char *output_separator; + const char *wrap_separator; wchar_t **ents; /* input entries */ size_t nents; /* number of entries */ @@ -287,6 +288,35 @@ static char *wcs_to_mbs(const wchar_t *s) #endif } +static char *apply_wrap_separator(const char *data, const char *wrap_sep, size_t *result_size) +{ + char *result, *p; + const char *q; + size_t sep_len, data_len; + + if (!data || !wrap_sep || !result_size) + return NULL; + + sep_len = strlen(wrap_sep); + data_len = strlen(data); + + result = xmalloc(data_len + 1); + p = result; + q = data; + + while (*q) { + if (strncmp(q, wrap_sep, sep_len) == 0) { + *p++ = '\0'; + q += sep_len; + } else + *p++ = *q++; + } + *p = '\0'; + + *result_size = p - result + 1; + return result; +} + static wchar_t *local_wcstok(struct column_control const *const ctl, wchar_t *p, wchar_t **state) { @@ -647,6 +677,43 @@ static void modify_table(struct column_control *ctl) apply_columnflag_from_list(ctl, ctl->tab_colwrap, SCOLS_FL_WRAP , N_("failed to parse --table-wrap list")); + if (ctl->wrap_separator && ctl->tab_colwrap) { + struct libscols_iter *itr_col, *itr_line; + struct libscols_column *cl; + struct libscols_line *ln; + + itr_col = scols_new_iter(SCOLS_ITER_FORWARD); + itr_line = scols_new_iter(SCOLS_ITER_FORWARD); + if (!itr_col || !itr_line) + err_oom(); + + /* Apply wrap separator to existing data in wrapped columns */ + while (scols_table_next_column(ctl->tab, itr_col, &cl) == 0) { + if (!scols_column_is_wrap(cl)) + continue; + + while (scols_table_next_line(ctl->tab, itr_line, &ln) == 0) { + struct libscols_cell *ce = scols_line_get_column_cell(ln, cl); + const char *data = scols_cell_get_data(ce); + char *wrapped; + size_t sz; + + if (!data) + continue; + wrapped = apply_wrap_separator(data, ctl->wrap_separator, &sz); + if (wrapped) + scols_cell_refer_memory(ce, wrapped, sz); + } + scols_reset_iter(itr_line, SCOLS_ITER_FORWARD); + + /* Set wrapzero function for wrapped columns */ + scols_column_set_wrapfunc(cl, NULL, scols_wrapzero_nextchunk, NULL); + } + + scols_free_iter(itr_col); + scols_free_iter(itr_line); + } + if (!ctl->tab_colnoextrem) { struct libscols_column *cl = get_last_visible_column(ctl, 0); if (cl) @@ -940,6 +1007,7 @@ static void __attribute__((__noreturn__)) usage(void) fputs(_(" -R, --table-right right align text in these columns\n"), out); fputs(_(" -T, --table-truncate truncate text in the columns when necessary\n"), out); fputs(_(" -W, --table-wrap wrap text in the columns when necessary\n"), out); + fputs(_(" --wrap-separator wrap text at this separator (implies --table-wrap)\n"), out); fputs(_(" -L, --keep-empty-lines don't ignore empty lines\n"), out); fputs(_(" -J, --json use JSON output format for table\n"), out); @@ -980,6 +1048,7 @@ int main(int argc, char **argv) enum { OPT_COLOR = CHAR_MAX + 1, OPT_COLORSCHEME, + OPT_WRAP_SEPARATOR, }; static const struct option longopts[] = @@ -1014,6 +1083,7 @@ int main(int argc, char **argv) { "tree-parent", required_argument, NULL, 'p' }, { "use-spaces", required_argument, NULL, 'S' }, { "version", no_argument, NULL, 'V' }, + { "wrap-separator", required_argument, NULL, OPT_WRAP_SEPARATOR }, { NULL, 0, NULL, 0 }, }; static const ul_excl_t excl[] = { /* rows and cols in ASCII order */ @@ -1130,6 +1200,9 @@ int main(int argc, char **argv) case OPT_COLORSCHEME: ctl.tab_colorscheme = optarg; break; + case OPT_WRAP_SEPARATOR: + ctl.wrap_separator = optarg; + break; case 'h': usage();