echo or print /dev/stdin /dev/stdout /dev/stderr

stdin, stdout, and stderr are streams attached to file descriptors 0, 1, and 2 respectively of a process.

At the prompt of an interactive shell in a terminal or terminal emulator, all those 3 file descriptors would refer to the same open file description which would have been obtained by opening a terminal or pseudo-terminal device file (something like /dev/pts/0) in read+write mode.

If from that interactive shell, you start your script without using any redirection, your script will inherit those file descriptors.

On Linux, /dev/stdin, /dev/stdout, /dev/stderr are symbolic links to /proc/self/fd/0, /proc/self/fd/1, /proc/self/fd/2 respectively, themselves special symlinks to the actual file that is open on those file descriptors.

They are not stdin, stdout, stderr, they are special files that identify what files stdin, stdout, stderr go to (note that it's different in other systems than Linux that have those special files).

reading something from stdin means reading from file descriptor 0 (which will point somewhere within the file referenced by /dev/stdin).

But in $(</dev/stdin), the shell is not reading from stdin, it opens a new file descriptor for reading on the same file as the one open on stdin (so reading from the start of the file, not where stdin currently points to).

Except in the special case of terminal devices open in read+write mode, stdout and stderr are usually not open for reading. They are meant to be streams that you write to. So reading from the file descriptor 1 will generally not work. On Linux, opening /dev/stdout or /dev/stderr for reading (as in $(</dev/stdout)) would work and would let you read from the file where stdout goes to (and if stdout was a pipe, that would read from the other end of the pipe, and if it was a socket, it would fail as you can't open a socket).

In our case of the script run without redirection at the prompt of an interactive shell in a terminal, all of /dev/stdin, /dev/stdout and /dev/stderr will be that /dev/pts/x terminal device file.

Reading from those special files returns what is sent by the terminal (what you type on the keyboard). Writing to them will send the text to the terminal (for display).

echo $(</dev/stdin)
echo $(</dev/stderr)

will be the same. To expand $(</dev/stdin), the shell will open that /dev/pts/0 and read what you type until you press ^D on an empty line. They will then pass the expansion (what you typed stripped of the trailing newlines and subject to split+glob) to echo which will then output it on stdout (for display).

However in:

echo $(</dev/stdout)

in bash (and bash only), it's important to realise that inside $(...), stdout has been redirected. It is now a pipe. In the case of bash, a child shell process is reading the content of the file (here /dev/stdout) and writing it to the pipe, while the parent reads from the other end to make up the expansion.

In this case when that child bash process opens /dev/stdout, it is actually opening the reading end of the pipe. Nothing will ever come from that, it's a deadlock situation.

If you wanted to read from the file pointed-to by the scripts stdout, you'd work around it with:

 { echo content of file on stdout: "$(</dev/fd/3)"; } 3<&1

That would duplicate the fd 1 onto the fd 3, so /dev/fd/3 would point to the same file as /dev/stdout.

With a script like:

#! /bin/bash -
printf 'content of file on stdin: %s\n' "$(</dev/stdin)"
{ printf 'content of file on stdout: %s\n' "$(</dev/fd/3)"; } 3<&1
printf 'content of file on stderr: %s\n' "$(</dev/stderr)"

When run as:

echo bar > err
echo foo | myscript > out 2>> err

You'd see in out afterwards:

content of file on stdin: foo
content of file on stdout: content of file on stdin: foo
content of file on stderr: bar

If as opposed to reading from /dev/stdin, /dev/stdout, /dev/stderr, you wanted to read from stdin, stdout and stderr (which would make even less sense), you'd do:

#! /bin/sh -
printf 'what I read from stdin: %s\n' "$(cat)"
{ printf 'what I read from stdout: %s\n' "$(cat <&3)"; } 3<&1
printf 'what I read from stderr: %s\n' "$(cat <&2)"

If you started that second script again as:

echo bar > err
echo foo | myscript > out 2>> err

You'd see in out:

what I read from stdin: foo
what I read from stdout:
what I read from stderr:

and in err:

bar
cat: -: Bad file descriptor
cat: -: Bad file descriptor

For stdout and stderr, cat fails because the file descriptors were open for writing only, not reading, the the expansion of $(cat <&3) and $(cat <&2) is empty.

If you called it as:

echo out > out
echo err > err
echo foo | myscript 1<> out 2<> err

(where <> opens in read+write mode without truncation), you'd see in out:

what I read from stdin: foo
what I read from stdout:
what I read from stderr: err

and in err:

err

You'll notice that nothing was read from stdout, because the previous printf had overwritten the content of out with what I read from stdin: foo\n and left the stdout position within that file just after. If you had primed out with some larger text, like:

echo 'This is longer than "what I read from stdin": foo' > out

Then you'd get in out:

what I read from stdin: foo
read from stdin": foo
what I read from stdout: read from stdin": foo
what I read from stderr: err

See how the $(cat <&3) has read what was left after the first printf and doing so also moved the stdout position past it so that the next printf outputs what was read after.


stdout and stderr are outputs, you do not read from them you can only write to them. For example:

echo "this is stdout" >/dev/stdout
echo "this is stderr" >/dev/stderr

programs write to stdout by default so the first one is equivalent to

echo "this is stdout"

and you can redirect stderr in other ways such as

echo "this is stderr" 1>&2