Skip to content

CLI Pipelines: Keeping Data in the Stream

How to work with command output without reaching for the mouse. The core idea: output is input to the next command, not text to be read and retyped.

The fundamental building block. | sends stdout of one command to stdin of the next.

Terminal window
# Count Go files
find . -name "*.go" | wc -l
# Find the 5 largest files
du -sh * | sort -rh | head -5
# Unique sorted IPs from a log
awk '{print $1}' access.log | sort -u
# Chain as many as you need
cat /etc/passwd | cut -d: -f1 | sort | head -20

Skip the select-and-copy step entirely.

Terminal window
# macOS
echo "something useful" | pbcopy
pbpaste # retrieve it
# Linux (X11)
echo "something useful" | xclip -selection clipboard
xclip -selection clipboard -o
# Linux (Wayland)
echo "something useful" | wl-copy
wl-paste

Capture output inline with $(). The shell runs the inner command first and substitutes the result.

Terminal window
# Open all files containing TODO
nvim $(rg -l "TODO")
# cd into a found directory
cd $(fd -t d "components" | head -1)
# Use a timestamp in a filename
tar czf "backup-$(date +%Y%m%d).tar.gz" ./src
# Assign to a variable
current_branch=$(git branch --show-current)
echo "On branch: $current_branch"
Terminal window
# Count lines in the most recently modified file
wc -l $(ls -t *.log | head -1)

Reuse pieces of previous commands without retyping.

ExpansionMeaning
!!Last command, entire line
!$Last argument of previous command
!^First argument of previous command
!*All arguments of previous command
!:2Second argument of previous command
$_Last argument (works in scripts)
!-2Command two entries back
Terminal window
# Rerun with sudo
apt install nginx
sudo !! # becomes: sudo apt install nginx
# Reuse the path you just typed
ls /var/log/nginx/error.log
less !$ # becomes: less /var/log/nginx/error.log
vim !$ # open the same file
# Reuse all arguments
cp file1.txt file2.txt /backup/
ls !* # becomes: ls file1.txt file2.txt /backup/
Terminal window
# Fix a typo in the last command
git stats
^stats^status^ # reruns as: git status

Convert stdin lines into arguments for another command.

Terminal window
# Delete all .tmp files
fd '\.tmp$' | xargs rm
# Open matching files in your editor
rg -l "deprecated" | xargs nvim
# Run in parallel (-P)
fd '\.png$' | xargs -P 4 optipng
# Handle filenames with spaces (-0 with -print0 or fd -0)
fd -0 '\.log$' | xargs -0 rm
# Limit arguments per invocation (-n)
echo "a b c d" | xargs -n 2 echo # "a b" then "c d"
# Substitute placement (-I)
cat urls.txt | xargs -I {} curl -O {}

<() creates a temporary file-like object from command output. Use when a command expects a filename, not stdin.

Terminal window
# Diff two commands without temp files
diff <(curl -s https://api.example.com/v1) <(curl -s https://api.example.com/v2)
# Diff sorted outputs
diff <(sort file1.txt) <(sort file2.txt)
# Compare directory listings
diff <(ls dir1/) <(ls dir2/)
# Source output of a command
source <(kubectl completion bash)

fzf turns any list into an interactive picker. No mouse needed — type to filter, arrow keys to select.

Terminal window
# Pick a file to edit
nvim $(fzf)
# Pick a branch to checkout
git checkout $(git branch --all | fzf)
# Pick a process to kill
kill $(ps aux | fzf | awk '{print $2}')
# Pick a docker container to exec into
docker exec -it $(docker ps --format '{{.Names}}' | fzf) bash
# Pick from command history
$(history | fzf | sed 's/^ *[0-9]* *//')
Terminal window
# File picker with preview
nvim $(fzf --preview 'bat --color=always {}')
# Git log picker with diff preview
git show $(git log --oneline | fzf --preview 'git show {1}' | awk '{print $1}')
BindingAction
Ctrl+TPaste selected file path into prompt
Ctrl+RSearch command history
Alt+Ccd into selected directory

See Shell Scripting for heredoc syntax, quoting variants, and here strings.

Send output to both a file and the next command in the pipeline.

Terminal window
# Save and display
curl -s https://api.example.com | tee response.json | jq '.status'
# Save intermediate pipeline results
ps aux | tee processes.txt | grep nginx
# Append instead of overwrite
echo "log entry" | tee -a debug.log

See Shell Scripting for variable assignment, exit status ($?), and output capture. The while read loop fed by process substitution < <(cmd) combines a shell loop with process substitution.

Real workflows chain these together.

Terminal window
# Find the function that changed most across git history
git log --oneline --all --follow -p -- '*.py' \
| grep "^+.*def " \
| sed 's/^+//' \
| sort \
| uniq -c \
| sort -rn \
| head -10
# Deploy: build, tag, push — stopping on any failure
version=$(git describe --tags) \
&& docker build -t "app:$version" . \
&& docker push "app:$version" \
&& echo "Deployed $version"
# Interactive git stash apply
git stash apply $(git stash list | fzf | cut -d: -f1)
# Find large files not tracked by git
comm -23 \
<(find . -type f -size +1M | sort) \
<(git ls-files | sort) \
| head -20
GoalPattern
Use output as argumentscmd $(other_cmd)
Use output as stdincmd1 | cmd2
Use output as a filecmd <(other_cmd)
Save output and pass it alongcmd1 | tee file | cmd2
Send output to clipboardcmd | pbcopy
Pick from output interactivelycmd | fzf
Reuse last argument!$ or $_
Rerun last command!!
Convert stdin to argumentscmd1 | xargs cmd2
Fix typo in last command^old^new^
  • Shell Scripting — Variables, heredocs, functions, loops, and error handling for scripts that use these pipeline patterns
  • Unix CLI — Core commands for file ops and text processing
  • jq — JSON processing in pipelines
  • Python CLI
  • Regex
  • CLI-First