599fbbb9feec987296d6bbdd82e19efe0ce77f3d — Gabor Koszegi 21 days ago f43de61
Implement tee
8 files changed, 201 insertions(+), 2 deletions(-)

M STATUS
M doc/ctools.7.scd
M doc/meson.build
A doc/tee.1.scd
M meson.build
A src/tee.c
M test/meson.build
A test/tee
M STATUS => STATUS +1 -1
@@ 131,7 131,7 @@
 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 @@
 :  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 @@
 	'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 @@
 	'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 @@
 	'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