Lesson: Streams, Redirection & Exit Codes

What you'll learn

By the end you can control where a script's input comes from and where its output and errors go — the foundation of logging and error handling.

The lesson

Every command on Linux is wired up to three streams — think of them as three pipes attached to the program. You will use these constantly in scripts to capture results, separate errors, and chain tools together.

You will run all of this on the Jumpbox (10.100.100.254, user ubuntu).

1. The three standard streams

                +---------------------+
   stdin (0) -->|     your command    |--> stdout (1)  normal output
                |                     |--> stderr (2)  error messages
                +---------------------+

Keeping normal output and errors on separate streams is deliberate: it lets you save results to a file while still seeing errors, or vice versa.

2. Redirecting output: >, >>, 2>, &>

A redirection changes where a stream goes. Send stdout to a file:

echo "line one" > out.txt     # > OVERWRITES the file (creates if missing)
echo "line two" >> out.txt    # >> APPENDS to the file
cat out.txt
# line one
# line two

> clobbers (replaces) the file every time — be careful. >> adds to the end.

Errors travel on stderr, so plain > does not capture them. Redirect stderr with 2>:

ls /does/not/exist 2> errors.txt    # error goes to the file, not the screen

Common combinations:

command > out.txt 2> err.txt    # stdout and stderr to SEPARATE files
command > all.txt 2>&1          # stderr follows stdout into the same file
command &> all.txt              # shorthand for the same thing (Bash)
command > /dev/null 2>&1        # discard everything (/dev/null is a black hole)

The order in 2>&1 matters: it means "make descriptor 2 point wherever descriptor 1 currently points," so it must come after you redirect stdout.

3. Pipes: |

A pipe connects the stdout of one command to the stdin of the next, so you can build a processing chain:

ls -l /etc | grep conf | wc -l

This lists /etc, keeps only lines containing conf, then counts them. Data flows left to right:

  ls -l /etc  --stdout-->  grep conf  --stdout-->  wc -l  --> screen

Pipes only connect stdout, not stderr. Each small tool does one job well and you compose them — the Unix philosophy.

4. Exit codes and $?

When a command finishes it returns an exit code: a number from 0 to 255. By convention 0 means success and anything non-zero means failure. This is how a script knows whether the previous step worked.

The special variable $? holds the exit code of the last command:

ls /etc > /dev/null
echo "$?"        # 0  (success)

ls /nope 2> /dev/null
echo "$?"        # 2  (failure)

You will use exit codes in the next lesson to make decisions. Your own scripts should set their exit code too, with exit:

#!/usr/bin/env bash
if [[ -f /etc/hostname ]]; then
  echo "found it"
  exit 0          # success
else
  echo "missing!" >&2   # send our error to stderr
  exit 1          # failure
fi

Note >&2 — that redirects our echo to stderr, the correct stream for error messages, so callers can separate them from real output.

5. Reading input with read

read pulls a line from stdin into a variable. Combined with -p it prompts the user:

#!/usr/bin/env bash
read -rp "What is your name? " name
echo "Hello, $name"

You can read several values at once; read splits on whitespace:

read -rp "Enter first and last name: " first last
echo "First: $first   Last: $last"

Because read reads from stdin, it also works with pipes and files:

while read -r line; do
  echo "got: $line"
done < out.txt

That < out.txt is input redirection — it feeds the file into the loop's stdin instead of the keyboard.

6. Here-documents

A here-document (here-doc) feeds a block of multi-line text into a command's stdin without a separate file. You pick a marker word (commonly EOF):

cat <<EOF > config.txt
host = jumpbox
user = ubuntu
date = $(date +%F)
EOF

Everything between <<EOF and the closing EOF becomes input to cat, which here writes it to config.txt. Variables and $( ) are expanded inside, as shown. If you quote the marker — <<'EOF' — expansion is turned off and the text is taken literally, which is useful when writing scripts that contain $ signs.

grep "user" <<< "user=ubuntu"

7. Putting it together

#!/usr/bin/env bash
# Save a directory listing; log any error separately.
target="/etc"
if ls -l "$target" > listing.txt 2> errors.log; then
  echo "Listing saved ($(wc -l < listing.txt) lines)"
else
  echo "ls failed, see errors.log" >&2
  exit 1
fi

This captures normal output to one file, errors to another, checks the exit code, and reports cleanly on the right streams — exactly the discipline real scripts need.

Dig deeper

Search terms

Check yourself

  1. Name the three standard streams and their file descriptor numbers.
  2. What is the difference between > file and >> file?
  3. What does command > out.txt 2>&1 do, and why must 2>&1 come last?
  4. By convention, what exit code means success, and how do you read the last command's exit code?
  5. Write a here-doc that writes two lines to a file called notes.txt.

Revision #5
Created 2026-05-15 14:46:00 UTC by Eslam Shapsough
Updated 2026-05-15 14:50:00 UTC by Eslam Shapsough