Redirection and Pipes · about 40 min

File descriptors and output redirection

Why this matters

So far, output has gone to your screen. Real work needs it elsewhere: a report saved to a file, a log appended to every night, the contents of a file fed into a program, errors kept apart from results. The shell can connect any program's input and output to files, and the program does not need to know or care. This lesson explains the mechanism, file descriptors, because once you understand it, all the redirection syntax (including the confusing 2>&1 in the next lesson) is just bookkeeping.

Files are opened, then used by number

When a program wants to use a file, it asks the kernel to open it (the open system call). The kernel does the work of finding the file on disk and checking permissions, and hands back a small integer, a file descriptor (fd). From then on the program says "write these bytes to fd 3", "read from fd 3", "close fd 3". Each process has its own file descriptor table:

  process: ls (PID 214)
  ┌────┬─────────────────────────────┐
  │ fd │ refers to                   │
  ├────┼─────────────────────────────┤
  │ 0  │ /dev/pts/0  (the terminal)  │  standard input   (stdin)
  │ 1  │ /dev/pts/0  (the terminal)  │  standard output  (stdout)
  │ 2  │ /dev/pts/0  (the terminal)  │  standard error   (stderr)
  │ 3  │ /etc  (opened by ls itself) │
  └────┴─────────────────────────────┘

By a convention every Unix program follows, the first three are always there when a program starts:

A program does not open these; it inherits them. Remember from module 1 that a forked child gets copies of its parent's open files, and that exec replaces the program but keeps the file descriptor table. Your interactive bash has fds 0, 1 and 2 pointing at the terminal, so every program it starts inherits a table pointing at the terminal. That is the only reason output "appears on the screen".

You can look at a real table. Linux exposes each process's descriptors under /proc:

$ ls -l /proc/self/fd
lrwx------ 1 learner learner 64 Nov  1 14:20 0 -> /dev/pts/0
lrwx------ 1 learner learner 64 Nov  1 14:20 1 -> /dev/pts/0
lrwx------ 1 learner learner 64 Nov  1 14:20 2 -> /dev/pts/0
lr-x------ 1 learner learner 64 Nov  1 14:20 3 -> /proc/214/fd

/proc/self always means "the process looking at it", here the ls process. Its fd 3 is the directory it opened to produce this very listing.

Redirection: changing the table before exec

Redirection means: in the child, after fork and before exec, bash rearranges the file descriptor table. Then the program starts with its fds already pointing somewhere else.

  bash: ls > list.txt
    fork ──▶ child bash
               open("list.txt", write|create|truncate) → gets fd 3
               dup2(3, 1)      make fd 1 refer to the same open file as fd 3
               close(3)
               execve("/usr/bin/ls", ["ls"])
             ls writes to fd 1 as usual → bytes land in list.txt

Two consequences:

  1. > list.txt is not an argument. bash removes it from the word list; ls receives no arguments at all. You can even put it first: > list.txt ls works identically.
  2. The file is opened before the program runs. If bash cannot open it (no permission, missing directory), the command is never started.
$ ls > /nope/list.txt
bash: /nope/list.txt: No such file or directory

Notice the message comes from bash, not ls: ls never ran.

Open the shell for this lesson and try ls -l /proc/self/fd > fds.txt, then cat fds.txt. You will see fd 1 pointing at /work/fds.txt: the program that wrote the listing was itself writing into the file.

The operators

> write to a file

$ echo hello > greeting.txt
$ cat greeting.txt
hello
$ echo goodbye > greeting.txt
$ cat greeting.txt
goodbye

> opens the file with truncate: if it exists, it is emptied to zero bytes first; if it does not exist, it is created. The old contents are gone, with no warning and no undo.

>> append

$ echo "line one" >> journal.txt
$ echo "line two" >> journal.txt
$ cat journal.txt
line one
line two

>> opens the file in append mode: every write goes to the current end of the file. It creates the file if needed. Logs are written this way. Append mode is enforced by the kernel per write, so even two processes appending at once will not overwrite each other's lines (though short lines may interleave).

< read from a file

< connects a file to standard input (fd 0):

$ wc -l names.txt
11 names.txt
$ wc -l < names.txt
11

Same count, different output. In the first form wc received the argument names.txt and opened the file itself, so it knows the name and prints it. In the second, bash opened the file and wc just read fd 0; it has no idea a file called names.txt exists. This is a practical trick for getting "just the number".

Many programs read standard input when you give them no file name: cat, sort, wc, head, grep. That is why cat alone waits for typing: its fd 0 is the terminal. (Ctrl-D ends the input.)

Combining them

Redirections can be combined and appear anywhere in the command:

$ sort < names.txt > sorted.txt
$ head -n 3 sorted.txt
Ada
Barbara
Dennis

Creating an empty file, and emptying one

A redirection with no command still opens (and so creates or truncates) the file:

$ > empty.txt
$ wc -c empty.txt
0 empty.txt

This is a common way to empty a log file without deleting it (programs that have it open keep writing to the same file).

Protecting files: noclobber

Because > silently destroys, bash has an optional safety switch:

$ set -o noclobber
$ echo oops > greeting.txt
bash: greeting.txt: cannot overwrite existing file
$ echo yes >| greeting.txt
$ set +o noclobber

With noclobber on, > refuses to overwrite an existing file; >| overrides it deliberately. Some people enable it in their interactive shell.

Common mistakes

Exercises

The quiz checks the mechanism. The tasks use the shell; the code exercise runs your script twice in the same directory, which is exactly the situation where > and >> differ.

Recap

Going further: run ls -l /proc/$$/fd to see your shell's own table, then exec 3> extra.txt (this opens fd 3 in the shell itself), echo hi >&3, ls -l /proc/$$/fd again, and finally exec 3>&- to close it. You will meet exec with redirections again in module 13.

Exercises

Exercise 1 · quiz

Who opens the file?

You run ls /etc > list.txt. Which process opens list.txt for writing?

Answer
  • The forked child of bash, after fork and before exec, so ls starts with file descriptor 1 already pointing at list.txt.

bash removes > list.txt from the command line (ls never sees it), forks, and in the child opens the file and installs it as file descriptor 1. Then it execs ls. ls simply writes to fd 1 as always; it has no idea that fd 1 is now a file rather than the terminal. The parent bash is not involved in the copying at all. This is why redirection works the same for every program, even ones written decades before you were born.

Exercise 2 · task in a shell

Save and append

Open the shell for this lesson. /work/inbox contains a few files.

  1. Save the output of ls inbox into /work/listing.txt.
  2. Then append a final line --- end --- to the same file, without losing what is already in it.

The result should be the file names (one per line) followed by --- end ---.

Hint 1

> creates or empties a file; >> opens it for appending.

Hint 2

ls inbox > listing.txt then echo '--- end ---' >> listing.txt.

Reference solution
ls inbox > listing.txt
echo '--- end ---' >> listing.txt

Exercise 3 · task in a shell

Just the number

/work/names.txt contains one name per line. Save the number of lines in it into /work/count.txt. The file must contain only the number, for example 12, with no file name after it.

Try wc -l names.txt first and look at its output. Then think about how to give wc the data without telling it a file name.

Hint 1

wc -l names.txt prints 11 names.txt. wc prints the name because it opened the file itself.

Hint 2

< connects a file to standard input: wc -l < names.txt > count.txt.

Reference solution
wc -l < names.txt > count.txt

Exercise 4 · write code

A log that starts fresh

Write a script that creates a file log.txt whose content is exactly two lines:

first
second

and then prints the file with cat log.txt.

The checker runs the script twice in the same directory. Both runs must print just those two lines, so the second run must not pile up more lines in the old file.

File: script.sh

Starting point

echo first >> log.txt
echo second >> log.txt
cat log.txt

Example

expected output

first
second

Example

expected output

first
second
Hint 1

Run the starter twice in your head: the second run appends to the file left by the first.

Hint 2

The first write should truncate the file (>), the second should append (>>).

Reference solution
echo first > log.txt
echo second >> log.txt
cat log.txt