Skip to content

Modern CLI Tools - Street Ops

Practical workflows that combine modern tools for real DevOps tasks.

Workflow 1: Searching Repos Efficiently

Find a config value across a large monorepo

# Old way (slow, noisy)
grep -rn "replicas:" --include="*.yaml" .

# Modern way (fast, filtered, contextual)
rg "replicas:" -t yaml -C 2
# Shows 2 lines of context, only YAML files, respects .gitignore

# Even better: interactive
rg "replicas:" -t yaml -l | fzf --preview 'bat --color=always -H $(rg -n "replicas:" {} | head -1 | cut -d: -f1) {}'
# Lists matching files, preview shows the file with the match highlighted

Find where a function is defined vs used

# Definition (likely has "def " or "func " prefix)
rg "def error_handler" -t py
rg "func.*ErrorHandler" -t go

# All usages
rg "error_handler" -t py -l  # Just file names
rg "error_handler" -t py -c  # Count per file

Under the hood: rg (ripgrep) is fast because it uses memory-mapped I/O, SIMD-accelerated regex (via the regex crate), and parallelism by default. It also respects .gitignore automatically, skipping node_modules/, vendor/, and build artifacts. This means rg in a large repo is often 10-50x faster than grep -r, not because of a better algorithm, but because it searches fewer files.

Find all TODOs and tech debt markers

rg "TODO|FIXME|HACK|XXX|DEPRECATED" --stats
# --stats shows total count at the end

Workflow 2: Log Triage Pipeline

Parse structured JSON logs

# Application emits JSON logs
# {"timestamp":"2024-01-15T10:23:45Z","level":"error","message":"connection refused","service":"api"}

# Filter errors only
cat app.log | jq 'select(.level == "error")'

# Count errors by service
cat app.log | jq -r 'select(.level == "error") | .service' | sort | uniq -c | sort -rn

# Extract error messages from the last hour
cat app.log | jq 'select(.level == "error" and .timestamp > "2024-01-15T09:00:00Z") | .message'

# Pretty table of recent errors
cat app.log | jq -r 'select(.level == "error") | [.timestamp, .service, .message] | @tsv' | column -t

Parse Kubernetes events

# Find all warning events, sorted by count
kubectl get events -A -o json | jq -r '.items[] | select(.type == "Warning") | [.count, .reason, .involvedObject.name, .message] | @tsv' | sort -rn | head -20

# Find recent crashloopbackoff events
kubectl get events -A -o json | jq '.items[] | select(.reason == "BackOff") | {name: .involvedObject.name, ns: .involvedObject.namespace, count, message}'

Systemd journal triage

# Errors from the last hour, service name highlighted
journalctl -p err --since "1 hour ago" --no-pager | rg "systemd|kernel|docker" --color=always

# Find the noisiest services
journalctl -p err --since today -o json | jq -r '._SYSTEMD_UNIT // "kernel"' | sort | uniq -c | sort -rn | head -10

Workflow 3: JSON Manipulation Patterns

jq Cheatsheet (Top 10 Patterns)

# 1. Pretty print
echo '{"a":1}' | jq '.'

# 2. Extract a field
kubectl get svc -o json | jq '.items[].metadata.name'

# 3. Filter by condition
kubectl get pods -o json | jq '.items[] | select(.status.phase != "Running")'

# 4. Transform to a new shape
kubectl get pods -o json | jq '.items[] | {name: .metadata.name, phase: .status.phase, restarts: .status.containerStatuses[0].restartCount}'

# 5. Count items
kubectl get pods -o json | jq '.items | length'

# 6. Sort by a field
kubectl get pods -o json | jq '.items | sort_by(.status.containerStatuses[0].restartCount) | reverse | .[0:5]'

# 7. Group by
kubectl get pods -o json | jq '[.items[] | {ns: .metadata.namespace, phase: .status.phase}] | group_by(.ns) | .[] | {namespace: .[0].ns, count: length}'

# 8. Raw output for scripting
kubectl get pods -o json | jq -r '.items[].metadata.name'

# 9. Multiple outputs per input
kubectl get pods -o json | jq -r '.items[] | "\(.metadata.name)\t\(.status.phase)"'

# 10. Slurp + combine
cat file1.json file2.json | jq -s '.[0] * .[1]'  # Merge objects

yq for YAML (same syntax as jq)

# Read a value
yq '.metadata.name' deployment.yaml

# Update a value
yq '.spec.replicas = 3' deployment.yaml

# Add an annotation
yq '.metadata.annotations.updated = "2024-01-15"' deployment.yaml

# Merge YAML files
yq '. *= load("overrides.yaml")' base.yaml

# Convert YAML to JSON
yq -o json deployment.yaml

Gotcha: There are two different tools both called yq: the Go version (mikefarah/yq, jq-like syntax) and the Python version (kislyuk/yq, wraps jq). They have incompatible syntax. The Go version is more common in DevOps workflows. Check which you have with yq --version. If recipes from the internet do not work, you likely have the wrong yq.

Workflow 4: Interactive Selection

Choose a pod to exec into

kubectl get pods -o name | fzf | xargs -I{} kubectl exec -it {} -- /bin/sh

Choose a branch to checkout

git branch -a | fzf | xargs git checkout

Choose a docker container to follow logs

docker ps --format '{{.Names}}' | fzf | xargs docker logs -f

Choose a file to edit

fd -t f | fzf --preview 'bat --color=always {}' | xargs $EDITOR

Remember: fzf keybindings mnemonic: Ctrl-R (history), Ctrl-T (file picker), Alt-C (cd into directory). Add source <(fzf --bash) to your .bashrc to get all three. These three keybindings replace hours of manual typing per week.

Workflow 5: Replacing Legacy Combos

Legacy Modern Notes
find . -name "*.py" \| xargs grep "import" rg "import" -t py Single command, faster
grep -rl "old" . \| xargs sed -i 's/old/new/g' rg -l "old" \| xargs sed -i 's/old/new/g' rg is faster for the search phase
find . -name "*.log" -mtime +7 -delete fd -e log --changed-before 7d -x rm More readable
ls -la \| sort -k5 -rn \| head eza -la --sort size Built-in sorting
du -sh * \| sort -rh \| head -20 dust Visual bar chart
cd $(find . -type d \| fzf) zi Remembers frecency
cat file \| less bat file Syntax highlighting
diff file1 file2 delta file1 file2 Highlighted, word-level diffs

Power One-Liners

Pipe viewer for progress on any pipeline

pv access.log | gzip > access.log.gz                             # compression progress
dd if=/dev/sda bs=4M | pv -s $(blockdev --getsize64 /dev/sda) | dd of=disk.img bs=4M   # disk clone with ETA
tar cf - /data | pv -s $(du -sb /data | awk '{print $1}') | gzip > data.tgz            # tar with progress

Breakdown: pv (pipe viewer) inserts into any pipeline and shows throughput, ETA, and progress bar. -s sets expected size for percentage/ETA calculation.

[!TIP] When to use: Any long-running pipeline where you need visibility — backups, transfers, compressions.


Quick Reference