Why does ''cat "${1:-/dev/stdin} | ... &>/dev/null'' work in bash but not dash?

&> is a bashism, you will have to change it to >/dev/null 2>&1 for POSIX shells

dash positioned as POSIX standard. POSIX specified only [n]> redirection. But bash introduces many own features. &> is one of them and means output descriptors (stderr and stdout).

You should read article about bash and dash compatibility.

Maybe you get helpful checkbashisms utility which can helps to find bash-specific instructions in your scripts.

[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'
$ echo | dash -c 'readlink /proc/self/fd/0 | cat & echo DONE'

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'
$ echo | bash -c 'readlink /proc/self/fd/0 | cat & echo DONE'
$ echo | bash -c 'readlink /proc/self/fd/0 & echo DONE'
$ bash -c 'cat | tr a A & echo 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
$ zsh -c 'readlink /proc/self/fd/0 &' </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 &