Actually it is a bit tricky how /dev/stdout works in practice, on Linux at least. At first glance I thought /dev/stdout would be a drop-in replacement for -, for utilities not supporting - as a way to specify stdout. However, these are not identical:
for i in $(seq 10)
do
echo test | dd of=/dev/stdout >> /tmp/ten_times_dev_stdout
echo test | dd of=/dev/stdout | cat >> /tmp/ten_times_dev_stdout_cat
echo test | dd >> /tmp/ten_times_stdout
done
wc -l /tmp/ten_times_*
As it outputs (ignoring the dd reporting)
1 /tmp/ten_times_dev_stdout
10 /tmp/ten_times_dev_stdout_cat
10 /tmp/ten_times_stdout
Where one would naively expect to see the number 10 three times. So it breaks >> in a very obscure way, but it can be fixed by piping through cat.
(As observed on Linux systems, e.g. using GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu) and dd (GNU coreutils) 8.32 on Linux 5.15.0-102-generic #112-Ubuntu SMP Tue Mar 5 16:50:32 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux, and also using zsh and tcsh, but not on MacOS using GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin23) and Darwin 23.3.0 Darwin Kernel Version 23.3.0)
update
This boils down to the system call open("/dev/stdout",O_TRUNC) where the O_TRUNC asks to truncate /dev/stdout, which is a symlink to /proc/self/fd/1, which is a symlink to the file the output is redirected to. This is due to a call to dup2() by the shell. These magic symlinks seem nice but introduce strange bugs.
update 2
For clarity, on MacOS the above outputs:
10 /tmp/ten_times_dev_stdout
10 /tmp/ten_times_dev_stdout_cat
10 /tmp/ten_times_stdout
As expected for a shell command explicitly asking to append using >>.
Some more proof to convince downvoters of this strange behavior:
cat << EOF > do_nothing.c
#include <fcntl.h>
int main() { }
EOF
cat << EOF > magic_truncate.c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
char linkbuf[256];
ssize_t len = readlink("/dev/stdout",linkbuf,sizeof(linkbuf));
if ( len >= 0 ) {
linkbuf[len] = 0;
fprintf(stderr,"opening /dev/stdout -> %s",linkbuf);
}
len = readlink(linkbuf,linkbuf,sizeof(linkbuf));
if ( len >= 0 ) {
linkbuf[len] = 0;
fprintf(stderr," -> %s",linkbuf);
}
int fd = open("/dev/stdout",O_TRUNC|O_WRONLY);
fprintf(stderr,", fd=%d\n",fd);
}
EOF
gcc do_nothing.c -o do_nothing
gcc magic_truncate.c -o magic_truncate
echo test > /tmp/test
wc -l /tmp/test
./do_nothing >> /tmp/test
wc -l /tmp/test
./magic_truncate >> /tmp/test
wc -l /tmp/test
This outputs:
1 /tmp/test
1 /tmp/test
opening /dev/stdout -> /proc/self/fd/1 -> /tmp/test, fd=3
0 /tmp/test
on Linux, and:
1 /tmp/test
1 /tmp/test
opening /dev/stdout -> fd/1, fd=3
1 /tmp/test
on MacOS. Please just try it before downvoting, and consider that it is highly unexpected for >> to truncate a file, probably in violation of POSIX standards. This is due to the way /dev/stdout and /proc/self/fd/1 are implemented on Linux, using symlinks rather than as a stream that can be opened like a file. I suspect that other issues could arise from this symlink based implementation, e.g. considering file permissions in conjunction with processes that drop their privileges.