Skip to main content
    Courses/Command Line/Pipes and Redirection

    Lesson 5 • Beginner

    Pipes and Redirection 🔗

    By the end of this lesson you'll be able to chain small commands into one powerful pipeline, send output and errors exactly where you want, and combine commands so they run in the right order — the skills that turn the terminal from a toy into a tool.

    What You'll Learn

    • Understand the three streams: stdin, stdout, and stderr
    • Chain commands with the pipe | (list | filter | count)
    • Redirect output to files with > (overwrite) and >> (append)
    • Capture errors with 2> and merge streams with 2>&1
    • Discard output with /dev/null and split it with tee
    • Sequence commands with &&, ||, and ;

    1. The Three Streams

    Before pipes make sense, you need to know that every command has three channels. stdin (channel 0) is where input arrives. stdout (channel 1) is where normal results go. stderr (channel 2) is a separate channel just for error messages. On screen, stdout and stderr look the same, but keeping them apart is what lets you save real results while still seeing warnings — and it's the reason a later trick (2>&1) exists at all.

    Worked example: stdout vs stderr
    # Every command talks through THREE streams (numbered channels):
    #   stdin  (0) — where INPUT comes from   (your keyboard by default)
    #   stdout (1) — where normal OUTPUT goes (your screen by default)
    #   stderr (2) — where ERROR output goes  (your screen by default)
    
    # 'echo' writes to stdout. This text is going to channel 1:
    echo "this line is stdout (channel 1)"
    
    # 'ls' on a missing file writes the complaint to stderr (channel 2),
    # NOT stdout. On screen they look identical, but they are separate pipes:
    ls no-such-file.txt
    Output
    this line is stdout (channel 1)
    ls: cannot access 'no-such-file.txt': No such file or directory
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    2. The Pipe Operator |

    The pipe (|) connects the stdout of the command on its left to the stdin of the command on its right. That lets you build a chain of tiny, single-purpose tools — list | filter | count — where data flows left to right. This is the Unix philosophy in action: "do one thing well," then combine. Read this worked example, run it, and watch each stage shrink the data.

    Worked example: chaining commands with pipes
    # A pipe (|) connects the stdout of one command to the stdin of the
    # next, so data flows left -> right through a chain of small tools.
    # Build a sample log first so every example below has real data:
    cat > server.log <<'EOF'
    [INFO]  GET /api/users
    [ERROR] database timeout
    [INFO]  POST /api/login
    [ERROR] database timeout
    [WARN]  slow query
    [ERROR] invalid token
    [ERROR] database timeout
    EOF
    
    # ls's list -> grep keeps the matches -> wc -l counts the lines.
    # This is the classic three-stage pipe:  list | filter | count
    ls | grep server | wc -l            # 1  (one file matches "server")
    
    echo "--- only the ERROR lines ---"
    grep ERROR server.log               # grep filters stdin/the file
    
    echo "--- count the ERROR lines ---"
    grep ERROR server.log | wc -l       # 4  (pass matches into wc -l)
    
    echo "--- most frequent errors ---"
    # sort groups duplicates, uniq -c counts each group, sort -rn ranks them:
    grep ERROR server.log | sort | uniq -c | sort -rn
    Output
    1
    --- only the ERROR lines ---
    [ERROR] database timeout
    [ERROR] database timeout
    [ERROR] invalid token
    [ERROR] database timeout
    --- count the ERROR lines ---
    4
    --- most frequent errors ---
          3 [ERROR] database timeout
          1 [ERROR] invalid token
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    Pro Tip — Build pipelines one stage at a time

    Don't write a five-command pipe in one go. Run grep ERROR server.log, check it looks right, then add | sort, check again, then | uniq -c, and so on. Each | you add only sees the output of the stage before it, so building up gradually makes mistakes obvious immediately.

    Your turn. The pipeline below is almost complete — fill in the blank marked ___ using the hint in the comment, then run it.

    🎯 Your turn: count the INFO lines
    # 🎯 YOUR TURN — replace each ___ then run it.
    # Goal: count how many lines contain the word INFO in server.log.
    
    # 1) Filter the file down to lines containing INFO
    #    👉 the tool that filters lines by a pattern is: grep
    ___ INFO server.log | wc -l         # 👉 put the filter tool in the blank
    
    # ✅ Expected output:
    #    2
    Output
    2
    Fill in the ___, run it (you'll need the server.log from the example above), and check your output matches the expected 2.

    3. Redirection: >, >>, <, 2>, 2>&1

    Redirection points a stream at a file instead of the screen or keyboard. > writes output to a file and overwrites it; >> appends to the end. < feeds a file in as stdin. To capture errors you must name the channel: 2> redirects stderr on its own, and 2>&1 means "send stderr to wherever stdout is going" — so > file 2>&1 puts both into one file. The order matters: redirect stdout first, then merge stderr into it.

    Worked example: redirecting input, output, and errors
    # Redirection sends a stream somewhere other than the screen/keyboard.
    
    # >  WRITES output to a file and OVERWRITES whatever was there.
    echo "Hello" > file.txt
    # >> APPENDS output to the end of a file (keeps existing contents).
    echo "World" >> file.txt
    cat file.txt                         # Hello / World on two lines
    
    # 2> redirects ONLY stderr (channel 2). Here stdout still hits the screen,
    # but the error is captured into errors.txt instead:
    ls file.txt no-such-file.txt 2> errors.txt
    echo "--- captured stderr ---"
    cat errors.txt
    
    # 2>&1 means "send channel 2 to wherever channel 1 is going" — i.e.
    # merge errors INTO normal output. Order matters: redirect stdout first.
    echo "--- merge both streams into one file ---"
    ls file.txt no-such-file.txt > all.txt 2>&1
    cat all.txt
    
    # < feeds a file in as stdin. sort reads its input from fruit.txt:
    echo "--- input redirection (sort < file) ---"
    printf 'banana\napple\ncherry\n' > fruit.txt
    sort < fruit.txt
    Output
    Hello
    World
    --- captured stderr ---
    ls: cannot access 'no-such-file.txt': No such file or directory
    --- merge both streams into one file ---
    ls: cannot access 'no-such-file.txt': No such file or directory
    file.txt
    --- input redirection (sort < file) ---
    apple
    banana
    cherry
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    4. /dev/null, tee, and Command Chaining

    /dev/null is a special file that throws away anything written to it — use it to silence output you don't care about. tee does the opposite of hiding: it splits a stream so it goes to a file and the screen at once. Finally, command chaining sequences whole commands: ; runs the next one regardless, && runs it only if the previous command succeeded, and || runs it only if the previous command failed.

    Worked example: /dev/null, tee, and &&, ||, ;
    # /dev/null is the "trash can" — anything written to it disappears.
    # Run a command but silence its normal output, keep only errors:
    ls file.txt no-such-file.txt > /dev/null
    echo "(stdout was thrown away; the error above came from stderr)"
    
    # Silence EVERYTHING (stdout and stderr) — a common, quiet check:
    grep ERROR server.log > /dev/null 2>&1
    echo "grep finished; we showed nothing from it"
    
    # tee splits a stream: it writes to a file AND passes it on to the screen.
    echo "--- tee: save and show at once ---"
    echo "saved and shown" | tee output.txt
    
    # Command chaining operators run commands in sequence:
    #   ;   run the next command no matter what
    #   &&  run the next command ONLY if the previous one SUCCEEDED
    #   ||  run the next command ONLY if the previous one FAILED
    echo "--- chaining ---"
    mkdir build && echo "made build/ (only printed because mkdir worked)"
    ls missing-dir || echo "ls failed, so this fallback ran"
    echo "first" ; echo "second (always runs)"
    Output
    ls: cannot access 'no-such-file.txt': No such file or directory
    (stdout was thrown away; the error above came from stderr)
    grep finished; we showed nothing from it
    --- tee: save and show at once ---
    saved and shown
    --- chaining ---
    made build/ (only printed because mkdir worked)
    ls: cannot access 'missing-dir': No such file or directory
    ls failed, so this fallback ran
    first
    second (always runs)
    This is real code — run it for free atonecompiler.com/bashor in your own editor.

    Now you try. Fill in the two blanks with the right chaining operators using the hints, then run it.

    🎯 Your turn: chain with && and ||
    # 🎯 YOUR TURN — replace each ___ then run it.
    # Goal: print "ok" ONLY if the file server.log exists, and run a
    # fallback message ONLY if the previous check failed.
    
    # 'test -f FILE' succeeds when the file exists (exit code 0).
    # 1) Run echo "ok" only if the test SUCCEEDS  -> use the AND operator
    test -f server.log ___ echo "ok"        # 👉 the "only if it worked" operator
    
    # 2) Run the fallback only if the test FAILS -> use the OR operator
    test -f ghost.log ___ echo "missing"     # 👉 the "only if it failed" operator
    
    # ✅ Expected output:
    #    ok
    #    missing
    Output
    ok
    missing
    Replace each ___ with the correct operator, run it, and check your output against the expected lines.

    Common Errors (and the fix)

    • > wiped out my file. A single > overwrites — it erases the file before writing. To add to a file without losing what's there, use >>. Treat > on an important file as a delete.
    • My error message still showed on screen after command > log.txt. Plain > only captures stdout. Errors are on stderr — capture them with 2> log.txt, or grab both with command > log.txt 2>&1.
    • "ambiguous redirect" or the merge didn't work. Order matters: write > file 2>&1, not 2>&1 > file. The 2>&1 copies wherever stdout points right now, so redirect stdout first.
    • Tried to pipe into a filename: grep ERROR | log.txt. A pipe | needs a command on its right, not a file. To send to a file use redirection: grep ERROR server.log > log.txt.
    • cat file | grep x works, but feels wasteful. It is — this is the "useless use of cat." grep can read the file directly: grep x file. Pipes are for chaining commands, not for feeding a single file in.

    📋 Quick Reference

    OperatorWhat it does
    cmd1 | cmd2Send cmd1's stdout into cmd2's stdin
    > fileRedirect stdout to a file (overwrite)
    >> fileRedirect stdout to a file (append)
    < fileFeed a file in as stdin
    2> fileRedirect only stderr to a file
    > file 2>&1Redirect both stdout and stderr to a file
    > /dev/null 2>&1Discard all output (silence everything)
    | tee fileWrite to a file AND show on screen
    cmd1 && cmd2Run cmd2 only if cmd1 succeeded
    cmd1 || cmd2Run cmd2 only if cmd1 failed
    cmd1 ; cmd2Run cmd2 after cmd1, regardless

    Frequently Asked Questions

    Q: What is the difference between a pipe (|) and redirection (>)?

    A pipe sends one command's output to ANOTHER command as its input (cmd1 | cmd2). Redirection sends output to or from a FILE (cmd > file.txt, or cmd < file.txt). Rule of thumb: a pipe always has a command on both sides; redirection has a filename on one side.

    Q: Why didn't > capture my error message?

    Plain > only redirects stdout (channel 1). Error messages travel on stderr (channel 2), so > leaves them on screen. Capture them with 2> file.txt, or merge both into one place with > file.txt 2>&1.

    Q: When do I use > versus >>?

    > overwrites the file — it erases the old contents first, so it's a common cause of accidental data loss. >> appends to the end and keeps what was already there. If in doubt and the file matters, use >>.

    Q: What does 2>&1 actually mean?

    It means 'send channel 2 (stderr) to wherever channel 1 (stdout) is currently going.' Put it AFTER you redirect stdout: command > file.txt 2>&1 sends both to the file. The shorthand &> file.txt does the same in Bash.

    Q: What is /dev/null for?

    It's a special file that discards everything written to it — the system 'trash can.' Use it to silence output you don't care about: command > /dev/null hides normal output, and command > /dev/null 2>&1 silences both output and errors.

    Q: What is the difference between && and ; when chaining commands?

    ; runs the next command no matter what happened before. && runs the next command only if the previous one SUCCEEDED (exit code 0). Use && to stop a chain when a step fails, e.g. cd build && make.

    Mini-Challenge: A Tiny Error Report

    No blanks this time — just a brief and an outline to keep you on track. Combine a pipe, tee, and && into a single line, run it, and check your output against the comments. This is exactly the kind of one-liner you'll reach for on real servers.

    🎯 Mini-Challenge: build an error-count report
    # 🎯 MINI-CHALLENGE: build a tiny error report
    # Using the server.log from earlier, in ONE chained line:
    # 1. Keep only the ERROR lines            (hint: grep)
    # 2. Count how many there are             (hint: | wc -l)
    # 3. Save that number to error-count.txt  AND show it on screen (hint: | tee)
    # 4. Then, ONLY if that worked, echo "report saved"  (hint: && )
    #
    # ✅ Expected output (screen):
    #    4
    #    report saved
    # ✅ error-count.txt should contain: 4
    
    # your one-line pipeline here
    Output
    4
    report saved
    Write the pipeline yourself using the outline. It should print 4 then report saved, and leave 4 inside error-count.txt.

    🎉 Lesson Complete!

    • ✅ Every command has three streams: stdin (0), stdout (1), stderr (2)
    • ✅ The pipe | feeds one command's stdout into the next command's stdin
    • > overwrites a file, >> appends, and < reads a file as stdin
    • 2> captures errors; 2>&1 merges stderr into stdout
    • /dev/null discards output; tee saves and shows at the same time
    • && (on success), || (on failure), and ; (always) sequence commands
    • Next lesson: Shell Scripting — wrap these pipelines into reusable scripts and automate them

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service