git: 585762c3733f - stable/13 - Improve usability of head(1) and tail(1):

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Wed, 13 Dec 2023 20:06:31 UTC
The branch stable/13 has been updated by des:

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

commit 585762c3733f3446fe2dad63a1f460903731927c
Author:     Xin LI <delphij@FreeBSD.org>
AuthorDate: 2022-07-13 04:14:25 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2023-12-13 16:39:38 +0000

    Improve usability of head(1) and tail(1):
    
     - Consistently support -q (quiet) and -v (verbose)
     - Allow specifying numbers with SI prefixes supported by expand_number(3)
     - Remove 2^31 limit on lines for head(1)
    
    MFC after:              2 weeks
    Reviewed by:            lwhsu, pauamma, gbe
    Relnotes:               yes
    Differential Revision: https://reviews.freebsd.org/D35720
    
    (cherry picked from commit 643ac419fafba89f5adda0e0ea75b538727453fb)
---
 usr.bin/head/Makefile           |  1 +
 usr.bin/head/head.1             | 24 +++++++++++++++++++---
 usr.bin/head/head.c             | 33 +++++++++++++++++++++---------
 usr.bin/head/tests/head_test.sh | 45 +++++++++++++++++++++++++++++++++++++++++
 usr.bin/tail/Makefile           |  1 +
 usr.bin/tail/extern.h           |  2 +-
 usr.bin/tail/forward.c          |  6 +++---
 usr.bin/tail/tail.1             | 25 ++++++++++++++++-------
 usr.bin/tail/tail.c             | 22 ++++++++++++++------
 usr.bin/tail/tests/tail_test.sh | 44 ++++++++++++++++++++++++++++++++++++++++
 10 files changed, 173 insertions(+), 30 deletions(-)

diff --git a/usr.bin/head/Makefile b/usr.bin/head/Makefile
index 0b4cf4b2a5d6..dbf9834da9a0 100644
--- a/usr.bin/head/Makefile
+++ b/usr.bin/head/Makefile
@@ -3,6 +3,7 @@
 .include <src.opts.mk>
 
 PROG=	head
+LIBADD=	util
 
 HAS_TESTS=
 SUBDIR.${MK_TESTS}+= tests
diff --git a/usr.bin/head/head.1 b/usr.bin/head/head.1
index 3a68f5a8ba82..2a0416d9883f 100644
--- a/usr.bin/head/head.1
+++ b/usr.bin/head/head.1
@@ -27,7 +27,7 @@
 .\"
 .\"	@(#)head.1	8.1 (Berkeley) 6/6/93
 .\"
-.Dd April 10, 2018
+.Dd June 12, 2022
 .Dt HEAD 1
 .Os
 .Sh NAME
@@ -35,6 +35,7 @@
 .Nd display first lines of a file
 .Sh SYNOPSIS
 .Nm
+.Op Fl qv
 .Op Fl n Ar count | Fl c Ar bytes
 .Op Ar
 .Sh DESCRIPTION
@@ -58,14 +59,30 @@ of each of the specified files.
 Print
 .Ar count
 lines of each of the specified files.
+.Pp
+Both
+.Ar count
+and
+.Ar bytes
+may also be specified with size suffixes supported by
+.Xr expand_number 3 .
+.It Fl q , Fl -quiet , Fl -silent
+Suppresses printing of headers when multiple files are being examined.
+.It Fl v , Fl -verbose
+Prepend each file with a header.
 .El
 .Pp
-If more than a single file is specified, each file is preceded by a
+If more than a single file is specified, or if the
+.Fl v
+option is used, each file is preceded by a
 header consisting of the string
 .Dq ==> XXX <==
 where
 .Dq XXX
 is the name of the file.
+The
+.Fl q
+flag disables the printing of the header in all cases.
 .Sh EXIT STATUS
 .Ex -std
 .Sh EXAMPLES
@@ -82,7 +99,8 @@ in the following way to, for example, display only line 500 from the file
 .Pp
 .Dl $ head -n 500 foo | tail -n 1
 .Sh SEE ALSO
-.Xr tail 1
+.Xr tail 1 ,
+.Xr expand_number 3
 .Sh HISTORY
 The
 .Nm
diff --git a/usr.bin/head/head.c b/usr.bin/head/head.c
index 0b76132e434c..de4faf632979 100644
--- a/usr.bin/head/head.c
+++ b/usr.bin/head/head.c
@@ -55,6 +55,8 @@ static char sccsid[] = "@(#)head.c	8.2 (Berkeley) 5/4/95";
 #include <string.h>
 #include <unistd.h>
 
+#include <libutil.h>
+
 #include <libcasper.h>
 #include <casper/cap_fileargs.h>
 
@@ -64,7 +66,7 @@ static char sccsid[] = "@(#)head.c	8.2 (Berkeley) 5/4/95";
  * Bill Joy UCB August 24, 1977
  */
 
-static void head(FILE *, int);
+static void head(FILE *, intmax_t);
 static void head_bytes(FILE *, off_t);
 static void obsolete(char *[]);
 static void usage(void);
@@ -73,6 +75,9 @@ static const struct option long_opts[] =
 {
 	{"bytes",	required_argument,	NULL, 'c'},
 	{"lines",	required_argument,	NULL, 'n'},
+	{"quiet",	no_argument,		NULL, 'q'},
+	{"silent",	no_argument,		NULL, 'q'},
+	{"verbose",	no_argument,		NULL, 'v'},
 	{NULL,		no_argument,		NULL, 0}
 };
 
@@ -80,29 +85,37 @@ int
 main(int argc, char *argv[])
 {
 	FILE *fp;
-	char *ep;
 	off_t bytecnt;
-	int ch, first, linecnt, eval;
+	intmax_t linecnt;
+	int ch, first, eval;
 	fileargs_t *fa;
 	cap_rights_t rights;
+	int qflag = 0;
+	int vflag = 0;
 
 	linecnt = -1;
 	eval = 0;
 	bytecnt = -1;
 
 	obsolete(argv);
-	while ((ch = getopt_long(argc, argv, "+n:c:", long_opts, NULL)) != -1) {
+	while ((ch = getopt_long(argc, argv, "+n:c:qv", long_opts, NULL)) != -1) {
 		switch(ch) {
 		case 'c':
-			bytecnt = strtoimax(optarg, &ep, 10);
-			if (*ep || bytecnt <= 0)
+			if (expand_number(optarg, &bytecnt) || bytecnt <= 0)
 				errx(1, "illegal byte count -- %s", optarg);
 			break;
 		case 'n':
-			linecnt = strtol(optarg, &ep, 10);
-			if (*ep || linecnt <= 0)
+			if (expand_number(optarg, &linecnt) || linecnt <= 0)
 				errx(1, "illegal line count -- %s", optarg);
 			break;
+		case 'q':
+			qflag = 1;
+			vflag = 0;
+			break;
+		case 'v':
+			qflag = 0;
+			vflag = 1;
+			break;
 		case '?':
 		default:
 			usage();
@@ -132,7 +145,7 @@ main(int argc, char *argv[])
 				eval = 1;
 				continue;
 			}
-			if (argc > 1) {
+			if (vflag || (qflag == 0 && argc > 1)) {
 				(void)printf("%s==> %s <==\n",
 				    first ? "" : "\n", *argv);
 				first = 0;
@@ -153,7 +166,7 @@ main(int argc, char *argv[])
 }
 
 static void
-head(FILE *fp, int cnt)
+head(FILE *fp, intmax_t cnt)
 {
 	char *cp;
 	size_t error, readlen;
diff --git a/usr.bin/head/tests/head_test.sh b/usr.bin/head/tests/head_test.sh
index fb9f28c1d2af..2dfc1896f6ef 100755
--- a/usr.bin/head/tests/head_test.sh
+++ b/usr.bin/head/tests/head_test.sh
@@ -118,6 +118,48 @@ read_from_stdin_body() {
 	atf_check cmp outfile expectfile 
 }
 
+atf_test_case silent_header
+silent_header_head() {
+	atf_set "descr" "Test head(1)'s silent header feature"
+}
+silent_header_body() {
+	#head(1) defaults to head -n 10 if no args are given.
+	jot 11 1 11 > file1
+	jot 11 2 12 > file2
+	jot 10 1 10 > expectfile
+	jot 10 2 11 >> expectfile
+	head -q file1 file2 > outfile
+	atf_check cmp outfile expectfile
+}
+
+atf_test_case verbose_header
+verbose_header_head() {
+	atf_set "descr" "Test head(1)'s verbose header feature"
+}
+verbose_header_body() {
+	#head(1) defaults to head -n 10 if no args are given.
+	jot -b test 10 > file1
+	echo '==> file1 <==' > expectfile
+	cat file1 >> expectfile
+	head -v file1 > outfile
+	atf_check cmp outfile expectfile
+}
+
+atf_test_case si_number
+si_number_head() {
+	atf_set "descr" "Test head(1)'s SI number feature"
+}
+si_number_body() {
+	jot -b aaaaaaa 129 > file1
+	jot -b aaaaaaa 128 > expectfile
+	head -c 1k file1 > outfile
+	atf_check cmp outfile expectfile
+	jot 1025 1 1025 > file1
+	jot 1024 1 1024 > expectfile
+	head -n 1k file1 > outfile
+	atf_check cmp outfile expectfile
+}
+
 atf_init_test_cases() {
 	atf_add_test_case empty_file 
 	atf_add_test_case default_no_options
@@ -128,4 +170,7 @@ atf_init_test_cases() {
 	atf_add_test_case missing_line_count
 	atf_add_test_case invalid_line_count
 	atf_add_test_case read_from_stdin
+	atf_add_test_case silent_header
+	atf_add_test_case verbose_header
+	atf_add_test_case si_number
 }
diff --git a/usr.bin/tail/Makefile b/usr.bin/tail/Makefile
index cd85466cdc38..17cf55eb49e5 100644
--- a/usr.bin/tail/Makefile
+++ b/usr.bin/tail/Makefile
@@ -4,6 +4,7 @@
 
 PROG=	tail
 SRCS=	forward.c misc.c read.c reverse.c tail.c
+LIBADD=	util
 
 .if ${MK_CASPER} != "no" && !defined(RESCUE)
 LIBADD+= casper
diff --git a/usr.bin/tail/extern.h b/usr.bin/tail/extern.h
index cbf70f27e28d..830f9be79ab1 100644
--- a/usr.bin/tail/extern.h
+++ b/usr.bin/tail/extern.h
@@ -75,5 +75,5 @@ int mapprint(struct mapinfo *, off_t, off_t);
 int maparound(struct mapinfo *, off_t);
 void printfn(const char *, int);
 
-extern int Fflag, fflag, qflag, rflag, rval, no_files;
+extern int Fflag, fflag, qflag, rflag, rval, no_files, vflag;
 extern fileargs_t *fa;
diff --git a/usr.bin/tail/forward.c b/usr.bin/tail/forward.c
index 48516a730a50..f991b2517c48 100644
--- a/usr.bin/tail/forward.c
+++ b/usr.bin/tail/forward.c
@@ -243,8 +243,8 @@ show(file_info_t *file)
 	int ch;
 
 	while ((ch = getc(file->fp)) != EOF) {
-		if (last != file && no_files > 1) {
-			if (!qflag)
+		if (last != file) {
+			if (vflag || (qflag == 0 && no_files > 1))
 				printfn(file->file_name, 1);
 			last = file;
 		}
@@ -322,7 +322,7 @@ follow(file_info_t *files, enum STYLE style, off_t off)
 		if (file->fp) {
 			active = 1;
 			n++;
-			if (no_files > 1 && !qflag)
+			if (vflag || (qflag == 0 && no_files > 1))
 				printfn(file->file_name, 1);
 			forward(file->fp, file->file_name, style, off, &file->st);
 			if (Fflag && fileno(file->fp) != STDIN_FILENO)
diff --git a/usr.bin/tail/tail.1 b/usr.bin/tail/tail.1
index 19c4046b6683..38e8f95b9f59 100644
--- a/usr.bin/tail/tail.1
+++ b/usr.bin/tail/tail.1
@@ -30,7 +30,7 @@
 .\"
 .\"	@(#)tail.1	8.1 (Berkeley) 6/6/93
 .\"
-.Dd March 22, 2020
+.Dd July 12, 2022
 .Dt TAIL 1
 .Os
 .Sh NAME
@@ -39,7 +39,7 @@
 .Sh SYNOPSIS
 .Nm
 .Op Fl F | f | r
-.Op Fl q
+.Op Fl qv
 .Oo
 .Fl b Ar number | Fl c Ar number | Fl n Ar number
 .Oc
@@ -115,7 +115,7 @@ option if reading from standard input rather than a file.
 The location is
 .Ar number
 lines.
-.It Fl q
+.It Fl q, Fl -quiet, Fl -silent
 Suppresses printing of headers when multiple files are being examined.
 .It Fl r
 The
@@ -134,16 +134,26 @@ from the beginning or end of the input from which to begin the display.
 The default for the
 .Fl r
 option is to display all of the input.
+.It Fl v, Fl -verbose
+Prepend each file with a header.
 .El
 .Pp
-If more than a single file is specified, each file is preceded by a
+If more than a single file is specified, or if the
+.Fl v
+option is used, each file is preceded by a
 header consisting of the string
 .Dq Li "==> " Ns Ar XXX Ns Li " <=="
 where
 .Ar XXX
-is the name of the file unless
+is the name of the file.
+The
 .Fl q
-flag is specified.
+flag disables the printing of the header in all cases.
+.Pp
+All
+.Ar number
+arguments may also be specified with size suffixes supported by
+.Xr expand_number 3 .
 .Sh EXIT STATUS
 .Ex -std
 .Sh EXAMPLES
@@ -166,7 +176,8 @@ from the beginning and then follow the file as usual:
 .Sh SEE ALSO
 .Xr cat 1 ,
 .Xr head 1 ,
-.Xr sed 1
+.Xr sed 1 ,
+.Xr expand_number 3
 .Sh STANDARDS
 The
 .Nm
diff --git a/usr.bin/tail/tail.c b/usr.bin/tail/tail.c
index 5babd89135ca..492a6494628d 100644
--- a/usr.bin/tail/tail.c
+++ b/usr.bin/tail/tail.c
@@ -56,12 +56,14 @@ static const char sccsid[] = "@(#)tail.c	8.1 (Berkeley) 6/6/93";
 #include <string.h>
 #include <unistd.h>
 
+#include <libutil.h>
+
 #include <libcasper.h>
 #include <casper/cap_fileargs.h>
 
 #include "extern.h"
 
-int Fflag, fflag, qflag, rflag, rval, no_files;
+int Fflag, fflag, qflag, rflag, rval, no_files, vflag;
 fileargs_t *fa;
 
 static void obsolete(char **);
@@ -72,6 +74,9 @@ static const struct option long_opts[] =
 	{"blocks",	required_argument,	NULL, 'b'},
 	{"bytes",	required_argument,	NULL, 'c'},
 	{"lines",	required_argument,	NULL, 'n'},
+	{"quiet",	no_argument,		NULL, 'q'},
+	{"silent",	no_argument,		NULL, 'q'},
+	{"verbose",	no_argument,		NULL, 'v'},
 	{NULL,		no_argument,		NULL, 0}
 };
 
@@ -85,7 +90,6 @@ main(int argc, char *argv[])
 	enum STYLE style;
 	int ch, first;
 	file_info_t file, *filep, *files;
-	char *p;
 	cap_rights_t rights;
 
 	/*
@@ -103,8 +107,9 @@ main(int argc, char *argv[])
 #define	ARG(units, forward, backward) {					\
 	if (style)							\
 		usage();						\
-	off = strtoll(optarg, &p, 10) * (units);                        \
-	if (*p)								\
+	if (expand_number(optarg, &off))				\
+		err(1, "illegal offset -- %s", optarg);			\
+	if (off > INT64_MAX / units || off < INT64_MIN / units )	\
 		errx(1, "illegal offset -- %s", optarg);		\
 	switch(optarg[0]) {						\
 	case '+':							\
@@ -124,7 +129,7 @@ main(int argc, char *argv[])
 	obsolete(argv);
 	style = NOTSET;
 	off = 0;
-	while ((ch = getopt_long(argc, argv, "+Fb:c:fn:qr", long_opts, NULL)) !=
+	while ((ch = getopt_long(argc, argv, "+Fb:c:fn:qrv", long_opts, NULL)) !=
 	    -1)
 		switch(ch) {
 		case 'F':	/* -F is superset of (and implies) -f */
@@ -144,10 +149,15 @@ main(int argc, char *argv[])
 			break;
 		case 'q':
 			qflag = 1;
+			vflag = 0;
 			break;
 		case 'r':
 			rflag = 1;
 			break;
+		case 'v':
+			vflag = 1;
+			qflag = 0;
+			break;
 		case '?':
 		default:
 			usage();
@@ -227,7 +237,7 @@ main(int argc, char *argv[])
 				ierr(fn);
 				continue;
 			}
-			if (argc > 1 && !qflag) {
+			if (vflag || (qflag == 0 && argc > 1)) {
 				printfn(fn, !first);
 				first = 0;
 			}
diff --git a/usr.bin/tail/tests/tail_test.sh b/usr.bin/tail/tests/tail_test.sh
index 412d198713e6..0b845dff0cf8 100755
--- a/usr.bin/tail/tests/tail_test.sh
+++ b/usr.bin/tail/tests/tail_test.sh
@@ -351,6 +351,47 @@ follow_rename_body()
 	atf_check kill $pid
 }
 
+atf_test_case silent_header
+silent_header_head() {
+	atf_set "descr" "Test tail(1)'s silent header feature"
+}
+silent_header_body() {
+	jot 11 1 11 > file1
+	jot 11 2 12 > file2
+	jot 10 2 11 > expectfile
+	jot 10 3 12 >> expectfile
+	tail -q file1 file2 > outfile
+	atf_check cmp outfile expectfile
+}
+
+atf_test_case verbose_header
+verbose_header_head() {
+	atf_set "descr" "Test tail(1)'s verbose header feature"
+}
+verbose_header_body() {
+	jot 11 1 11 > file1
+	echo '==> file1 <==' > expectfile
+	jot 10 2 11 >> expectfile
+	tail -v file1 > outfile
+	atf_check cmp outfile expectfile
+}
+
+atf_test_case si_number
+si_number_head() {
+	atf_set "descr" "Test tail(1)'s SI number feature"
+}
+si_number_body() {
+	jot -b aaaaaaa 129 > file1
+	jot -b aaaaaaa 128 > expectfile
+	tail -c 1k file1 > outfile
+	atf_check cmp outfile expectfile
+	jot 1025 1 1025 > file1
+	jot 1024 2 1025 > expectfile
+	tail -n 1k file1 > outfile
+	atf_check cmp outfile expectfile
+}
+
+
 atf_init_test_cases()
 {
 	atf_add_test_case empty_r
@@ -371,4 +412,7 @@ atf_init_test_cases()
 	atf_add_test_case follow
 	atf_add_test_case follow_stdin
 	atf_add_test_case follow_rename
+	atf_add_test_case silent_header
+	atf_add_test_case verbose_header
+	atf_add_test_case si_number
 }