If there’s one feature that captures the spirit of the Unix philosophy — small tools that do one thing well, combined to do something powerful — it’s the pipe. The first time I chained three or four commands together with pipes and watched a messy log file turn into a clean, sorted summary in one line, I understood why so many people describe the shell as addictive once it clicks. In this article, I’ll cover how piping works, common patterns, and how to build genuinely useful one-liners and scripts around it.
What Is a Pipe?
A pipe, written as |, takes the standard output of one command and feeds it directly into the standard input of the next command.
command1 | command2
Here, whatever command1 prints to the screen instead gets sent directly to command2 as its input.
A Simple Example
ls -l | grep ".txt"
This lists files in the current directory (ls -l) and pipes that output into grep, which filters the lines to only those containing “.txt”.
Chaining Multiple Pipes
You’re not limited to a single pipe — you can chain as many as you need:
cat access.log | grep "ERROR" | sort | uniq -c | sort -nr
Let’s break this down:
cat access.log— outputs the full contents of the log file.grep "ERROR"— filters to only lines containing “ERROR”.sort— sorts those lines alphabetically (important for the next step).uniq -c— collapses duplicate lines and counts how many times each appeared.sort -nr— sorts numerically in reverse order, so the most frequent errors appear first.
This kind of pipeline is one of the most common patterns in day-to-day command-line work — take raw text, filter it, transform it, and summarize it, all in one line.
Common Commands Used in Pipelines
grep— filters lines matching a patternsort— sorts linesuniq— removes or counts duplicate lineswc— counts lines, words, or charactersawk— processes and reformats structured textsed— performs find-and-replace or other text transformationscut— extracts specific columns or fieldshead/tail— shows the first or last N linesxargs— converts piped input into arguments for another command
Counting Lines, Words, and Characters with wc
cat file.txt | wc -l # counts lines
Technically, in this case, you could skip the cat and just run wc -l file.txt directly, since wc can read files on its own. This is a common pattern people notice once they get more comfortable — the “useless use of cat” — but it’s not wrong, just occasionally redundant. I still use cat at the start of a pipeline sometimes because it makes the flow of data easier to read left to right, especially in longer pipelines.
Extracting Columns with cut and awk
echo "alex:x:1000:1000:Alex:/home/alex:/bin/bash" | cut -d: -f1
This outputs alex, extracting the first field from a colon-delimited string. awk can do similar things with more power:
ps aux | awk '{print $1, $11}'
This prints the first and eleventh columns (typically user and command) from the output of ps aux.
Using xargs to Pass Piped Data as Arguments
Pipes send data through standard input, but some commands expect their input as arguments rather than reading from stdin. That’s where xargs comes in:
find . -name "*.tmp" | xargs rm
This finds all .tmp files and passes each one as an argument to rm, effectively deleting them all. Without xargs, rm wouldn’t know what to do with data piped into it, since rm doesn’t read from standard input.
Redirecting Both Pipe and File Output with tee
Sometimes you want to see command output on screen and save it to a file at the same time:
ls -l | tee output.txt
tee reads from standard input, writes it to the specified file, and also passes it along to standard output — so it still works within a longer pipeline:
cat access.log | grep "ERROR" | tee errors_only.log | wc -l
This saves matching lines to errors_only.log while also counting them.
How Pipes Work Internally
When Bash encounters a pipe, it creates a pipe using the pipe() system call, which returns two file descriptors: one for reading and one for writing. Bash then forks a new process for each command in the pipeline, redirecting the write end of the pipe to the first process’s standard output and the read end to the second process’s standard input, using dup2() to make the substitution. Crucially, all commands in a pipeline start running roughly simultaneously — the kernel manages a buffer between them, so command2 can start consuming data as soon as command1 produces it, rather than waiting for command1 to finish completely. This is why pipelines can process huge files without needing to hold the entire output in memory at once. It’s also worth knowing that by default, only the exit status of the last command in a pipeline is what a script sees in $? — if an earlier command in the pipeline fails, that failure can be silently swallowed unless you enable set -o pipefail.
Real-World Use Cases
Finding the top memory-consuming processes:
ps aux --sort=-%mem | head -n 10
Counting how many times each IP address appears in a web server log:
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -n 20
Searching for a specific process and killing it:
ps aux | grep "myapp" | grep -v grep | awk '{print $2}' | xargs kill
This finds processes matching “myapp”, excludes the grep command itself from the results (a classic trick), extracts the process ID, and passes it to kill.
Automation Example: Disk Usage Report
#!/bin/bash
echo "Top 10 largest directories under /var:"
du -sh /var/* 2>/dev/null | sort -rh | head -n 10
echo ""
echo "Files modified in the last 24 hours:"
find /var/log -type f -mtime -1 | wc -l
This script combines du, sort, and head in a pipeline to produce a quick disk usage summary, along with a separate pipeline counting recently modified files.
Best Practices
- Keep pipelines readable — if a chain grows past four or five stages, consider breaking it into a script with intermediate variables or comments explaining each step.
- Use
set -o pipefailin scripts where you need to detect failures anywhere in a pipeline, not just the last command. - Prefer
awkover long chains ofcut,grep, andsedwhen the transformation logic gets complex, since a singleawkscript is often clearer and faster than five chained commands. - Use
teewhen you need to both inspect and save intermediate pipeline output, rather than running the same command twice.
Security Considerations
- Be cautious piping output into
xargswhen the input might contain unexpected characters like spaces or special shell characters — usexargs -0combined withfind -print0for filenames that might contain spaces or newlines, to avoid misinterpretation. - Never pipe untrusted or unvalidated data directly into commands like
sh,bash, oreval, since this is a classic pattern for command injection if the source of that data isn’t fully trusted (for example, piping the output ofcurldirectly intobashwithout inspecting it first). - When using
sudowithin a pipeline, remember that only the specific command you prefix withsudoruns with elevated privileges — the rest of the pipeline still runs as your regular user, which can lead to permission errors that are easy to misdiagnose.
Optimization Tips
- Since pipeline stages run concurrently, well-designed pipelines can process very large files efficiently without waiting for each stage to fully complete before the next begins.
- Avoid unnecessary intermediate commands (the “useless use of cat” pattern) when a command can read directly from a file, since each additional process in a pipeline adds a small amount of overhead.
- For very large datasets, tools like
awkthat combine filtering and transformation in a single pass are often more efficient than chaining multiple single-purpose commands together.
Troubleshooting
A pipeline seems to hang with no output: Check if an earlier command is waiting for input it’s not receiving — for example, forgetting to specify a filename to cat will cause it to wait on standard input from your keyboard.
$? shows success even though a command in the middle of the pipeline failed: By default, Bash only reports the exit status of the last command. Add set -o pipefail at the top of your script to make the pipeline’s exit status reflect any failure in the chain.
xargs mishandles filenames with spaces: Use find ... -print0 | xargs -0 ... to null-delimit filenames, which safely handles spaces and other unusual characters.
Common Mistakes
- Forgetting that pipeline exit status defaults to the last command only, hiding failures earlier in the chain.
- Overusing
cut,grep, andsedin long chains when a singleawkcommand would be clearer and faster. - Piping filenames into
xargswithout null-delimiting, causing issues with filenames containing spaces. - Piping untrusted data directly into an interpreter like
bashorshwithout reviewing it first.
FAQs
What’s the difference between a pipe (|) and redirection (>)? A pipe connects the output of one command directly to the input of another command. Redirection sends output to (or reads input from) a file rather than another command.
Can I pipe the output of a loop? Yes. For example: for i in {1..5}; do echo "$i"; done | sort -nr pipes the loop’s combined output into sort.
How do I see the exit status of every command in a pipeline, not just the last one? Use the PIPESTATUS array immediately after running the pipeline: echo "${PIPESTATUS[@]}" shows the exit status of each stage.
Does the order of commands in a pipeline matter for performance? Generally yes — filtering out unnecessary data as early as possible (for example, running grep before sort) reduces the amount of data later stages need to process.
Summary
Pipes are one of the most powerful and elegant features of Bash, letting you compose small, focused commands into pipelines that can filter, transform, and summarize data in a single line. Understanding how data flows between commands, knowing tools like xargs and tee for the edge cases pipes alone can’t handle, and being aware of pipefail for proper error detection will let you build pipelines that are both expressive and reliable. Once you’re fluent in piping, a huge amount of everyday text processing stops requiring a “real” script at all.
References
- Bash Reference Manual (Pipelines): https://www.gnu.org/software/bash/manual/bash.html#Pipelines
- GNU Coreutils Manual: https://www.gnu.org/software/coreutils/manual/coreutils.html
xargs(1)man page: https://man7.org/linux/man-pages/man1/xargs.1.html
