The core of the problem is that POSIX has that (generally unhelpful) requirement for sh that asynchronous commands must have their stdin redirected to /dev/null unless there's an explicit stdin redirection (well, technically, that implicit /dev/null redirection happens before explicit redirections if any).
See for instance on Linux-based systems:
$ sh -c 'realpath /dev/stdin & wait'
/dev/null
A common work around for the asynchronous command to have its stdin left alone is to do something like:
{ cmd <&3 3<&- & } 3<&0
Where the original stdin is made available on fd 3 in addition to 0 in a command group with 3<&0, and within the command group cmd's stdin, which was reopened on /dev/null because of the & is being redirected back to the oringal stdin through that fd 3 (which we then close as it's no longer needed).
In:
f() (
mkfifo foo
tee foo &
tr -s '[:lower:]' '[:upper:]' <foo
rm foo
)
printf %s\\n bar | f
tee's stdin will be /dev/null, not the reading end of the pipe from printf. Changing it to:
f() (
mkfifo foo
{ tee foo <&3 3<&- & } 3<&0
tr -s '[:lower:]' '[:upper:]' <foo
rm foo
wait
)
printf '%s\n' bar | f
Would address it, but as you found out, so would
f() (
mkfifo foo
tr -s '[:lower:]' '[:upper:]' <foo &
tee foo
rm foo
)
printf %s\\n bar | f
As then tee is not run asynchronously so doesn't have its stdin redirected to /dev/null and tr's stdin is being redirected explicitly, it doesn't matter that it was redirected to /dev/null beforehand.
We also don't need the wait, as tee is the process running synchronously (behind waited for implicitly by the shell) and would normally not terminated before tr as it's waiting for eof on its stdin (and tr's stdout which is the other end of that pipe is only closed upon exit).
You may still want to wait for tr though to retrieve its exit status:
f() (
ret=0
mkfifo foo || exit
tr -s '[:lower:]' '[:upper:]' <foo &
tee foo || ret=$?
rm -f foo
wait "$!" || ret=$?
exit "$ret"
)
printf '%s\n' bar | f
foo won't be removed if the subshell that's the body of the function is killed. You could reduce the window during which the fifo exists and make it even more like an unnamed pipe (where the named pipe is just an ephemeral meeting place for those two processes to establish a pipe), by removing it as soon as it's been opened both in read and write mode by the two processes.
f() (
ret=0
mkfifo foo || exit
{ tee foo <&3 3<&- & } 3<&0
{
# at that point, foo will have been opened here in read-only mode
# on stdin which can only happen if tee has opened it in write-only
# mode already.
rm -f foo
tr -s '[:lower:]' '[:upper:]' || ret=$?
} < foo
wait "$!" || ret=$?
exit "$ret"
)
printf '%s\n' bar | f
external_programinto a detachedtee?