~nabijaczleweli/voreutils

ccbd57c0675fe805fb08438ae65c46d351b76c43 — наб a month ago 8341746
Add mktemp. Proof wc.1
10 files changed, 465 insertions(+), 22 deletions(-)

M README.md
A cmd/mktemp.cpp
M cmd/printf.cpp
M include/vore-print
A man/mktemp.1
M man/nohup.1
M man/pwd.1
M man/uname.1
A tests/mktemp
M tests/tee
M README.md => README.md +1 -1
@@ 19,7 19,7 @@ GNU coreutils provide the following 105 binaries, according to `dpkg -L coreutil
  * ☐ /bin/ls
  * ☑ /bin/mkdir
  * ☑ /bin/mknod
  * ☐ /bin/mktemp
  * ☑ /bin/mktemp
  * ☐ /bin/mv
  * ☑ /bin/pwd
  * ☐ /bin/readlink

A cmd/mktemp.cpp => cmd/mktemp.cpp +132 -0
@@ 0,0 1,132 @@
// SPDX-License-Identifier: 0BSD


#include <algorithm>
#include <cstdio>
#include <errno.h>
#include <fcntl.h>
#include <random>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <vore-getopt>
#include <vore-print>
#undef rand


#define USAGE(self)                                            \
	"usage: %s [-dqu]    [-p directory] [-s suffix]\n"           \
	"usage: %s [-dqu] [-t|-p directory] [-s suffix] template\n", \
	    self, self

static constexpr const char dictionary[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";


int main(int argc, char * const * argv) {
	bool create = true, quiet{};
	const char *template_{}, *directory{}, *suffix = "";
	int (*makef)(const char *) = [](auto fn) { return open(fn, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600); };
	int (*rmf)(const char *)   = [](auto fn) { return unlink(fn); };
	for(auto && [arg, val] : vore::opt::get{argc,
	                                        argv,
	                                        "duqtp:s:",
	                                        {{"directory", no_argument, nullptr, 'd'},
	                                         {"quiet", no_argument, nullptr, 'q'},
	                                         {"dry-run", no_argument, nullptr, 'u'},
	                                         {"tmpdir", optional_argument, nullptr, 'p'},
	                                         {"suffix", required_argument, nullptr, 's'}}})
		switch(arg) {
			case 'd':
				makef = [](auto fn) { return mkdir(fn, 0700); };
				rmf   = [](auto fn) { return rmdir(fn); };
				break;
			case 'q':
				quiet = true;
				break;
			case 'u':
				create = false;
				break;
			case 'p':
				if(val) {
					directory = val;
					break;
				} else
					[[fallthrough]];
			case 't':
				if(!directory)
					directory = std::getenv("TMPDIR") ?: "/tmp";
				break;
			case 's':
				suffix = val;
				break;
			default:
				return std::fprintf(stderr, USAGE(argv[0])), 1;
		}
	if(*(argv + optind)) {
		if(*(argv + optind + 1))
			return std::fprintf(stderr, USAGE(argv[0])), 1;

		template_ = *(argv + optind);
		if(!directory)
			directory = "";
	} else {
		template_ = "tmp.XXXXXXXXXX";
		if(!directory)
			directory = std::getenv("TMPDIR") ?: "/tmp";
	}


	std::string final{directory};
	if(!final.empty() && final.back() != '/')
		final += '/';
	final += template_;
	auto lastx_idx = final.find_last_of("X/");
	if(lastx_idx == std::string::npos || final[lastx_idx] == '/' || lastx_idx < 3 || [&] {
		   for(std::size_t lx = lastx_idx, minx = 3; --minx && lx-- + 1;)
			   if(final[lx] != 'X')
				   return true;
		   return false;
	   }())
		return std::fprintf(stderr, "%s: %s: < 3 Xes\n", argv[0], template_), 1;
	final += suffix;

	auto x_end = final.begin() + lastx_idx + 1, x_begin = x_end - 1;
	while(x_begin != final.begin() && *(x_begin - 1) == 'X')
		--x_begin;


	std::mt19937 rand{std::random_device{}()};
	std::uniform_int_distribution<short> dist{0, sizeof(dictionary) - 1 - 1};  // char is UB
	auto reroll = [&] { std::generate(x_begin, x_end, [&] { return dictionary[dist(rand)]; }); };
	auto err    = [&] {
    if(!quiet)
      std::fprintf(stderr, "%s: %s%s%s%s (%s): %s\n", argv[0], directory, *directory && directory[std::strlen(directory) - 1] != '/' ? "/" : "", template_,
                   suffix, final.c_str(), std::strerror(errno));
	};
	if(create)
		for(;;) {
			reroll();
			if(makef(final.c_str()) != -1)
				break;
			else if(errno != EEXIST)
				return err(), 1;
		}
	else
		for(struct stat sb;;) {
			reroll();
			if(!stat(final.c_str(), &sb))
				;
			else if(errno == ENOENT)
				break;
			else
				return err(), 1;
		}

	if(vore::print_spaced(argv[0], &final, &final + 1)) {
		if(create)
			rmf(final.c_str());
		return 1;
	} else
		return 0;
}

M cmd/printf.cpp => cmd/printf.cpp +1 -1
@@ 170,7 170,7 @@ std::optional<std::uint8_t> decode_backslash(const char * self, const char * for
}


// These two would be much better fit to be in the conv == 'q' branch, but transition calls itself, so
// These two would be much better fit to be in the conv == 'q' branch, but transition calls itself, so not until P0847 (Deducing this)
enum class state_t { none, quoted, dollared };
static void transition(state_t & state, state_t to) {
	switch(to) {

M include/vore-print => include/vore-print +5 -3
@@ 16,10 16,12 @@ namespace vore {
			static const char * c_str(const T * str) {
				return str ? str->data() : nullptr;
			}
			template <class = void>
			static const char * c_str(const char * str) {
				return str;
			template <class T>
			static const char * c_str(const T & str) {
				return str.data();
			}
			[[maybe_unused]] static const char * c_str(const char * str) { return str; }
			[[maybe_unused]] static const char * c_str(char * str) { return str; }
		}

		int flush_stdout(const char * self) {

A man/mktemp.1 => man/mktemp.1 +236 -0
@@ 0,0 1,236 @@
.\" SPDX-License-Identifier: 0BSD
.\"
.Dd
.Dt MKTEMP 1
.Os
.
.Sh NAME
.Nm mktemp
.Nd create temporary file or directory
.Sh SYNOPSIS
.Nm
.Op Fl dqu
.Op Fl p Ar directory
.Op Fl s Ar suffix
.Nm
.Op Fl dqu
.Op Fl t Ns | Ns Fl p Ar directory
.Op Fl s Ar suffix
.Ar template
.
.Sh DESCRIPTION
Creates a file
.Li u=rw
\-
.Va umask
.Pq directory Li u=rwx No \- Va umask No if Fl d ,
replacing a run of
.Sq Sy X
characters in the
.Ar template ,
and prints its path to the standard output stream.
If no
.Ar template ,
defaults to
.Qq Li tmp.XXXXXXXXXX
and
.Fl t .
The final path is
.Qq Oo Ar directory Ns Oo Pa / Oc Oc Ns Ar template Ns Op Ar suffix .
.Pp
The
.Ar template
must contain at least three consecutive
.So Sy X Sc Ns es
after its final slash (if any).
These are replaced with random alphanumeric characters until a path that doesn't already exist is generated; this behaviour, coupled with the mode, makes
.Nm
suitable for safely generating unique temporary files
.Pq unless Fl u .
.
.Sh OPTIONS
.Bl -tag -compact -width "-p, --tmpdir=directory"
.It Fl d , -directory
Create a directory instead of a regular file.
.It Fl q , -quiet
Suppress error output. The
.Sx EXIT STATUS
is unaffected.
.It Fl u , -dry-run
Don't create anything, just output a matching path that doesn't exist.
Do
.Em not
use this \(em there's no guarantee the file can be created in the first place, and the file may be used or subverted by a third party before the
.Nm
output is used.
.It Fl t , -tmpdir
If the
.Ev TMPDIR
environment variable is set, equivalent to
.Fl p Ev TMPDIR ,
otherwise to
.Fl p Pa /tmp .
.It Fl p , -tmpdir Ns = Ns Ar directory
Prepend
.Ar directory Ns Op Pa /
to the
.Ar template .
.It Fl s , -suffix Ns = Ns Ar suffix
Append
.Ar suffix
to the
.Ar template
.Em after
parsing it: no amount of
.So Sy X Sc Ns es
will interfere with the
.Ar template
pattern.
.El
.
.Sh ENVIRONMENT
.Bl -tag -compact -width "TMPDIR"
.It Ev TMPDIR
Used if valid and
.Fl L .
.El
.
.Sh EXIT STATUS
.Sy 1
if
.Fl u
and
.Xr stat 2
failed for a reason other than
.Er ENOENT ,
otherwise if
.Xr open 2 Ns / Ns Xr mkdir 2
failed for a reason other than
.Er EEXIST .
In case of a write error, created files are removed.
.
.Sh EXAMPLES
.Bd -literal -compact
.Li $ Nm
/tmp/tmp.K2EHQCo6LG
.Li $ Nm Fl p Pa .cache
\&.cache/tmp.Vp2q7gVUX5

.Li $ Nm Ar inXXX.jpeg
inpBH.jpeg
.Li $ TMPDIR=~/.cache Nm Fl t Ar inXXX.jpeg
/home/cicada/.cache/inq8e.jpeg
.Ed
.Pp
Or, as part of a script:
.Bd -literal -compact
#!/bin/sh
.Li tmpfile="$( Ns Nm Ns )" || exit
echo "Program output." > "$tmpfile"
.Ed
.Pp
.Bd -literal -compact
#!/bin/sh
.Li exec > \&"$( Ns Nm Ar /var/tmp/cleanup-XXXXXXXX.log Ns )" || exit
.Ed
.
.Sh SEE ALSO
.Xr open 2 ,
.Xr mkdir 2 ,
.Xr mkstemp 3
.
.Sh STANDARDS
Compatible with the GNU system; short
.Fl s
and allowing
.Fl s
when
.Ar template
doesn't end with an
.Sq Sy X
are extensions.
.Pp
.Ox
supports
.Fl dqutp
with
.Ev TMPDIR
overriding
.Fl p ,
its
.Ar template
must end with the
.So Sy X Sc Ns es ,
and the minimum amount thereof is
.Sy 6 .
.Nx
allows any amount of
.So Sy X Sc Ns es ,
.Fl p
overrides
.Ev TMPDIR ,
and supports any amount of
.Ar template Ns s ,
and its
.Fl t Ar prefix
injects a
.Qq Ar prefix Ns Li .XXXXXXXX
.Ar template .
.Fx
is as
.Nx
but drops
.Fl p .
All three of those implementations first create the file with
.Fl u ,
then remove it.
An
.Ox Ns -compatible
.Nm
also exists in Illumos.
.Pp
The only
.No strictly- Ns St -p1003.1-2008 Ns -compliant
way to emulate
.Nm
is to use
.Dl Nm echo Li 'mkstemp(/tmp/prefixXXXXXX)' \&| Nm m4
.
.Sh HISTORY
Originates from
.Ox 2.1
as
.Xr mktemp 1 :
.Nm mktemp No \- make temporary file name (unique)
Supporting
.Fl dqu ,
defaulting to a
.Li tmp.XXXXXXXXXX
.Ar template .
.Pp
.\" https://github.com/freebsd/freebsd-src/commit/4ca332d5bff412fef921f5440d01b94c5eb301d6#diff-d14154fdd31feb0e3a11b1a4e305777adbdb1aa369365504f1d9a4ea241dfa40
A compatible implementation appeared in
.Fx 3.0 ,
adding
.Fl t Ar prefix ,
which constructs a
.Ar template
in the form
.Qq Li ${TMPDIR:-/tmp} Ns Oo Pa / Oc Ns Ar prefix Ns Li .XXXXXXXX
and allows any amount of
.Ar template Ns s
afterward, with a default
.Li mktemp
.Ar prefix .
This was imported into
.Nx 1.5 .
.Pp
.Ox 3.0
adds
.Fl tp ,
as present-day.
.Pp
.Nx 6.1
adds
.Fl p Ar tmpdir ,
with the highest priority.

M man/nohup.1 => man/nohup.1 +4 -2
@@ 38,8 38,10 @@ if the standard output stream is a terminal, appending it to
or, should that fail,
.Pa $HOME/nohup.out
.Pq provided the Ev HOME No environment variable is set ,
creating the file as read-write by user
.Pq Dv 600
creating the file as
.Li u=rw
\-
.Va umask
if necessary,
.
.It

M man/pwd.1 => man/pwd.1 +1 -1
@@ 34,7 34,7 @@ Just fully canonicalise the current working directory.
.
.Sh ENVIRONMENT
.Bl -tag -compact -width "PWD"
.It Dv PWD
.It Ev PWD
Used if valid and
.Fl L .
.El

M man/uname.1 => man/uname.1 +2 -1
@@ 65,7 65,8 @@ sysctl on
.Nx , Fx , and Dx , the
.Dv MACHINE_ARCH
macro on
.Ox , and
.Ox ,
and
.Qq unknown
elsewhere
.Pc .

A tests/mktemp => tests/mktemp +70 -0
@@ 0,0 1,70 @@
#!/bin/sh
# SPDX-License-Identifier: 0BSD

tmpdir="$(mktemp -dt "mktemp.XXXXXXXXXX")/"
mktemp="${CMDDIR}mktemp"
unset TMPDIR

tmp="$("$mktemp" 2>&3)"
[ "$tmp" = "/tmp/tmp.XXXXXXXXXX" ]                       && echo "mktemp: no args: unchanged" >&3
expr "$tmp" : '/tmp/tmp\...........$' >/dev/null         || echo "mktemp: no args: mangled" >&3
[ "$(ls -ld "$tmp" | awk '{print $1}')" = "-rw-------" ] || echo "mktemp: no args: wrong mode" >&3
rm "$tmp" 2>&3

tmp="$("$mktemp" -d 2>&3)"
[ "$tmp" = "/tmp/tmp.XXXXXXXXXX" ]                       && echo "mktemp: -d: unchanged" >&3
expr "$tmp" : '/tmp/tmp\...........$' >/dev/null         || echo "mktemp: -d: mangled" >&3
[ "$(ls -ld "$tmp" | awk '{print $1}')" = "drwx------" ] || echo "mktemp: -d: wrong mode" >&3
rmdir "$tmp" 2>&3

tmp="$("$mktemp" -t +XXX+ 2>&3)"
[ "$tmp" = "/tmp/+XXX+" ]                                && echo "mktemp: -t +XXX+: unchanged" >&3
expr "$tmp" : '/tmp/+...+$' >/dev/null                   || echo "mktemp: -t +XXX+: mangled" >&3
[ "$(ls -ld "$tmp" | awk '{print $1}')" = "-rw-------" ] || echo "mktemp: -t +XXX+: wrong mode" >&3
rm "$tmp" 2>&3

tmp="$("$mktemp" -ts +XXX+ +XXXXXX+XXX+ 2>&3)"
[ "$tmp" = "/tmp/+XXXXXX+XXX++XXX+" ]                    && echo "mktemp: -ts +XXX+ +XXXXXX+XXX+: unchanged" >&3
expr "$tmp" : '/tmp/+XXXXXX+...++XXX+$' >/dev/null       || echo "mktemp: -ts +XXX+ +XXXXXX+XXX+: mangled" >&3
[ "$(ls -ld "$tmp" | awk '{print $1}')" = "-rw-------" ] || echo "mktemp: -ts +XXX+ +XXXXXX+XXX+: wrong mode" >&3
rm "$tmp" 2>&3

TMPDIR="${tmpdir}"
export TMPDIR

tmp="$("$mktemp" -ts +XXX+ +XXXXXX+XXX+ 2>&3)"
[ "$tmp" = "${tmpdir}+XXXXXX+XXX++XXX+" ]                && echo "mktemp: TMPDIR -ts +XXX+ +XXXXXX+XXX+: unchanged" >&3
expr "$tmp" : "${tmpdir}"'+XXXXXX+...++XXX+$' >/dev/null || echo "mktemp: TMPDIR -ts +XXX+ +XXXXXX+XXX+: mangled" >&3
[ "$(ls -ld "$tmp" | awk '{print $1}')" = "-rw-------" ] || echo "mktemp: TMPDIR -ts +XXX+ +XXXXXX+XXX+: wrong mode" >&3
rm "$tmp" 2>&3

tmp="$("$mktemp" -p "${tmpdir}///" -ts +XXX+ +XXXXXX+XXX+ 2>&3)"
[ "$tmp" = "${tmpdir}///+XXXXXX+XXX++XXX+" ]                && echo "mktemp: TMPDIR -p TMPDIR/// -ts +XXX+ +XXXXXX+XXX+: unchanged" >&3
expr "$tmp" : "${tmpdir}///"'+XXXXXX+...++XXX+$' >/dev/null || echo "mktemp: TMPDIR -p TMPDIR/// -ts +XXX+ +XXXXXX+XXX+: mangled" >&3
[ "$(ls -ld "$tmp" | awk '{print $1}')" = "-rw-------" ]    || echo "mktemp: TMPDIR -p TMPDIR/// -ts +XXX+ +XXXXXX+XXX+: wrong mode" >&3
rm "$tmp" 2>&3

tmp="$("$mktemp" -p "${tmpdir}///" -uts +XXX+ +XXXXXX+XXX+ 2>&3)"
[ "$tmp" = "${tmpdir}///+XXXXXX+XXX++XXX+" ]                && echo "mktemp: TMPDIR -p TMPDIR/// -uts +XXX+ +XXXXXX+XXX+: unchanged" >&3
expr "$tmp" : "${tmpdir}///"'+XXXXXX+...++XXX+$' >/dev/null || echo "mktemp: TMPDIR -p TMPDIR/// -uts +XXX+ +XXXXXX+XXX+: mangled" >&3
[ -f "$tmp" ]                                               && echo "mktemp: TMPDIR -p TMPDIR/// -uts +XXX+ +XXXXXX+XXX+: created" >&3

errstr="$("$mktemp" -p "${tmpdir}ENOENT" -ts +XXX+ +XXXXXX+XXX+  2>&1 >&3)" && echo "mktemp: -p ENOENT okay?" >&3
[ -n "$errstr" ] || echo "mktemp: -p ENOENT: empty stderr" >&3

"$mktemp" -p "${tmpdir}ENOENT" -qts +XXX+ +XXXXXX+XXX+ >&3 2>&3 && echo "mktemp: -qp ENOENT okay?" >&3

tmp="$("$mktemp" -p "${tmpdir}ENOENT" -uts +XXX+ +XXXXXX+XXX+ 2>&3)" || echo "mktemp: -up ENOENT failed" >&3
[ -f "$tmp" ]                                                        && echo "mktemp: -up ENOENT: created" >&3


if [ -w '/dev/full' ]; then
  mkdir "${tmpdir}empty"
  errstr="$("$mktemp" -p "${tmpdir}empty" 2>&1 > /dev/full)" && echo "mktemp: /dev/full okay?" >&3
  [ -n "$errstr" ]                                           || echo "mktemp: stderr empty for /dev/full" >&3
  [ -z "$(ls "${tmpdir}empty")" ]                            || echo "mktemp: /dev/full made something" >&3
else
  echo "mktemp: skipping error testing, /dev/full unavailable" >&2
fi

rm -rf "$tmpdir" 2>&3

M tests/tee => tests/tee +13 -13
@@ 9,16 9,16 @@ data="$(tr -cd '[:alpha:]' < /dev/urandom | dd bs=20w count=1 2>/dev/null)"
[ "$(echo "$data" | "$tee")" = "$data" ] || echo "tee: no files wrong" >&3

[ "$(echo "$data" | "$tee" "${tmpdir}f1")" = "$data" ] || echo "tee: one file wrong" >&3
echo "$data" | cmp "${tmpdir}f1" >&3                   || echo "tee: f1 wrong" >&3
echo "$data" | cmp - "${tmpdir}f1" >&3                 || echo "tee: f1 wrong" >&3

[ "$(echo "$data" | "$tee" -a "${tmpdir}f1" "${tmpdir}f2")" = "$data" ] || echo "tee: two files wrong" >&3
{ echo "$data"; echo "$data"; } | cmp "${tmpdir}f1" >&3                 || echo "tee: -a f1 wrong" >&3
echo "$data"                    | cmp "${tmpdir}f2" >&3                 || echo "tee: -a f2 wrong" >&3
{ echo "$data"; echo "$data"; } | cmp - "${tmpdir}f1" >&3                 || echo "tee: -a f1 wrong" >&3
echo "$data"                    | cmp - "${tmpdir}f2" >&3                 || echo "tee: -a f2 wrong" >&3

[ "$(echo "$data" | "$tee" "${tmpdir}f1" "${tmpdir}f2" "${tmpdir}f3")" = "$data" ] || echo "tee: three files wrong" >&3
echo "$data" | cmp "${tmpdir}f1" >&3                                               || echo "tee: f1 wrong" >&3
echo "$data" | cmp "${tmpdir}f2" >&3                                               || echo "tee: f2 wrong" >&3
echo "$data" | cmp "${tmpdir}f3" >&3                                               || echo "tee: f3 wrong" >&3
echo "$data" | cmp - "${tmpdir}f1" >&3                                             || echo "tee: f1 wrong" >&3
echo "$data" | cmp - "${tmpdir}f2" >&3                                             || echo "tee: f2 wrong" >&3
echo "$data" | cmp - "${tmpdir}f3" >&3                                             || echo "tee: f3 wrong" >&3


mkfifo "${tmpdir}fifo"


@@ 27,13 27,13 @@ mkfifo "${tmpdir}fifo"
# { echo "$data"; sleep 1; echo "$data"; } > "${tmpdir}fifo" &
# kill -INT $pid
# wait
# echo "$data" | cmp "${tmpdir}out" >&3 || echo "tee: SIGINT no -i wrong" >&3
# echo "$data" | cmp - "${tmpdir}out" >&3 || echo "tee: SIGINT no -i wrong" >&3

"$tee" -i > "${tmpdir}out" < "${tmpdir}fifo" & pid=$!
{ echo "$data"; sleep 1; echo "$data"; } > "${tmpdir}fifo" &
kill -INT $pid
wait
{ echo "$data"; echo "$data"; } | cmp "${tmpdir}out" >&3 || echo "tee: SIGINT -i wrong" >&3
{ echo "$data"; echo "$data"; } | cmp - "${tmpdir}out" >&3 || echo "tee: SIGINT -i wrong" >&3


if [ -w '/dev/full' ]; then


@@ 43,14 43,14 @@ if [ -w '/dev/full' ]; then

	errmsg="$(echo "$data" | "$tee" "${tmpdir}f1" /dev/full "${tmpdir}f2" 2>&1 > "${tmpdir}out")" && echo "tee: /dev/full okay?" >&3
	[ -n "$errmsg" ]                                                                              || echo "tee: no message after /dev/full?" >&3
	echo "$data" | cmp "${tmpdir}out" >&3                                                         || echo "tee: 2=/dev/full: out wrong" >&3
	echo "$data" | cmp "${tmpdir}f1" >&3                                                          || echo "tee: 2=/dev/full: f1 wrong" >&3
	echo "$data" | cmp "${tmpdir}f2" >&3                                                          || echo "tee: 2=/dev/full: f2 wrong" >&3
	echo "$data" | cmp - "${tmpdir}out" >&3                                                       || echo "tee: 2=/dev/full: out wrong" >&3
	echo "$data" | cmp - "${tmpdir}f1" >&3                                                        || echo "tee: 2=/dev/full: f1 wrong" >&3
	echo "$data" | cmp - "${tmpdir}f2" >&3                                                        || echo "tee: 2=/dev/full: f2 wrong" >&3

	errmsg="$(echo "$data" | "$tee" -e "${tmpdir}f1" /dev/full "${tmpdir}f2" 2>&1 > "${tmpdir}out")" && echo "tee: /dev/full okay?" >&3
	[ -n "$errmsg" ]                                                                              || echo "tee: no message after /dev/full?" >&3
	echo "$data" | cmp "${tmpdir}out" >&3                                                         || echo "tee: -e 2=/dev/full: out wrong" >&3
	echo "$data" | cmp "${tmpdir}f1" >&3                                                          || echo "tee: -e 2=/dev/full: f1 wrong" >&3
	echo "$data" | cmp - "${tmpdir}out" >&3                                                       || echo "tee: -e 2=/dev/full: out wrong" >&3
	echo "$data" | cmp - "${tmpdir}f1" >&3                                                        || echo "tee: -e 2=/dev/full: f1 wrong" >&3
	[ -s "${tmpdir}f2" ]                                                                          && echo "tee: -e 2=/dev/full: f2 nonempty?" >&3
else
  echo "tee: skipping error testing, /dev/full unavailable" >&2