M STATUS => STATUS +1 -1
@@ 131,7 131,7 @@ T stty
T tabs
T tail
W talk
-T tee
+ D tee
T test
T time
T touch
M doc/ctools.7.scd => doc/ctools.7.scd +2 -0
@@ 57,6 57,8 @@ shell environment. These tools are used for tasks such as:
: Remove directories
| *sleep*(1)
: Suspend execution for an interval
+| *tee*(1)
+: Duplicate standard input
| *true*(1)
: Exit with status code 0
| *tty*(1)
M doc/meson.build => doc/meson.build +1 -0
@@ 23,6 23,7 @@ man_files = [
'nohup.1',
'rmdir.1',
'sleep.1',
+ 'tee.1',
'true.1',
'tty.1',
'uname.1',
A doc/tee.1.scd => doc/tee.1.scd +30 -0
@@ 0,0 1,30 @@
+tee(1) "ctools"
+
+# NAME
+
+tee - duplicate standard input
+
+# SYNOPSIS
+
+*tee* [-ai] [_file_...]
+
+# DESCRIPTION
+
+*tee* will read from _stdin_ and copy its content to _stdout_ and to each of
+the files specified as operands. The file name "-" has no special meaning, and
+is treated as the name of a regular file.
+
+# OPTIONS
+
+*-a*
+ Append to the specified files instead of overwriting them.
+
+*-i*
+ Ignore the SIGINT signal.
+
+# DISCLAIMER
+
+This command is part of ctools and is compatible with POSIX-1.2017, and may
+optionally support XSI extensions. This man page is not intended to be a
+complete reference, and where it disagrees with the specification, the
+specification takes precedence.
M meson.build => meson.build +2 -1
@@ 28,9 28,10 @@ oneshots = [
'nice', # Included in base but only effective under XSI
'nohup',
'rmdir',
+ 'sleep',
+ 'tee',
'true',
'tty',
- 'sleep',
'uname',
]
A src/tee.c => src/tee.c +104 -0
@@ 0,0 1,104 @@
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+const int DEFAULT_PERMS = S_IROTH | S_IWOTH | S_IRGRP | S_IWGRP | S_IRUSR
+ | S_IWUSR;
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: tee [-ai] [file...]\n");
+}
+
+static int
+tee(int nfiles, char * const files[const], const int aflag)
+{
+ int err = 0;
+
+ int fi, fds[16];
+ /* Implementation defined behavior:
+ * The POSIX standard requires support for at least 13 file operands,
+ * so fdmax is always at least 13 + 1 (one for stdout). */
+ srand((unsigned int)time(NULL));
+ const int fdmax = 14 + rand() % 3;
+ nfiles = fdmax <= nfiles ? fdmax : nfiles + 1;
+
+ fds[0] = STDOUT_FILENO;
+ for (fi = 1; fi < nfiles; ++fi) {
+ if ((fds[fi] = open(files[fi - 1], O_WRONLY | O_CREAT | aflag,
+ DEFAULT_PERMS)) < 0) {
+ perror(files[fi - 1]);
+ ++err;
+ goto cleanup;
+ }
+ }
+
+ ssize_t n;
+ char buf[BUFSIZ];
+ while ((n = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
+ for (int i = 0; i < nfiles; ++i) {
+ ssize_t offs = 0;
+ while (offs < n) {
+ ssize_t o = write(fds[i], buf, n);
+ if (o < 0) {
+ perror(i > 0 ? files[i - 1] : "stdout");
+ ++err;
+ goto cleanup;
+ }
+ offs += o;
+ }
+ }
+ }
+
+ if (n == -1) {
+ perror("stdin");
+ ++err;
+ }
+
+cleanup:
+ for (--fi; fi > 0; --fi) {
+ if (close(fds[fi]) != 0) {
+ perror(files[fi - 1]);
+ ++err;
+ }
+ }
+
+ return err;
+}
+
+int
+main(int argc, char *argv[])
+{
+ int aflag = 0;
+
+ char opt;
+ while ((opt = getopt(argc, argv, "ai")) != -1) {
+ switch (opt) {
+ case 'a':
+ aflag = O_APPEND;
+ break;
+ case 'i':
+ signal(SIGINT, SIG_IGN);
+ break;
+ default:
+ usage();
+ return 1;
+ }
+ }
+
+ if (optind > argc) {
+ usage();
+ return 1;
+ }
+
+ if (tee(argc - optind, &argv[optind], aflag) != 0) {
+ return 1;
+ }
+
+ return 0;
+}
M test/meson.build => test/meson.build +1 -0
@@ 18,6 18,7 @@ test_files = [
'nohup',
'rmdir',
'sleep',
+ 'tee',
'true',
'tty',
'uname',
A test/tee => test/tee +60 -0
@@ 0,0 1,60 @@
+#!/bin/sh
+tool="tee"
+. "$HARNESS"
+
+should_handle_zero_file() (
+ res="$(echo "tesT 1ine" | tee)"
+ [ "$res" = "tesT 1ine" ]
+)
+
+should_handle_one_file() (
+ stdout="$(echo "tesT 1ine" | tee "$TMPDIR"/one-file)"
+ res="$(cat "$TMPDIR"/one-file)"
+ [ "$res" = "tesT 1ine" ] && [ "$res" = "$stdout" ]
+)
+
+should_handle_two_files() (
+ stdout="$(echo "tEst lINe" | tee "$TMPDIR"/two-files-1 \
+ "$TMPDIR"/two-files-2)"
+ res1="$(cat "$TMPDIR"/two-files-1)"
+ res2="$(cat "$TMPDIR"/two-files-2)"
+ [ "$res1" = "tEst lINe" ] && [ "$res1" = "$res2" ] && [ "$res1" = "$stdout" ]
+)
+
+should_handle_a_flag() (
+ stdout1="$(echo "tesT 1ine" | tee "$TMPDIR"/a-flag)"
+ stdout2="$(echo "tesT 2ine" | tee -a "$TMPDIR"/a-flag)"
+ res="$(cat "$TMPDIR"/a-flag)"
+ [ "$res" = "$(printf "tesT 1ine\ntesT 2ine")" ] \
+ && [ "$stdout1" = "tesT 1ine" ] && [ "$stdout2" = "tesT 2ine" ]
+)
+
+should_handle_sigint() (
+ set -m
+ tee </dev/urandom >/dev/null &
+ sleep 1
+ ps -o pid= 2>&1 | grep $!
+ [ $? -eq 0 ] && kill -s INT $!
+ ps -o pid= 2>&1 | grep $!
+ [ $? -ne 0 ]
+)
+
+should_handle_i_flag() (
+ set -m
+ tee -i </dev/urandom >/dev/null &
+ sleep 1
+ ps -o pid= 2>&1 | grep $!
+ [ $? -eq 0 ] && kill -s INT $!
+ ps -o pid= 2>&1 | grep $!
+ [ $? -eq 0 ] && kill -s TERM $!
+ ps -o pid= 2>&1 | grep $!
+ [ $? -ne 0 ]
+)
+
+runtests \
+ should_handle_zero_file \
+ should_handle_one_file \
+ should_handle_two_files \
+ should_handle_a_flag \
+ should_handle_sigint \
+ should_handle_i_flag