~sircmpwn/ctools

599fbbb9feec987296d6bbdd82e19efe0ce77f3d — Gabor Koszegi 1 year, 4 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       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