TerminalVibes / Text & Pipes
Part 4

Text & Pipes: How Small Tools Combine

"Every command speaks plain text. Pipes let them talk to each other."

So far you've run one command at a time and read its output on screen. Here you'll capture output into files, feed one command's output into another, and search, count, and reshape text the way experienced people do. This is where the terminal stops feeling like a museum piece: once output can move into files and into other commands, small tools multiply — and you'll recognize the same style in a lot of AI-suggested one-liners.

Important
The big idea of this whole part: in the terminal, everything is text. Logs, file listings, error messages, command output — all plain text. Once you can redirect text and pipe text, every small tool you know multiplies every other one.

4.1 Redirection — Point the Output Somewhere Else

Every command writes its results to something called standard output — which is normally your screen. Redirection says: "don't print that, put it in a file instead." One character does it: >.

Redirection reroutes a command's output from the screen into a file
Write, overwrite, append
echo "hello" > greeting.txt      # Create the file (or OVERWRITE it!)
cat greeting.txt
hello

echo "hello again" >> greeting.txt   # >> APPENDS to the end
cat greeting.txt
hello
hello again
Caution
> truncates. The instant you press Enter, the target file is emptied — before the command even runs. Redirect into an existing file and its old contents are gone, no confirmation, no trash can. When in doubt, use >> (append) or redirect to a new filename. And when an AI hands you a command containing >, check what's on the right side of the arrow before you run it.

Standard output isn't the only channel — every command is born with three. Standard input (stdin) is where it reads from, normally your keyboard; standard output (stdout) is where its results go; and standard error (stderr) is a separate channel for its complaints, kept apart so a failure never gets mixed in with the results. They're numbered 0, 1 and 2 — which is the whole story of the 2 in 2>. A plain > captures stream 1 only, so errors still land on your screen. To catch those too:

Capturing errors with 2>
ls reports/ missing/ > listing.txt 2> errors.txt

cat listing.txt      # The successful part
q1-summary.md
q2-summary.md

cat errors.txt       # The complaint went to its own file
ls: missing/: No such file or directory

You'll also meet > all.log 2>&1 in AI-generated commands, and it reads left to right: send stream 2 to wherever stream 1 is currently pointing, which the first half of the line has just set to all.log. The &1 is what makes it a reference to a stream rather than a filename — drop the & and you create a file called 1. Finally, the arrow points the other way too: < feeds a file into a command as its input, as in sort < names.txt. It's rarer — most commands happily take a filename argument — but it completes the picture: arrows move text in and out of files.

One member of the family points both ways at once. tee — named for the T-shaped pipe fitting, and it is exactly that — sits in the middle of a stream, writes everything flowing through it into a file, and passes the same text along untouched. It's the answer to a want the plain arrow can't satisfy: "show me this and keep a copy."

tee — watch it and save it
npm run build | tee build.log
# The build scrolls past as usual — AND lands in build.log

npm test 2>&1 | tee test.log     # errors included, still watching live

With a plain > you trade the live view for the file; tee refuses the trade. That makes it the receipt habit for anything long-running — let an agent's build talk to the screen while tee keeps the transcript, and "what did it actually say?" always has an answer. (tee -a appends instead of overwriting, the same courtesy >> extends.)

Vibe it

"Run the build and save all output — including errors — to build.log so I can read it later"

"Append today's date and a one-line status to my notes.txt without overwriting what's there"

Try It: Catch the Red Text

List one real file and one missing one, then split the streams — the useful listing into found.txt, the scary error into errors.txt — with > and 2>.
Catch the Red Text

Loading playground...

4.2 Pipes — Small Tools, Composed

Redirection sends output into a file. The pipe — | — sends it into another command. Whatever the left command prints becomes the right command's input, no temporary file needed. This one character is the reason terminal users never left.

A pipe connects the output of one command to the input of the next
Your first pipes
ls ~/Downloads | wc -l        # How many things are in Downloads?
47

history | tail -5             # The last 5 commands you ran

cat server.log | grep "ERROR" | wc -l   # How many errors in the log?
12

Two small things in there. history prints the shell's own record of what you've typed — it keeps one, whether or not you asked, and History Superpowers puts that record to work. And tail -5 is shorthand for tail -n 5: for head and tail, a bare dash-and-number is a line count. Not every dash-and-number is: the 9 in kill -9 is a signal number, which Stopping Things unpacks.

Read a pipeline left to right, like an assembly line: the text flows through each station and comes out transformed. Here's the last example as a picture:

This is the Unix philosophy, and it's older than most programming languages you know: "Write programs that do one thing and do it well," as Doug McIlroy — the inventor of the pipe — put it. grep only searches. wc only counts. sort only sorts. None of them is impressive alone — but because they all speak plain text, any of them can feed any other, and a handful of tiny tools becomes thousands of combinations.

Tip
Build pipelines one stage at a time. Run the first command alone and look at its output. Then add one | and look again. This isn't just how you write pipelines — it's how you audit the ones your AI writes. A five-stage pipeline you can't follow is really five one-stage commands you haven't run yet.
Vibe it

"Explain this pipeline stage by stage before I run it: cat access.log | grep POST | wc -l"

"Write me a pipeline that counts how many files in this folder end in .png"

4.3 Searching Text — grep

A server just crashed and the log is 10,000 lines long. Nobody scrolls that. grep prints only the lines that match a pattern — it's the terminal's search box, and probably the most-used tool in this course.

grep keeps only the lines that match — the haystack goes in, the needles come out

That 10,000-line log is a log file — a plain text file a program keeps appending to as it works, one line per event, oldest at the top. Most programs tag each line with a severity word: DEBUG for chatter, INFO for normal, WARN and ERROR for trouble. That convention is why searching for the bare word ERROR works at all.

grep basics
grep "ERROR" server.log          # Lines containing ERROR
2026-07-11 14:02:11 ERROR db connection refused
2026-07-11 14:02:15 ERROR retry limit reached

grep -r "TODO" src/              # Search every file under src/
src/app.js:14:  // TODO: handle empty cart
src/utils.js:3: // TODO: remove this hack

Both patterns above are plain words, which is why nothing surprising happened. The moment you reach for a wildcard, though, you've changed languages. grep doesn't use globs. It uses regular expressions — a second, older pattern language that looks deceptively similar and means different things. Here * means "zero or more of whatever came just before it", . matches any single character, and ^ and $ anchor a match to the start and end of a line. The trap isn't hypothetical: ERR* as a glob means "starts with ERR", but as a regular expression it means "ER followed by any number of Rs" — so it matches ER anywhere in any line, and quietly hands you the wrong answer.

CharacterIn a glob (filenames)In a regular expression (grep)
*Any run of charactersZero or more of the character before it
?Exactly one characterAn ordinary question mark
.An ordinary dotAny single character

The glob column is the one you drilled in Wildcards, and it still governs every filename you type. The regex column governs what's inside the quotes you hand to grep, and later to sed and awk. Same characters, different room.

Five flags cover 95% of real-world grep:

-i — ignore case

Matches error, Error, and ERROR. Use it by default when hunting in logs.

-n — line numbers

Prefixes each match with where it lives — essential when you're about to open the file and fix it.

-v — invert

Keep the lines that don't match. Perfect for filtering noise out: grep -v "DEBUG".

-c — count

Print how many lines matched instead of the lines themselves — a built-in | wc -l.

-r — recursive

Search a whole directory tree instead of one file. grep -rn "api_key" . is how developers answer "where in this codebase is that used?"

And because grep reads standard input, it slots into any pipeline as a filter. This is the pattern you'll type every single day:

grep as a pipeline filter
history | grep "cd"              # Every cd you've ever run
tail -50 server.log | grep -i "error"   # Only recent errors
grep "ERROR" server.log | grep -v "retry"  # Errors, minus the noisy ones

Try It: Find the Crash

A server died last night and server.log has hundreds of lines. Use grep — with -i, -n, and -v to cut the noise — and pipes to wc -l to pin down exactly when and why it crashed.
Find the Crash

Loading playground...

Vibe it

"Search server.log for errors, ignoring case, and show me the line numbers"

"Find every file in this project that still mentions the old function name 'fetchUser'"

4.4 Counting & Shaping — wc, sort, uniq, cut

grep finds lines. These four tools reshape them: count them, order them, de-duplicate them, and slice out columns. Individually they're almost boring — together they answer real questions, like "who's hammering my website?"

Four tiny tools: count with wc, order with sort, collapse with uniq, slice with cut
The cast, one line each
wc -l server.log        # Count lines (-w words, -c bytes)
843 server.log

sort names.txt          # Alphabetical order (-n numeric, -r reversed)

uniq names.txt          # Collapse REPEATED ADJACENT lines into one

cut -d',' -f2 users.csv # Slice column 2, using ',' as the delimiter

That last line hides a rule: some flags take a value. -d',' is one flag — d for delimiter — with its value jammed straight onto the letter, and -f2 is f for field, value 2. Other commands want a space between the two, as in -n 3, and many take either. So -f2 is not two clustered flags the way ls -la is — where the letter ends and its value begins is each command's own decision, and man cut is what tells you.

Warning
The uniq gotcha: uniq only collapses duplicates that are next to each other. Given a b a, it removes nothing. That's why it practically always appears as sort | uniqsort herds the duplicates together first, then uniq collapses them.

Try It: Count Before You Fix

Before ranking anything, answer the simplest question: how many? Pipe grep ERROR server.log into wc -l to count the error lines, and save the total to a file.
Count Before You Fix

Loading playground...

Now the payoff. Your web server writes one line per visit to access.log, and you want to know your top visitors. This is the most famous pipeline in Unix history, and we'll build it one stage at a time — exactly the way you should build every pipeline.

Stage 0 — look at the raw material
head -4 access.log
203.0.113.9 GET /home
198.51.100.4 GET /pricing
203.0.113.9 GET /docs
192.0.2.55 GET /home

Two things to read before you start slicing. The number at the front is the visitor's IP address — each machine on a network has one, and What Is localhost? takes it apart. And /home and /pricing are URL paths — the part after the site's address — not folders on this machine. Same slashes, different tree.

Stage 1 — cut: keep only the IP column
cut -d' ' -f1 access.log
203.0.113.9
198.51.100.4
203.0.113.9
192.0.2.55
...

# -d' ' = columns are separated by spaces; -f1 = give me field 1
Stage 2 — sort: herd the duplicates together
cut -d' ' -f1 access.log | sort
192.0.2.55
198.51.100.4
198.51.100.4
203.0.113.9
203.0.113.9
203.0.113.9
...
Stage 3 — uniq -c: collapse and count
cut -d' ' -f1 access.log | sort | uniq -c
   1 192.0.2.55
   2 198.51.100.4
   3 203.0.113.9
...

# -c = count: each collapsed group carries a tally of how many lines it stood for.
# (Not grep's -c, which prints one total and no lines at all.)
Stage 4 — sort -n: rank by the count
cut -d' ' -f1 access.log | sort | uniq -c | sort -n
   1 192.0.2.55
   2 198.51.100.4
   3 203.0.113.9

# Biggest number last — your top visitor is 203.0.113.9
Tip
Read the finished pipeline aloud and it's a sentence: "take column one, group the duplicates, count each group, rank by count." When you can narrate a pipeline like that, you own it — whether you wrote it or an AI did.

Try It: Build a Pipeline

access.log is waiting in the playground. Build the classic pipeline stage by stage — cut, then | sort, then | uniq -c, then | sort -n — and identify your site's top visitor. Run each stage before adding the next.
Build a Pipeline

Loading playground...

Vibe it

"From access.log, show me the top 5 most-requested pages with their counts"

"How many unique visitors are in this log file? Walk me through the pipeline you use"

4.5 Finding Files — find

grep searches inside files. But sometimes the question is "where is that file?" — a config you know exists, every Markdown file in a project, that one script you wrote last month. find walks an entire directory tree and prints every path that matches your criteria.

find walks the whole tree and reports every path that matches
find essentials
find . -name '*.md'            # Every Markdown file below here
./README.md
./docs/setup.md
./docs/notes/ideas.md

find . -name '*.md' -type f    # -type f: files only
find . -type d -name 'test*'   # -type d: directories only
find ~ -name '.zshrc'          # Start the search from your home folder
Caution
Quote the pattern. Write '*.md' with quotes, not bare *.md. Remember Copy, Move, Delete: the shell expands wildcards before the command ever runs. Unquoted, *.md becomes a list of the Markdown files in your current folder — and find receives that list instead of the pattern, searching for the wrong thing entirely. Single quotes are the fully literal kind, so the pattern reaches find intact and gets applied at every level of the tree — the rule for both kinds of quote is in Paths.

Keep the two search tools straight — they're a team, not rivals:

find — searches filenames

"Where are the files called *.md?" It never opens a file; it only looks at names, types, and locations.

grep — searches contents

"Which lines contain TODO?" It reads inside files but doesn't care what they're named.

And naturally, they compose. Find the files by name, then grep inside exactly those:

find + grep, together
find . -name '*.js' | xargs grep -n "TODO"
./src/app.js:14:  // TODO: handle empty cart
./src/utils.js:3: // TODO: remove this hack

# (For a whole tree, grep -rn "TODO" . is the simpler everyday version.)

xargs is there because grep wants filenames as arguments, not on standard input. Pipe a list of paths straight into grep and it dutifully searches the text of those paths instead of the files they name — a pipeline that runs, returns nothing, and looks like an answer. xargs collects the lines arriving on standard input and lays them out as arguments, which is the shape grep was waiting for. Reach for it when you want to narrow the file set first — by name, type or depth — and reach for grep -rn when you want everything under a folder.

Try It: Hunt Down Every TODO

A project tree is scattered with leftover TODOs. Use find . -name '*.js' -type f to locate the source files (quote that glob!), then combine with grep to list every TODO with its file and line number.
Hunt Down Every TODO

Loading playground...

Vibe it

"Find every Markdown file in this project, including ones in nested folders"

"List all the TODO comments left anywhere in src/ with their file names and line numbers"

Notice what all of these have in common: they find and count — none of them changes a single character. That's deliberate, and it's why they're safe to experiment with. When you're ready to make text different — swap a word across a file, drop every DEBUG line, pull one column out of a table — that's sed and awk, waiting in Text Surgery.

Challenge: Who Is Actually Visiting?

Loading challenge...