Advanced Shell Patterns
Advanced shell patterns for power users.
Process Substitution <() and >()
Process substitution treats command output as a file. The shell creates a named pipe (/dev/fd/NN) that commands can read from.
Basic Pattern: Remote File Access
# Read remote file with line numbers
awk 'NR>=74 && NR<=90 {print NR": "$0}' <(ssh bind-01 "sudo cat /var/named/inside.domusdigitalis.dev.zone")
# Same pattern, different filter
grep -n "CNAME" <(ssh bind-01 "sudo cat /var/named/inside.domusdigitalis.dev.zone")
Diff Remote Files
# Compare zone files between two DNS servers
diff <(ssh bind-01 "sudo cat /var/named/zone.db") \
<(ssh bind-02 "sudo cat /var/named/zone.db")
# Side-by-side comparison
diff -y <(ssh kvm-01 "cat /etc/hosts") \
<(ssh kvm-02 "cat /etc/hosts")
# Colorized diff
diff --color=always <(ssh host1 "cat /etc/resolv.conf") \
<(ssh host2 "cat /etc/resolv.conf")
Compare Local vs Remote
# Check if local config matches remote
diff /etc/chrony.conf <(ssh kvm-02 "cat /etc/chrony.conf")
# Compare sorted outputs
diff <(sort /etc/hosts) <(ssh remote "sort /etc/hosts")
Multi-Source Aggregation
# Combine outputs from multiple hosts
cat <(ssh kvm-01 "hostname; uptime") \
<(ssh kvm-02 "hostname; uptime") \
<(ssh kvm-03 "hostname; uptime")
# With headers
{
echo "=== kvm-01 ===" && ssh kvm-01 "df -h /"
echo "=== kvm-02 ===" && ssh kvm-02 "df -h /"
}
# Parallel execution with paste (side by side)
paste <(ssh kvm-01 "vmstat 1 5") <(ssh kvm-02 "vmstat 1 5")
Filter Remote Logs
# Extract specific time range from remote syslog
awk '/Mar 1 19:/ && /sshd/' <(ssh kvm-02 "sudo journalctl --no-pager")
# Filter remote auth logs for failures
grep "Failed password" <(ssh kvm-02 "sudo cat /var/log/secure")
# Real-time remote log filtering (blocks)
grep --line-buffered "error" <(ssh kvm-02 "sudo tail -f /var/log/messages")
LVM/Disk Comparison
# Compare LVM layouts
diff <(ssh kvm-01 "sudo lvs --noheadings") \
<(ssh kvm-02 "sudo lvs --noheadings")
# Compare partition tables
diff <(ssh kvm-01 "lsblk -o NAME,SIZE,TYPE,MOUNTPOINT") \
<(ssh kvm-02 "lsblk -o NAME,SIZE,TYPE,MOUNTPOINT")
Join/Paste Remote Data
# Join user lists from two systems (find common users)
comm -12 <(ssh host1 "cut -d: -f1 /etc/passwd | sort") \
<(ssh host2 "cut -d: -f1 /etc/passwd | sort")
# Find users on host1 but NOT on host2
comm -23 <(ssh host1 "cut -d: -f1 /etc/passwd | sort") \
<(ssh host2 "cut -d: -f1 /etc/passwd | sort")
Output Process Substitution >()
Write to multiple destinations:
# Tee to file AND process
echo "test" | tee >(gzip > test.gz) >(sha256sum > test.sha256)
# Log to file while also sending to remote
command 2>&1 | tee >(ssh loghost "cat >> /var/log/remote.log")
IPMI/Network Comparison
# Compare IPMI settings across hosts
diff <(ssh kvm-01 "sudo ipmitool lan print 1") \
<(ssh kvm-02 "sudo ipmitool lan print 1")
# Compare firewall rules
diff <(ssh kvm-01 "sudo firewall-cmd --list-all") \
<(ssh kvm-02 "sudo firewall-cmd --list-all")
AWK with Line Ranges
# Print lines 50-60 with line numbers
awk 'NR>=50 && NR<=60 {print NR": "$0}' <(ssh host "cat /etc/ssh/sshd_config")
# Print first match and 5 lines after
awk '/^PermitRootLogin/,NR==FNR+5' <(ssh host "cat /etc/ssh/sshd_config")
# Extract section between markers
awk '/BEGIN_SECTION/,/END_SECTION/' <(ssh host "cat config.conf")
Verification Patterns
# Verify DNS zone serial across servers
echo "bind-01: $(ssh bind-01 "sudo grep -oP 'Serial.*\K\d+' /var/named/zone.db")"
echo "bind-02: $(ssh bind-02 "sudo grep -oP 'Serial.*\K\d+' /var/named/zone.db")"
# One-liner comparison
diff <(ssh bind-01 "sudo grep Serial /var/named/zone.db") \
<(ssh bind-02 "sudo grep Serial /var/named/zone.db") && echo "MATCH" || echo "DRIFT!"
Command Substitution $()
Capture output into variables or inline:
# Capture for variable
UUID=$(sudo blkid -s UUID -o value /dev/nvme0n1p1)
echo "UUID=$UUID /var/lib/libvirt/images xfs defaults 0 0"
# Inline substitution
echo "Uptime: $(ssh kvm-02 uptime)"
# Nested substitution
echo "Kernel: $(ssh kvm-02 "uname -r") on $(ssh kvm-02 hostname)"
Here Strings <<<
# Feed string to command
grep "pattern" <<< "$variable"
# Process JSON inline
jq '.name' <<< '{"name": "test"}'
The Magic: Why This Works
<(ssh host "cat file")
↓
/dev/fd/63
↓
Named pipe (FIFO)
↓
awk reads it like a file!
-
No temp file on disk
-
Streams directly through kernel buffer (~64KB)
-
Writer (ssh) and reader (awk) synchronize automatically
-
Clean, composable, Unix-philosophy
Performance Notes
-
Process substitution uses kernel pipes, not disk
-
Multiple
<()in one command run in parallel -
Large outputs may block if reader is slow (pipe buffer fills)
-
For huge files, consider
scpthen local processing