diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -1223,6 +1223,8 @@ .. stat .. + tac + .. tail .. tar diff --git a/usr.bin/tac/Makefile b/usr.bin/tac/Makefile new file mode 100644 --- /dev/null +++ b/usr.bin/tac/Makefile @@ -0,0 +1,8 @@ +.include + +PROG = tac + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include diff --git a/usr.bin/tac/tac.1 b/usr.bin/tac/tac.1 new file mode 100644 --- /dev/null +++ b/usr.bin/tac/tac.1 @@ -0,0 +1,97 @@ +.\"- +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2025 Dag-Erling Smørgrav +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd August 7, 2025 +.Dt TAC 1 +.Os +.Sh NAME +.Nm tac +.Nd print a file in reverse order +.Sh SYNOPSIS +.Nm +.Op Fl bns +.Op Ar +.Sh DESCRIPTION +The +.Nm +utility reads text files sequentially and writes them to the standard +output in reverse order, bottom to top. +The +.Ar file +operands are processed in command-line order. +If +.Ar file +is a single dash +.Pq Sq \- +or absent, +.Nm +reads from the standard input. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl b +Number non-blank output lines, starting at 1, in the order in which +they are printed. +If both +.Fl b +and +.Fl n +are specified, +.Fl b +takes precedence. +.It Fl n +Number all output lines, starting at 1, in the order in which they are +printed. +If both +.Fl b +and +.Fl n +are specified, +.Fl b +takes precedence. +.It Fl s +Squeeze multiple successive blank lines into one. +If +.Fl n +was specified, omitted lines do not increment the line counter. +.El +.Sh EXIT STATUS +The +.Nm +utility exits 0 on success, and >0 if an error occurs. +.Sh SEE ALSO +.Xr cat 1 +.Sh HISTORY +The +.Nm +utility was added in +.Fx 15.0 . +.Sh AUTHORS +.An -nosplit +The +.Nm +utility and this manual page were written by +.An Dag-Erling Sm\(/orgrav Aq Mt des@FreeBSD.org . diff --git a/usr.bin/tac/tac.c b/usr.bin/tac/tac.c new file mode 100644 --- /dev/null +++ b/usr.bin/tac/tac.c @@ -0,0 +1,181 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Dag-Erling Smørgrav + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include + +#include +#include +#include +#include +#include + +static bool bflag, nflag, sflag; + +static void +ftac(FILE *f, const char *fn) +{ + struct stat sb; + char *buf, *end, *line, *p; + size_t *lines; + size_t len, szbuf, szlines, nlines; + unsigned long n; + bool blank; + + /* get file size if possible, otherwise we will realloc */ + if (fstat(fileno(f), &sb) == 0) + szbuf = sb.st_size; + else + szbuf = BUFSIZ; + szlines = 64; + + /* allocate buf for text and lines for line offsets */ + if ((end = buf = malloc(szbuf)) == NULL || + (lines = malloc(szlines * sizeof(*lines))) == NULL) + err(EXIT_FAILURE, NULL); + + /* first line */ + lines[1] = 0; + nlines = 1; + + /* read the file */ + for (;;) { + /* grow the buffer if full */ + if (end == buf + szbuf) { + szbuf *= 2; + if ((buf = realloc(buf, szbuf)) == NULL) + err(EXIT_FAILURE, NULL); + end = buf + szbuf / 2; + } + /* read more data into the buffer */ + if ((len = fread(end, 1, szbuf - (end - buf), f)) == 0) { + if (ferror(f)) + err(EXIT_FAILURE, "%s", fn); + break; + } + /* locate newlines */ + for (p = end; p < end + len; p++) { + if (*p != '\n') + continue; + /* grow the line offset array if full */ + if (nlines == szlines) { + szlines *= 2; + if ((lines = reallocarray(lines, szlines, + sizeof(*lines))) == NULL) + err(EXIT_FAILURE, NULL); + } + lines[nlines++] = p - buf + 1; + } + end += len; + } + + /* dummy last line? */ + if (buf + lines[nlines - 1] == end) + nlines--; + + /* non-empty file? */ + if (end > buf) { + /* print in reverse order */ + flockfile(stdout); + for (n = 1; nlines; end = line) { + line = buf + lines[--nlines]; + /* squeeze consecutive blank lines */ + if (*line == '\n' && blank && sflag) + continue; + blank = *line == '\n'; + /* print line number if requested */ + if (nflag && (!blank || !bflag)) + (void)fprintf(stdout, "%6lu\t", n++); + /* print the line */ + (void)fwrite_unlocked(line, 1, end - line, stdout); + /* print a newline if there wasn't one already */ + if (line[end - line - 1] != '\n') + (void)fputc_unlocked('\n', stdout); + /* check for errors */ + if (ferror(stdout)) + err(EXIT_FAILURE, "stdout"); + } + funlockfile(stdout); + } + free(lines); + free(buf); +} + +static void +tac(const char *fn) +{ + FILE *f; + + if (fn[0] == '-' && fn[1] == '\0') { + f = stdin; + fn = "stdin"; + } else { + if ((f = fopen(fn, "r")) == NULL) + err(EXIT_FAILURE, "%s", fn); + } + ftac(f, fn); + if (f != stdin) + fclose(f); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: tac [-bns] [file [...]]\n"); + exit(EXIT_FAILURE); +} + +int +main(int argc, char *argv[]) +{ + int opt; + + while ((opt = getopt(argc, argv, "bns")) != -1) { + switch (opt) { + case 'b': + bflag = nflag = true; + break; + case 'n': + nflag = true; + break; + case 's': + sflag = true; + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (argc == 0) { + tac("-"); + } else { + while (argc--) + tac(*argv++); + } + exit(EXIT_SUCCESS); +} diff --git a/usr.bin/tac/tests/Makefile b/usr.bin/tac/tests/Makefile new file mode 100644 --- /dev/null +++ b/usr.bin/tac/tests/Makefile @@ -0,0 +1,4 @@ +PACKAGE= tests +ATF_TESTS_SH= tac_test + +.include diff --git a/usr.bin/tac/tests/tac_test.sh b/usr.bin/tac/tests/tac_test.sh new file mode 100644 --- /dev/null +++ b/usr.bin/tac/tests/tac_test.sh @@ -0,0 +1,97 @@ +# +# Copyright (c) 2025 Dag-Erling Smørgrav +# +# SPDX-License-Identifier: BSD-2-Clause +# + +fwd="The +magic +words +are +x +Squeamish +Ossifrage" +rev="Ossifrage +Squeamish +x +are +words +magic +The" + +atf_test_case basic +basic_head() { + atf_set descr "Basic operation" +} +basic_body() { + echo "$fwd" >input + echo "$rev" >expect + atf_check -o file:expect tac input + atf_check -o file:expect tac empty + atf_check tac empty + atf_check tac input + echo "$rev" | sed 's/x/\n/' >output + cat -n output >expect + atf_check -o file:expect tac -n input + cat -b output >expect + atf_check -o file:expect tac -b input + atf_check -o file:expect tac -bn input + atf_check -o file:expect tac -nb input +} + +atf_test_case squeeze +squeeze_head() { + atf_set descr "Line squeezeing" +} +squeeze_body() { + echo "$fwd" | sed 's/x/\n\n/' >input + echo "$rev" | sed 's/x/\n\n/' >output + atf_check -o file:output tac input + cat -s output >expect + atf_check -o file:expect tac -s input + cat -sn output >expect + atf_check -o file:expect tac -sn input + cat -snb output >expect + atf_check -o file:expect tac -snb input +} + +atf_test_case multi +multi_head() { + atf_set descr "Multiple inputs" +} +multi_body() { + printf "a\nx\n" >a + printf "b\ny\n" >b + printf "c\nz\n" >c + printf "x\na\ny\nb\nz\nc\n" >expect + atf_check -o file:expect tac - b c