diff options
Diffstat (limited to 'usr.bin/tee')
-rw-r--r-- | usr.bin/tee/Makefile | 9 | ||||
-rw-r--r-- | usr.bin/tee/Makefile.depend | 17 | ||||
-rw-r--r-- | usr.bin/tee/tee.1 | 98 | ||||
-rw-r--r-- | usr.bin/tee/tee.c | 193 | ||||
-rw-r--r-- | usr.bin/tee/tests/Makefile | 6 | ||||
-rw-r--r-- | usr.bin/tee/tests/sigint.orch | 14 | ||||
-rw-r--r-- | usr.bin/tee/tests/tee_test.sh | 106 |
7 files changed, 443 insertions, 0 deletions
diff --git a/usr.bin/tee/Makefile b/usr.bin/tee/Makefile new file mode 100644 index 000000000000..fbfbb8e1c9c3 --- /dev/null +++ b/usr.bin/tee/Makefile @@ -0,0 +1,9 @@ +.include <src.opts.mk> + +PACKAGE= runtime +PROG= tee + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include <bsd.prog.mk> diff --git a/usr.bin/tee/Makefile.depend b/usr.bin/tee/Makefile.depend new file mode 100644 index 000000000000..38d979787478 --- /dev/null +++ b/usr.bin/tee/Makefile.depend @@ -0,0 +1,17 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + include \ + include/xlocale \ + lib/${CSU_DIR} \ + lib/libc \ + lib/libcapsicum \ + lib/libcasper/libcasper \ + lib/libcompiler_rt \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/usr.bin/tee/tee.1 b/usr.bin/tee/tee.1 new file mode 100644 index 000000000000..9884dcf37919 --- /dev/null +++ b/usr.bin/tee/tee.1 @@ -0,0 +1,98 @@ +.\" Copyright (c) 1991, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" the Institute of Electrical and Electronics Engineers, Inc. +.\" +.\" 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. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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 December 25, 2024 +.Dt TEE 1 +.Os +.Sh NAME +.Nm tee +.Nd duplicate standard input +.Sh SYNOPSIS +.Nm +.Op Fl ai +.Op Ar +.Sh DESCRIPTION +The +.Nm +utility copies standard input to standard output, +making a copy in zero or more files. +The output is unbuffered. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl a +Append the output to the files rather than +overwriting them. +.It Fl i +Ignore the +.Dv SIGINT +signal. +.El +.Pp +The following operands are available: +.Bl -tag -width indent +.It Ar file +A pathname of an output +.Ar file . +.El +.Pp +The +.Nm +utility takes the default action for all signals, +except in the event of the +.Fl i +option. +.Pp +This implementation of the +.Nm +utility may also write to +.Xr unix 4 +sockets. +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +Send the echoed message both to stdout and to the +.Pa greetings.txt +file: +.Bd -literal -offset indent +$ echo "Hello" | tee greetings.txt +Hello +.Ed +.Sh STANDARDS +The +.Nm +utility is expected to be +.St -p1003.2 +compatible. +.Sh HISTORY +The +.Nm +command first appeared in +.At v7 . diff --git a/usr.bin/tee/tee.c b/usr.bin/tee/tee.c new file mode 100644 index 000000000000..fb73b311a31b --- /dev/null +++ b/usr.bin/tee/tee.c @@ -0,0 +1,193 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1988, 1993 + * The Regents of the University of California. All rights reserved. + * + * 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. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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 <sys/types.h> +#include <sys/capsicum.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/un.h> + +#include <capsicum_helpers.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +struct entry { + int fd; + const char *name; + STAILQ_ENTRY(entry) entries; +}; +static STAILQ_HEAD(, entry) head = STAILQ_HEAD_INITIALIZER(head); + +static void add(int, const char *); +static int tee_open(const char *, int); +static void usage(void) __dead2; + +int +main(int argc, char *argv[]) +{ + char *bp, *buf; + struct entry *p; + int append, ch, exitval, fd, n, oflags, rval, wval; +#define BSIZE (8 * 1024) + + append = 0; + while ((ch = getopt(argc, argv, "ai")) != -1) + switch((char)ch) { + case 'a': + append = 1; + break; + case 'i': + (void)signal(SIGINT, SIG_IGN); + break; + case '?': + default: + usage(); + } + argv += optind; + argc -= optind; + + if ((buf = malloc(BSIZE)) == NULL) + err(1, "malloc"); + + if (caph_limit_stdin() == -1 || caph_limit_stderr() == -1) + err(EXIT_FAILURE, "unable to limit stdio"); + + add(STDOUT_FILENO, "stdout"); + + oflags = O_WRONLY | O_CREAT; + if (append) + oflags |= O_APPEND; + else + oflags |= O_TRUNC; + + for (exitval = 0; *argv; ++argv) { + if ((fd = tee_open(*argv, oflags)) < 0) { + warn("%s", *argv); + exitval = 1; + } else { + add(fd, *argv); + } + } + + if (caph_enter() < 0) + err(EXIT_FAILURE, "unable to enter capability mode"); + while ((rval = read(STDIN_FILENO, buf, BSIZE)) > 0) + STAILQ_FOREACH(p, &head, entries) { + n = rval; + bp = buf; + do { + if ((wval = write(p->fd, bp, n)) == -1) { + warn("%s", p->name); + exitval = 1; + break; + } + bp += wval; + } while (n -= wval); + } + if (rval < 0) + err(1, "read"); + exit(exitval); +} + +static void +usage(void) +{ + (void)fprintf(stderr, "usage: tee [-ai] [file ...]\n"); + exit(1); +} + +static void +add(int fd, const char *name) +{ + struct entry *p; + cap_rights_t rights; + + if (fd == STDOUT_FILENO) { + if (caph_limit_stdout() == -1) + err(EXIT_FAILURE, "unable to limit stdout"); + } else { + cap_rights_init(&rights, CAP_WRITE, CAP_FSTAT); + if (caph_rights_limit(fd, &rights) < 0) + err(EXIT_FAILURE, "unable to limit rights"); + } + + if ((p = malloc(sizeof(struct entry))) == NULL) + err(1, "malloc"); + p->fd = fd; + p->name = name; + STAILQ_INSERT_HEAD(&head, p, entries); +} + +static int +tee_open(const char *path, int oflags) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + size_t pathlen; + int fd; + + if ((fd = open(path, oflags, DEFFILEMODE)) >= 0) + return (fd); + + if (errno != EOPNOTSUPP) + return (-1); + + pathlen = strnlen(path, sizeof(sun.sun_path)); + if (pathlen >= sizeof(sun.sun_path)) + goto failed; + + /* + * For EOPNOTSUPP, we'll try again as a unix(4) socket. Any errors here + * we'll just surface as the original EOPNOTSUPP since they may not have + * intended for this. + */ + fd = socket(PF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + goto failed; + + (void)strlcpy(&sun.sun_path[0], path, sizeof(sun.sun_path)); + sun.sun_len = SUN_LEN(&sun); + + if (connect(fd, (const struct sockaddr *)&sun, sun.sun_len) == 0) + return (fd); + +failed: + if (fd >= 0) + close(fd); + errno = EOPNOTSUPP; + return (-1); +} diff --git a/usr.bin/tee/tests/Makefile b/usr.bin/tee/tests/Makefile new file mode 100644 index 000000000000..acb78e671a8c --- /dev/null +++ b/usr.bin/tee/tests/Makefile @@ -0,0 +1,6 @@ +PACKAGE= tests + +ATF_TESTS_SH+= tee_test +${PACKAGE}FILES+= sigint.orch + +.include <bsd.test.mk> diff --git a/usr.bin/tee/tests/sigint.orch b/usr.bin/tee/tests/sigint.orch new file mode 100644 index 000000000000..2fc0b32c61fb --- /dev/null +++ b/usr.bin/tee/tests/sigint.orch @@ -0,0 +1,14 @@ +-- We expect the caller to have spawned the appropriate application +write "text\r" +match "text" + +write "^C" + +-- If SIGINT isn't being ignored, we'll bail out somewhere in the process of +-- writing to the pty or trying to read from it to perform the following match. +write "text\r" +match "text" + +-- Finally, just close it out cleanly. +write "^D" +eof() diff --git a/usr.bin/tee/tests/tee_test.sh b/usr.bin/tee/tests/tee_test.sh new file mode 100644 index 000000000000..cf8e74dd47e7 --- /dev/null +++ b/usr.bin/tee/tests/tee_test.sh @@ -0,0 +1,106 @@ +# +# Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> +# +# SPDX-License-Identifier: BSD-2-Clause +# + +atf_test_case single_file +single_file_body() +{ + atf_check -o inline:"text\n" -x "echo text | tee file" + atf_check -o inline:"text\n" cat file +} + +atf_test_case device +device_body() +{ + atf_check -e inline:"text\n" -o inline:"text\n" -x \ + "echo text | tee /dev/stderr" +} + +atf_test_case multiple_file +multiple_file_body() +{ + atf_check -o inline:"text\n" -x "echo text | tee file1 file2" + atf_check -o inline:"text\n" cat file1 + atf_check -o inline:"text\n" cat file2 +} + +atf_test_case append +append_body() +{ + atf_check -o ignore -x "echo text | tee file" + atf_check -o inline:"text\n" cat file + + # Should overwrite if done again + atf_check -o ignore -x "echo text | tee file" + atf_check -o inline:"text\n" cat file + + # Should duplicate if we use -a + atf_check -o ignore -x "echo text | tee -a file" + atf_check -o inline:"text\ntext\n" cat file +} + +atf_test_case sigint_ignored +sigint_ignored_head() +{ + # This is most cleanly tested with interactive input, to avoid adding + # a lot of complexity in trying to manage an input and signal delivery + # dance purely in shell. + atf_set "require.progs" "porch" +} +sigint_ignored_body() +{ + + # sigint.orch will write "text" to the file twice if we're properly + # ignoring SIGINT, so we'll do one test to confirm that SIGINT is not + # being ignored by porch(1), then another to confirm that tee(1) will + # ignore SIGINT when instructed to. + atf_check -s exit:1 -e ignore \ + porch -f $(atf_get_srcdir)/sigint.orch tee file + atf_check -o inline:"text\n" cat file + + atf_check porch -f $(atf_get_srcdir)/sigint.orch tee -i file + atf_check -o inline:"text\ntext\n" cat file +} + +atf_test_case unixsock "cleanup" +unixsock_pidfile="nc.pid" + +unixsock_body() +{ + outfile=out.log + + nc -lU logger.sock > "$outfile" & + npid=$! + + atf_check -o save:"$unixsock_pidfile" echo "$npid" + + # Wait for the socket to come online, just in case. + while [ ! -S logger.sock ]; do + sleep 0.1 + done + + atf_check -o inline:"text over socket\n" -x \ + 'echo "text over socket" | tee logger.sock' + + atf_check rm "$unixsock_pidfile" + atf_check -o inline:"text over socket\n" cat "$outfile" +} +unixsock_cleanup() +{ + if [ -s "$unixsock_pidfile" ]; then + read npid < "$unixsock_pidfile" + kill "$npid" + fi +} + +atf_init_test_cases() +{ + atf_add_test_case single_file + atf_add_test_case device + atf_add_test_case multiple_file + atf_add_test_case append + atf_add_test_case sigint_ignored + atf_add_test_case unixsock +} |