Bash vs Dash – Why ‘cat ${1:-/dev/stdin} | … &>/dev/null’ Works in Bash Only

bashdashio-redirectionpipeshell

Script:

#!/bin/sh
#
# reads stdin/file and copies it to clipboard
# clears it after 30s
#
cat "${1:-/dev/stdin}" | timeout 30 xclip -i -selection clipboard -r -verbose &>/dev/null &

I can see that only stdin does not work (with bash it works on stdin/file).
P.S. verbose is used to make xclip not daemonize.

Best Answer

[this answer is about asynchronous pipelines in scripts; for the deprecated &> bash operator and why you should always use >output 2>&1 instead, refer to obsolete and deprecated syntax]

#! /bin/sh
cat "${1:-/dev/stdin}" | ... &

Here you have a pipeline running asynchronously (because terminated by &), started from a script, ie is from a shell with the job control disabled.

According to the standard:

command1 & [command2 & ... ]

The standard input for an asynchronous list, before any explicit redirections are performed, shall be considered to be assigned to a file that has the same properties as /dev/null.

The problem is that dash, ksh, mksh, yash, etc intepret "asynchronous list" as any command, including a pipeline, and will redirect the stdin of the first command from /dev/null:

$ echo foo | dash -c 'cat | tr fo FO & echo DONE'
DONE
$ echo | dash -c 'readlink /proc/self/fd/0 | cat & echo DONE'
DONE
/dev/null

But bash will only interpret it as "simple command" and will only redirect its stdin from /dev/null when it's not part of a pipeline:

$ echo foo | bash -c 'cat | tr fo FO & echo DONE'
DONE
FOO
$ echo | bash -c 'readlink /proc/self/fd/0 | cat & echo DONE'
DONE
pipe:[69872]
$ echo | bash -c 'readlink /proc/self/fd/0 & echo DONE'
DONE
/dev/null
$ bash -c 'cat | tr a A & echo DONE'
DONE
cat: -: Input/output error

zsh will only redirect it from /dev/null when the original stdin is a tty, not when it's other kind of file:

$ zsh -c 'readlink /proc/self/fd/0 &' </dev/tty
/dev/null
$ zsh -c 'readlink /proc/self/fd/0 &' </dev/zero
/dev/zero

A workaround which works in all shells is to duplicate the stdin into another file descriptor, and redirect the stdin of the first command from it:

#! /bin/sh
exec 3<"${1:-/dev/stdin}"
cat <&3 | timeout 30 xclip -i -selection clipboard -verbose -r >/dev/null 2>&1 &