]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
column: add --wrap-separator option for custom text wrapping
authorKarel Zak <kzak@redhat.com>
Mon, 15 Sep 2025 14:14:08 +0000 (16:14 +0200)
committerKarel Zak <kzak@redhat.com>
Tue, 16 Sep 2025 09:59:19 +0000 (11:59 +0200)
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 <kzak@redhat.com>
tests/expected/column/table-wrap-separator-all-columns [new file with mode: 0644]
tests/expected/column/table-wrap-separator-basic [new file with mode: 0644]
tests/expected/column/table-wrap-separator-multichar [new file with mode: 0644]
tests/expected/column/table-wrap-separator-multiple-separators [new file with mode: 0644]
tests/expected/column/table-wrap-separator-without-wrap [new file with mode: 0644]
tests/ts/column/table
text-utils/column.1.adoc
text-utils/column.c

diff --git a/tests/expected/column/table-wrap-separator-all-columns b/tests/expected/column/table-wrap-separator-all-columns
new file mode 100644 (file)
index 0000000..cb53955
--- /dev/null
@@ -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 (file)
index 0000000..cb53955
--- /dev/null
@@ -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 (file)
index 0000000..e59801a
--- /dev/null
@@ -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 (file)
index 0000000..d50724c
--- /dev/null
@@ -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 (file)
index 0000000..69a685a
--- /dev/null
@@ -0,0 +1,3 @@
+0  1  2
+a     b|c
+x  y  z
index 5d5e1311d218fba6678375016993f00abf4bcfdf..1cb3804be18d4630e9b201e6d99fe6adcda80626 100755 (executable)
@@ -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
index cb1f77a7a0b1236e580cfcfc49c26f8fe25d4bb5..69f2f794ffc9b9eeeffdd8ce443a4f0d2bede89a 100644 (file)
@@ -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),
index 4158c15bb60eb3e20fe72dda84b7e84de89bdd9f..656d4f14a1c240ac8a238ed683cc9777ba518610 100644 (file)
@@ -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 <columns>      right align text in these columns\n"), out);
        fputs(_(" -T, --table-truncate <columns>   truncate text in the columns when necessary\n"), out);
        fputs(_(" -W, --table-wrap <columns>       wrap text in the columns when necessary\n"), out);
+       fputs(_("     --wrap-separator <string>    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();