git: c82e2174150a - stable/13 - column(1): add tests and -l flag

From: Lexi Winter <ivy_at_FreeBSD.org>
Date: Wed, 21 May 2025 00:30:08 UTC
The branch stable/13 has been updated by ivy:

URL: https://cgit.FreeBSD.org/src/commit/?id=c82e2174150a47cdc5bfa61e6383cfc6343c5389

commit c82e2174150a47cdc5bfa61e6383cfc6343c5389
Author:     Lexi Winter <ivy@FreeBSD.org>
AuthorDate: 2025-05-07 09:27:20 +0000
Commit:     Lexi Winter <ivy@FreeBSD.org>
CommitDate: 2025-05-20 21:03:29 +0000

    column(1): add tests and -l flag
    
    column(1): add tests
    
    Reviewed by:    des
    Approved by:    des (mentor)
    Differential Revision:  https://reviews.freebsd.org/D49911
    
    (cherry picked from commit 6f2b1b56ac3dd154bd98f5a7ea075abcb4356560)
    
    column(1): add -l flag
    
    the '-l <tblcols>' flag limits the number of columns that column(1) will
    produce in -t mode.  this is syntax-compatible with the same option in
    util-linux's column(1), but due to existing differences between the two
    implementations, it's not semantically compatible.
    
    as a side-effect, fix a pre-existing bug where empty fields could cause
    incorrect output:
    
            % echo ':' | column -ts:
            (null)
    
    while here, also fix a couple of minor existing issues.
    
    Reviewed by:    des
    Approved by:    des (mentor)
    Differential Revision:  https://reviews.freebsd.org/D50290
    
    (cherry picked from commit 313713b24c6d2a3061972c4f431515c4f1b01c77)
---
 etc/mtree/BSD.tests.dist       |   2 +
 usr.bin/column/Makefile        |   4 +
 usr.bin/column/column.1        |  11 ++-
 usr.bin/column/column.c        |  45 +++++++---
 usr.bin/column/tests/Makefile  |   3 +
 usr.bin/column/tests/column.sh | 200 +++++++++++++++++++++++++++++++++++++++++
 6 files changed, 254 insertions(+), 11 deletions(-)

diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index 4315fe9657c9..fa5c9e910049 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1009,6 +1009,8 @@
         ..
         cmp
         ..
+        column
+        ..
         compress
         ..
         cpio
diff --git a/usr.bin/column/Makefile b/usr.bin/column/Makefile
index 1c304e2b3927..23933d9ef96f 100644
--- a/usr.bin/column/Makefile
+++ b/usr.bin/column/Makefile
@@ -1,5 +1,9 @@
 #	@(#)Makefile	8.1 (Berkeley) 6/6/93
+.include <src.opts.mk>
 
 PROG=	column
 
+HAS_TESTS=
+SUBDIR.${MK_TESTS}= tests
+
 .include <bsd.prog.mk>
diff --git a/usr.bin/column/column.1 b/usr.bin/column/column.1
index f9b05ccf2210..c9dff361cc9c 100644
--- a/usr.bin/column/column.1
+++ b/usr.bin/column/column.1
@@ -27,7 +27,7 @@
 .\"
 .\"     @(#)column.1	8.1 (Berkeley) 6/6/93
 .\"
-.Dd July 29, 2004
+.Dd May 13, 2025
 .Dt COLUMN 1
 .Os
 .Sh NAME
@@ -37,6 +37,7 @@
 .Nm
 .Op Fl tx
 .Op Fl c Ar columns
+.Op Fl l Ar tblcols
 .Op Fl s Ar sep
 .Op Ar
 .Sh DESCRIPTION
@@ -55,6 +56,14 @@ The options are as follows:
 Output is formatted for a display
 .Ar columns
 wide.
+.It Fl l
+When used with
+.Fl t ,
+limit the table to
+.Ar tblcols
+columns in width.
+The last column will contain the rest of the line,
+including any delimiters.
 .It Fl s
 Specify a set of characters to be used to delimit columns for the
 .Fl t
diff --git a/usr.bin/column/column.c b/usr.bin/column/column.c
index a808113649bc..97fc83e6b589 100644
--- a/usr.bin/column/column.c
+++ b/usr.bin/column/column.c
@@ -67,6 +67,7 @@ static void	usage(void);
 static int	width(const wchar_t *);
 
 static int	termwidth = 80;		/* default terminal width */
+static int	tblcols;		/* number of table columns for -t */
 
 static int	entries;		/* number of records */
 static int	eval;			/* exit value */
@@ -81,7 +82,7 @@ main(int argc, char **argv)
 	FILE *fp;
 	int ch, tflag, xflag;
 	char *p;
-	const char *src;
+	const char *errstr, *src;
 	wchar_t *newsep;
 	size_t seplen;
 
@@ -94,17 +95,26 @@ main(int argc, char **argv)
 		termwidth = win.ws_col;
 
 	tflag = xflag = 0;
-	while ((ch = getopt(argc, argv, "c:s:tx")) != -1)
+	while ((ch = getopt(argc, argv, "c:l:s:tx")) != -1)
 		switch(ch) {
 		case 'c':
-			termwidth = atoi(optarg);
+			termwidth = strtonum(optarg, 0, INT_MAX, &errstr);
+			if (errstr != NULL)
+				errx(1, "invalid terminal width \"%s\": %s",
+				    optarg, errstr);
+			break;
+		case 'l':
+			tblcols = strtonum(optarg, 0, INT_MAX, &errstr);
+			if (errstr != NULL)
+				errx(1, "invalid max width \"%s\": %s",
+				    optarg, errstr);
 			break;
 		case 's':
 			src = optarg;
 			seplen = mbsrtowcs(NULL, &src, 0, NULL);
 			if (seplen == (size_t)-1)
 				err(1, "bad separator");
-			newsep = malloc((seplen + 1) * sizeof(wchar_t));
+			newsep = calloc(seplen + 1, sizeof(wchar_t));
 			if (newsep == NULL)
 				err(1, NULL);
 			mbsrtowcs(newsep, &src, seplen + 1, NULL);
@@ -123,6 +133,9 @@ main(int argc, char **argv)
 	argc -= optind;
 	argv += optind;
 
+	if (tblcols && !tflag)
+		errx(1, "the -l flag cannot be used without the -t flag");
+
 	if (!*argv)
 		input(stdin);
 	else for (; *argv; ++argv)
@@ -230,7 +243,7 @@ maketbl(void)
 	int *lens, maxcols;
 	TBL *tbl;
 	wchar_t **cols;
-	wchar_t *last;
+	wchar_t *s;
 
 	if ((t = tbl = calloc(entries, sizeof(TBL))) == NULL)
 		err(1, NULL);
@@ -239,9 +252,11 @@ maketbl(void)
 	if ((lens = calloc(maxcols, sizeof(int))) == NULL)
 		err(1, NULL);
 	for (cnt = 0, lp = list; cnt < entries; ++cnt, ++lp, ++t) {
-		for (coloff = 0, p = *lp;
-		    (cols[coloff] = wcstok(p, separator, &last));
-		    p = NULL)
+		for (p = *lp; wcschr(separator, *p); ++p)
+			/* nothing */ ;
+		for (coloff = 0; *p;) {
+			cols[coloff] = p;
+
 			if (++coloff == maxcols) {
 				if (!(cols = realloc(cols, ((u_int)maxcols +
 				    DEFCOLS) * sizeof(wchar_t *))) ||
@@ -252,6 +267,16 @@ maketbl(void)
 				    0, DEFCOLS * sizeof(int));
 				maxcols += DEFCOLS;
 			}
+
+			if ((!tblcols || coloff < tblcols) &&
+			    (s = wcspbrk(p, separator))) {
+				*s++ = L'\0';
+				while (*s && wcschr(separator, *s))
+					++s;
+				p = s;
+			} else
+				break;
+		}
 		if ((t->list = calloc(coloff, sizeof(*t->list))) == NULL)
 			err(1, NULL);
 		if ((t->len = calloc(coloff, sizeof(int))) == NULL)
@@ -332,8 +357,8 @@ width(const wchar_t *wcs)
 static void
 usage(void)
 {
-
 	(void)fprintf(stderr,
-	    "usage: column [-tx] [-c columns] [-s sep] [file ...]\n");
+	    "usage: column [-tx] [-c columns] [-l tblcols]"
+	    " [-s sep] [file ...]\n");
 	exit(1);
 }
diff --git a/usr.bin/column/tests/Makefile b/usr.bin/column/tests/Makefile
new file mode 100644
index 000000000000..40a7767f0dc0
--- /dev/null
+++ b/usr.bin/column/tests/Makefile
@@ -0,0 +1,3 @@
+ATF_TESTS_SH=	column
+
+.include <bsd.test.mk>
diff --git a/usr.bin/column/tests/column.sh b/usr.bin/column/tests/column.sh
new file mode 100644
index 000000000000..283dc88bff1a
--- /dev/null
+++ b/usr.bin/column/tests/column.sh
@@ -0,0 +1,200 @@
+# SPDX-License-Identifier: ISC
+#
+# Copyright (c) 2025 Lexi Winter
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+atf_test_case "basic"
+basic_head()
+{
+	atf_set descr "Basic column(1) with default options"
+}
+
+basic_body()
+{
+	cat >input.1 <<END
+this is the first input file
+it has multiple lines
+END
+
+	cat >input.2 <<END
+here lies the second input file
+some lines
+
+are empty
+END
+
+	cat >input.3 <<END
+third of the input files am i
+and i have
+more
+lines
+than before
+END
+
+	cat >expected <<END
+this is the first input file	are empty			lines
+it has multiple lines		third of the input files am i	than before
+here lies the second input file	and i have
+some lines			more
+END
+
+	atf_check -o save:output column -c120 input.1 input.2 input.3
+	atf_check diff expected output
+}
+
+atf_test_case "rows"
+rows_head()
+{
+	atf_set descr "column(1) with -x (row-wise) option"
+}
+
+rows_body()
+{
+	cat >input.1 <<END
+this is the first input file
+it has multiple lines
+END
+
+	cat >input.2 <<END
+here lies the second input file
+some lines
+
+are empty
+END
+
+	cat >input.3 <<END
+third of the input files am i
+and i have
+more
+lines
+than before
+END
+
+	cat >expected <<END
+this is the first input file	it has multiple lines		here lies the second input file
+some lines			are empty			third of the input files am i
+and i have			more				lines
+than before
+END
+
+	atf_check -o save:output column -xc120 input.1 input.2 input.3
+	atf_check diff expected output
+}
+
+atf_test_case "basic_table"
+basic_table_head()
+{
+	atf_set descr "column(1) with -t (table) option"
+}
+
+basic_table_body()
+{
+	cat >input.1 <<END
+1 2 3 4
+foo bar baz quux
+END
+
+	cat >input.2 <<END
+fie fi fo fum
+END
+
+	cat >input.3 <<END
+where did my
+fields go
+argh
+END
+
+	cat >expected <<END
+1       2    3    4
+foo     bar  baz  quux
+fie     fi   fo   fum
+where   did  my
+fields  go
+argh
+END
+
+	atf_check -o save:output column -tc120 input.1 input.2 input.3
+	atf_check diff expected output
+}
+
+atf_test_case "colonic_table"
+colonic_table_head()
+{
+	atf_set descr "column(1) with -t (table) and -s options"
+}
+
+colonic_table_body()
+{
+	cat >input <<END
+one:two.three
+four.five:six
+seven.:eight.:nine
+:ein
+::zwei
+drei..
+vier:
+:
+
+END
+
+	cat >expected <<END
+one    two    three
+four   five   six
+seven  eight  nine
+ein
+zwei
+drei
+vier
+END
+
+	atf_check -o save:output column -tc120 -s:. input
+	atf_check diff expected output
+}
+
+atf_test_case "ncols"
+ncols_head()
+{
+	atf_set descr "column(1) with -t (table) and -s and -l options"
+}
+
+ncols_body()
+{
+	cat >input <<END
+now we have five columns
+here there are four
+now only three
+just two
+one
+END
+
+	cat >expected <<END
+now   we     have five columns
+here  there  are four
+now   only   three
+just  two
+one
+END
+
+	atf_check -o save:output column -tc120 -l3 input
+	atf_check diff expected output
+}
+
+atf_init_test_cases()
+{
+	atf_add_test_case basic
+	atf_add_test_case rows
+	atf_add_test_case basic_table
+	atf_add_test_case colonic_table
+	atf_add_test_case ncols
+}