How to Use Pipes, Redirection, and Command Chaining on Fedora

Fedora's Bash shell lets you combine commands powerfully using pipes, input/output redirection, and chaining operators — all standard tools for scripting and interactive work.

You ran a command and the output vanished

You ran a long update or a search through logs and the terminal scrolled past the error before you could read it. Or you need to compare the installed packages on two machines without copying files back and forth. You know how to run commands, but you need them to talk to each other. The shell gives you pipes, redirection, and chaining to connect commands into a single workflow.

What's actually happening

Every command in the terminal has three data streams. Standard input is where the command reads data. Standard output is where the command sends results. Standard error is where the command sends warnings and failures. By default, output and error both go to your screen. Input comes from your keyboard.

Pipes and redirection let you reroute these streams. Exit codes tell the shell whether a command succeeded or failed. Chaining operators use those codes to decide what to run next. Think of the shell as a traffic cop directing data flow. You tell it where the data comes from, where it goes, and what to do if a command crashes.

Connect commands with pipes

A pipe sends the standard output of one command directly into the standard input of the next. The | character connects the commands. The left command writes to the pipe. The right command reads from the pipe.

Here's how to filter journal logs for failed units and count them.

# Filter journal logs for failed units and count them
# The pipe sends the text output of journalctl directly into grep
# grep filters lines containing "Failed" and passes only those to wc
# wc -l counts the lines, giving you a total number of failures
# --no-pager is required so journalctl writes to stdout instead of opening a pager
journalctl -p err --no-pager | grep "Failed" | wc -l

Pipes work best with commands that read and write text. You can chain multiple pipes. Each stage processes the data and passes it forward.

Here's how to find the largest files in a directory tree.

# Find the 10 largest files in the current directory tree
# du -sh calculates sizes in human-readable format
# sort -rh sorts the lines by size in reverse order
# head -10 takes only the top 10 lines from the sorted output
du -sh * | sort -rh | head -10

Pipe output to less when the result is too long to read at once. This lets you scroll through the pipeline result interactively.

# View a long man page one screen at a time
# man sends output to the pipe
# less reads from stdin and provides scrolling and search
man dnf | less

Pipe to less when the output exceeds the screen height.

Save output with redirection

Redirection sends streams to files or reads files as input. The > operator overwrites a file. The >> operator appends to a file. The 2> operator redirects standard error. The &> operator redirects both output and error.

Here's how to save the list of installed packages.

# Save the list of installed packages to a file
# > creates the file or overwrites it if it already exists
# This captures only standard output, not errors
# Use this to create a backup of your package state
dnf list installed > ~/packages-backup.txt

# Append a timestamp to a log file without losing previous entries
# >> opens the file in append mode and writes to the end
# Use this for logs that grow over time
echo "$(date): backup finished" >> ~/backup-log.txt

# Capture errors separately from normal output
# 2> redirects file descriptor 2 (stderr) to a file
# stdout still goes to the terminal unless redirected separately
# This helps isolate failures during upgrades
sudo dnf upgrade 2> ~/upgrade-errors.txt

Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. If you redirect output to /etc/, you are modifying user config. If you accidentally overwrite /usr/lib/, the package manager will complain on the next upgrade.

Test redirection with echo before pointing it at a real command. One typo with > can wipe a config file.

Control flow with chaining

Chaining operators control whether subsequent commands run based on the exit status of the previous one. Exit code 0 means success. Any non-zero code means failure.

Here's how to update the system and reboot only if the update succeeds.

# Update the system and reboot only if the update succeeds
# && checks the exit code of the left command
# If dnf returns 0 (success), reboot runs
# If dnf fails, reboot is skipped and you can investigate
# --refresh forces dnf to check for newer metadata before upgrading
sudo dnf upgrade --refresh && sudo reboot

# Check network connectivity and warn if the ping fails
# || runs the right command only when the left command returns a non-zero exit code
# ping returns 0 on success, so echo runs only on failure
# -c 1 sends one packet. -W 2 sets a 2-second timeout
ping -c 1 -W 2 fedoraproject.org || echo "Network unreachable"

# Clean cache and then upgrade regardless of cache status
# ; runs the next command unconditionally
# Use this when the second command doesn't depend on the first
sudo dnf clean all; sudo dnf upgrade

Use && for dependent steps. Use ; only when the order matters but the result of the first step doesn't.

Advanced patterns

Process substitution treats command output as a file path. The <(...) syntax runs the command and presents the output as a temporary file descriptor. This is useful when a command expects a file argument but you have dynamic output.

Here's how to compare installed packages on two machines without creating temporary files.

# Compare installed packages on two machines without creating temp files
# <(...) runs the command and presents the output as a temporary file path
# diff reads both "files" and shows the differences
# This avoids writing large lists to disk manually
# ssh runs the command on the remote host and streams the output back
diff <(dnf list installed) <(ssh remote-host dnf list installed)

Process substitution is your friend when a command expects a file path but you have command output.

Here-documents let you pass multi-line input to a command. The << operator reads input until it sees the delimiter.

# Pass a multi-line string to a command
# cat reads from the here-document and writes to stdout
# Quoting the delimiter ('EOF') prevents variable expansion inside the block
# This is useful for generating config snippets inline
cat <<'EOF'
# Generated config block
[settings]
mode = production
debug = false
EOF

Verify the exit code

Check the exit code of the last command with $?. This variable holds the exit status of the most recent foreground pipeline.

Here's how to check the exit code immediately after a command.

# Check the exit code of the last command
# $? holds the exit status of the most recent foreground pipeline
# 0 means success. Any other number indicates a specific error
# Run this immediately after the command you want to check
dnf check-update
echo $?

Run echo $? immediately after a command. The exit code tells you the truth.

Common pitfalls

The grep command returns exit code 1 when it finds no matches. If you chain grep with &&, the next command won't run when the search fails. This breaks scripts that expect the pipeline to continue. Use || true if you want to ignore the "no match" exit code.

# grep returns 1 if no lines match, which stops && chains
# Add || true to force a success exit code when no match is found
# This keeps the pipeline alive even if the search returns nothing
grep "error" /var/log/messages || true

The > operator overwrites files silently. If you type echo "test" > /etc/fstab instead of >>, you lose your mount configuration. Always double-check the operator. Use set -o noclobber in your shell to prevent accidental overwrites, or use >| to force an overwrite when needed.

Pagers can break pipes. Commands like journalctl and man open a pager by default. The pager intercepts output and prevents it from flowing through the pipe. Add --no-pager to commands that support it.

Check the exit code of grep in scripts. A missing match returns 1, which can abort a pipeline unexpectedly.

When to use each tool

Use pipes when you need to filter or transform text streams between commands. Use redirection when you need to save output to a file or feed a file as input. Use && when the second command depends on the success of the first. Use || when you want to handle failure or provide a fallback action. Use ; when commands must run in order but are independent of each other. Use process substitution when a command requires a file path but you have dynamic output. Stay on simple commands when the logic gets too complex for a one-liner and write a script instead.

Where to go next