Surprising behaviour when reading from a named pipe and standard input
I came across this behavioural difference between dash and sh(bash), bash and zsh.
When attempting to read from a named pipe and stdin at the same time in a backgrounded process I appear to be losing stdin:
#!/bin/dash --
f() {
mkfifo pipe
cat pipe - <&0 &
echo hello > pipe
rm -f pipe
wait
}
echo hi | fThis outputs (busybox sh likewise):
hello
With sh(bash), bash and zsh I get (and expect):
hello
hi
At first I thought it might be a difference in how dash implements set -m due to how jobs without it set have their stdin opened as /dev/null but adding set -m does not appear to help.
Can you help me understand what is happening and why dash differs?
---
I would like to use this technique with age to read identities from a pipe as the tool doesn't offer another way, read code below:
decrypt() {
# XXX https://github.com/FiloSottile/age/discussions/685
tmpdir=${TMPDIR:-/tmp}/curator-$$
pipe=$tmpdir/s
defer rm -rf -- "$tmpdir"
nofail mkdir -m 0700 -- "$tmpdir"
# XXX Using a pipe limits the size of the identity to around 1MiB on Linux.
# cf. /proc/sys/fs/pipe-max-size
nofail mkfifo -m 0600 -- "$pipe"
# nb. stdin needs to be specified as without job control (set -m) the
# command will have its stdin set to /dev/null.
age -i "$pipe" -d <&0 &
exec 9>"$pipe"
rm -f -- "$pipe"
key_request private >&9
exec 9>&-
wait
}Quoted 6 lines · Earnestly
I came across this behavioural difference between dash and sh(bash), bash and zsh.
When attempting to read from a named pipe and stdin at the same time in a backgrounded process I appear to be losing stdin:
Quoted 6 lines
#!/bin/dash --
f() {
mkfifo pipe
cat pipe - <&0 &In both bash and dash, backgound commands have an implicit </dev/null redirection unless an explicit redirection is provided, but they disagree on exactly how that works.
In bash, <&0 suppresses the implicit </dev/null, meaning the outer environment's stdin gets used.
In dash, internally, <&0 is a no-op because redirecting stdin to itself shouldn't change anything. Because it is a no-op, it does not suppress the implicit </dev/null.
In dash, effectively, it is as if <&0 overrides rather than suppresses the implicit </dev/null: the implict </dev/null is performed, and then <&0 overrides that but at that point fd 0 is already /dev/null, so after that, it is still /dev/null.
I have mixed feelings on which behaviour makes more sense, but for better or worse this is something that portable shell scripts just need to avoid. You can make this work by temporarily duplicating stdin to a different fd and then back:
{ cat pipe - <&3 & } 3<&0
Cheers,
Harald van Dijk
Quoted 2 lines · Harald van Dijk
In bash, <&0 suppresses the implicit <
/dev/null, meaning the outer environment's stdin gets used.
I hoped that dash would do the same, when faced with an explicit use of <&0, which I try to base on my reading of the standard:
Quoted 6 lines
If, and only if, job control is disabled, the standard input for the subshell in which an asynchronous AND-OR list is executed shall initially be assigned to an open file description that behaves as if
/dev/nullhad been opened for reading only. This initial assignment shall be overridden by any explicit redirection of standard input within the AND-OR list.
With emphasis on the last sentence.
You write that dash does ultimately interpret this correctly but applies it too late, after fd 0 is already /dev/null.
Perhaps because this is not merely a no-op (as 0<&0 is) and that dash does attempt to do the right thing (too late), it suggests maybe dash should change behaviour.
However you're right about portable shell scripts. I have to accept the minimal interpretation and your solution is very reasonable, perhaps even more explicit as well.
Many thanks for your explanation.