Linux Pipes Explained: How to Use the | Operator

By 

Published on

8 min read

Linux commands connected through a shell pipeline

A pipe in Linux is a one-way channel that connects the standard output of one process to the standard input of another. The shell builds the channel for you when you type a | character between two commands. The first command writes; the second command reads. Both run at the same time, and data flows between them through the kernel without ever touching disk.

The pipe is one of the oldest and most useful ideas in Unix. It is what makes a small command such as grep or sort feel like part of a much larger toolbox. Knowing how pipes behave, where they fit in a shell pipeline, and when to reach for related tools such as xargs and process substitution makes shell work more effective.

This guide explains how to build pipelines, handle errors and exit statuses, and use related features such as named pipes and process substitution.

Pipe Syntax

The general syntax for a shell pipeline is:

txt
command1 | command2 [| command3 ...]

Each command passes its standard output to the standard input of the command on its right. A pipeline can contain two commands or a longer chain, and the shell runs all stages concurrently.

How a Pipe Works

A shell pipeline of the form cmd1 | cmd2 follows these steps:

  1. The shell creates an anonymous pipe with the pipe(2) system call. The call returns two file descriptors: one for reading and one for writing.
  2. The shell starts the commands and connects the first command’s standard output (file descriptor 1) to the write end of the pipe.
  3. The second command reads from the pipe through its standard input (file descriptor 0), while the kernel handles the data flow between them.

Linux pipes have a limited in-kernel buffer. The default is commonly 16 memory pages, or 64 KiB on systems with 4 KiB pages, but applications should not rely on a fixed capacity. When the buffer fills, the writer waits until the reader consumes more data.

A First Example

The following pipeline generates three service names and filters them with grep:

Terminal
printf '%s\n' ssh.service NetworkManager cron.service | grep -i network
output
NetworkManager

printf writes one service name per line. The pipe sends those lines to grep, which prints the line containing network. The -i option makes the match case-insensitive.

The same pattern works with any command that writes to standard output and any command that reads from standard input.

Pipes Pass Data, Not Files

A pipe carries a stream of bytes, not a file name or command-line argument. The receiving command reads that stream from standard input.

For example, the following command sends the text /etc/hosts to wc. It does not ask wc to open that file:

Terminal
printf '%s\n' /etc/hosts | wc -l
output
1

The result is 1 because wc received one line of text. To count lines in the file, pass the path as an argument with wc -l /etc/hosts. Commands such as rm, mv, and stat also expect paths as arguments rather than reading them from standard input.

Combining Pipes with xargs

xargs reads items from standard input and turns them into arguments for another command. This example creates three empty files:

Terminal
printf '%s\0' report-1.txt report-2.txt report-3.txt | xargs -0 touch

The null character separates each name, and xargs -0 converts the three items into arguments for touch. The same null-delimited pattern safely handles spaces and newlines in file names.

Before deleting files found by find, print the matches and inspect them:

Terminal
find . -type f -name '*.log' -mtime +30 -print

When the list is correct, use -print0 with xargs -0. The -r option prevents GNU xargs from running rm when no files match:

Terminal
find . -type f -name '*.log' -mtime +30 -print0 | xargs -0 -r rm --

The -- marker prevents a path beginning with a hyphen from being interpreted as an rm option.

Reading the Exit Status of a Pipeline

Bash returns the exit status of the last command in a pipeline by default. A failure in an earlier stage can therefore go unnoticed:

Terminal
grep something /missing/file 2>/dev/null | wc -l
echo "$?"
output
0
0

grep failed because the file did not exist, but wc -l succeeded, so the pipeline status was 0. Enable pipefail when the script must detect failures in any stage:

Terminal
set -o pipefail
grep something /missing/file 2>/dev/null | wc -l
echo "$?"
output
0
2

With pipefail enabled, the pipeline returns the status of the rightmost command that failed, or 0 if every command succeeded.

To inspect every stage individually, use the PIPESTATUS array:

Terminal
ls /missing 2>/dev/null | grep foo
echo "${PIPESTATUS[@]}"
output
2 1

PIPESTATUS[0] is ls and PIPESTATUS[1] is grep.

Pipes and Standard Error

A pipe only carries standard output (file descriptor 1). Standard error (file descriptor 2) goes straight to the terminal:

Terminal
ls /missing | wc -l
output
ls: cannot access '/missing': No such file or directory
0

The error message bypassed the pipe. To send errors through the same pipe, redirect standard error to standard output with 2>&1 before the pipe:

Terminal
ls /missing 2>&1 | tee log.txt

The tee command writes the redirected error stream to log.txt and displays it in the terminal. Bash also provides |& as shorthand for 2>&1 |:

Terminal
ls /missing |& tee log.txt

For more redirection patterns and details about ordering, see how to redirect stderr to stdout .

Named Pipes (FIFOs)

The | operator creates an anonymous pipe that vanishes when the pipeline ends. A named pipe lives on the filesystem and lets unrelated processes share data. Create one with mkfifo:

Terminal
mkfifo /tmp/linuxize-pipe

In the first terminal, start the reader:

Terminal
cat /tmp/linuxize-pipe

In a second terminal, write to the FIFO:

Terminal
printf '%s\n' "hello" > /tmp/linuxize-pipe
output
hello

The first terminal prints hello. Opening a FIFO normally blocks until the other end connects, so start the reader and writer in separate terminals. Named pipes are useful for communication between scripts and processes that do not share a parent.

Remove the FIFO when finished:

Terminal
rm /tmp/linuxize-pipe

Process Substitution

When a command expects one or more file paths instead of standard input, process substitution can expose command output through a file-like path:

Terminal
diff <(printf '%s\n' alpha beta) <(printf '%s\n' alpha gamma)

diff receives two generated paths as arguments and compares the output from both printf commands. Bash implements those paths with /dev/fd entries or named pipes, depending on the operating system.

The reverse form, >(command), provides a file-like destination. The following pipeline saves a compressed copy while continuing to filter the original stream:

Terminal
some_command | tee >(gzip > out.gz) | grep error

Process substitution is available in Bash, ksh, and zsh, but it is not part of POSIX sh.

Useful Pipe-Based One-Liners

A handful of pipelines come up again and again in shell work:

Terminal
ps -eo pid,comm,%cpu --sort=-%cpu | head -n 6

ps sorts processes by CPU use, and head keeps the header plus the five busiest processes.

Terminal
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head

This pipeline extracts client addresses from a common Nginx or Apache access log, counts duplicates, and prints the most frequent addresses first.

Terminal
du -h --max-depth=1 . 2>/dev/null | sort -h

The output shows disk use for the current directory and its immediate subdirectories, sorted from smallest to largest.

Each pipeline reads as a sentence: do this, then this, then this. The flow matches how the data moves between processes.

Quick Reference

TaskCommand
Send output to another commandcommand1 | command2
Build a multi-stage pipelinecommand1 | command2 | command3
Pipe stdout and stderr in Bashcommand1 |& command2
Enable pipeline failure detectionset -o pipefail
Read every Bash pipeline statusprintf '%s\n' "${PIPESTATUS[@]}"
Convert null-delimited input to argumentscommand1 | xargs -0 command2
Save and continue a streamcommand1 | tee file | command2
Create a named pipemkfifo /tmp/name
Compare generated output as filesdiff <(command1) <(command2)

Troubleshooting

A variable set in a pipeline is empty afterward
In Bash, each command in a multi-command pipeline normally runs in its own subshell. Variables set there do not survive in the parent shell:

Terminal
echo "data" | read x
printf '<%s>\n' "$x"
output
<>

Use a here-string or redirect process substitution into read so it runs in the current shell:

Terminal
read -r x <<< "data"
printf '<%s>\n' "$x"
output
<data>

A command reports a broken pipe
A writer receives SIGPIPE when the reader closes early, which often happens when head has collected enough lines. This can be expected, but do not hide all errors with a broad 2>/dev/null. Check the individual statuses with PIPESTATUS, especially when pipefail is enabled.

The next command ignores piped file names
Tools such as rm, mv, and stat expect paths as arguments. Use xargs -0 with null-delimited input, and preview destructive operations before running them.

Output appears in delayed batches
Some programs use larger output buffers when writing to a pipe instead of a terminal. GNU grep supports --line-buffered, and GNU stdbuf -oL command can request line-buffered output from programs that use standard C streams. These options are not portable to every Unix system or effective with every command.

Conclusion

Linux pipes let you connect focused commands without intermediate files. Use pipefail when every stage must succeed, xargs when a command needs arguments, and process substitution or named pipes when a simple pipeline does not fit the task.

Linuxize Weekly Newsletter

A quick weekly roundup of new tutorials, news, and tips.

About the authors

Dejan Panovski

Dejan Panovski

Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 800+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.

View author page