Shell Redirections

Intro

What is described on this page targets Bash, but most things also work the same way in other shells.

When bash starts, it normally opens three file descriptors. We associated them with the numbers 0, 1 and 2.

  • 0 is the standard input, or STDIN;

  • 1 is the starndard output, or STDOUT;

  • 2 is the standard error, or STDERR;

The numbers are used a lot in the command line and in shell scripts, as we shall see with several examples along this page.

In C, it is simple to send output to both STDOUT and STDERR.

#include <stdio.h>

int main(void) {
  fprintf(stdout, "%s\n", "OK");
  fprintf(stderr, "%s\n", "ERR");
  return 0;
}

In the shell, we use the <, > and & operators in combination with the numbers.

stdout

When print or echo are used to output text, it goes to stdout by default:

$ echo hey
hey

$ printf '%d\n' {1..3}
1
2
3

Same goes for cat. By default, output goes to stdout:

$ cat << EOF
> I'm $(whoami). My shell is $SHELL, version $BASH_VERSION.
> My main editor is $EDITOR, but I love Emacs too!
> EOF
I'm devy. My shell is /bin/bash, version 5.3.15(1)-release.
My main editor is vim, but I love emacs too!

So, it means:

$ cat << EOF
Hello!
EOF

Is actually the same as:

$ cat << EOF >&1
Hello!
EOF

That is, >&1 is the explicit way to say “send the output to STDOUT.”

And by the way, the only position where the redirection operators cannot appear is between << and EOF. The other (possible) positions are valid:

>&1 cat << EOF (OK)
cat >&1 << EOF (OK)
cat << >&1 EOF (ERR)
cat << EOF >&1 (OK)

stderr

By default, errors on a shell session are redirected to stdout. Consider this shell session:

$ ls -1
redir.c
redir.sh*

$ echo $?
0

$ rm ./main.c
rm: cannot remove './main.c': No such file or directory

$ echo $?
1

Observe that the exit status of ls is 0 (zero), which means the command ran successfully. However, rm failed because main.c does exist in that directory, the command exit status is 1 (one), which signifies some sort of failure. Regardless, both the output of ls and the error message from rm are displayed on the terminal.

That said, we can redirect stdout and stderr to different destinations, including the blackwhole /dev/null, or, very commonly, to text files.

We use 1> to redirect output to stdout, and 2> to rediret error output to stderr. Let’s rerun the previous commands and redirect the output:

$ 1> ./out.txt ls -1

$ 2> ./err.txt rm ./main.c

$ cat ./out.txt
out.txt
redir.c
redir.sh*

$ cat ./err.txt
rm: cannot remove './main.c': No such file or directory

Remember that ls runs successfully, so its output goes to stdoug, while rm fails with an exit status of 1 and its error message output goes to stderr. But we set up redirections to send the output to specific text files.

Note that out.txt also includes out.txt itself as a result of ls. How come? That is because redirection is set up before the command actually runs. Bash first creates out.txt and connects stdout to that file, and then runs the command itself. So, by the time ls is run, out.txt already exists (even though it is still empty at that point).

Same for err.txt. Bash will first create the file, point stderr to it, and only then actually execute the rm command.

Consider what happens in this case:

$ ls -1
err.txt
out.txt
redir.c
redir.sh*

$ 2> ./guess.txt rm ./guess.txt

$ echo $?
0

$ ls -1
err.txt
out.txt
redir.c
redir.sh*

First, we don’t have a file called guess.txt. When then setup stderr to point to guess.txt to run rm to send error message output to that file. But, because redirections are setup first, before the command is actually run, guess.txt is created before rm runs. Then, when rm runs, it actually succeeds in removing that file, which is why the exit status of the command is 0 (and not 1 like in the previous examples).