~kris/dots

srice

ref: 9f828eb14bdd54d2c4fd8a3b2c90253021df3152 srice/doc/editor-shell.md -rw-r--r-- 23.6 KiB
9f828eb1 — Kris Yotam mksh: backslash-escape commands in history hook to bypass module aliases (wc=tokei, tr=transmission-remote, cat=bat) 2 months ago

#Editor-Shell Integration Reference

Everything here assumes bash/zsh/fish-compatible shell and Neovim or Vim 8+. Commands that differ between the two are noted explicitly.


#1. Vim as a CLI Tool

Vim is not just an interactive editor. It can run non-interactively as a scripting engine.

#Read from stdin

# Open stdin in vim
cat file.txt | vim -

# Pipe, edit interactively, pipe out
cat file.txt | vim - -c 'v/ERROR/d' -c 'w! /dev/stdout' -c 'q!' > filtered.txt

#Run a command and quit (-c)

# Open file, jump to pattern, quit
vim -c '/TODO' file.txt

# Open file, run substitution, save, quit
vim -c '%s/foo/bar/g' -c 'wq' file.txt

# Chain multiple -c flags
vim -c 'set ft=json' -c '%!python3 -m json.tool' -c 'wq' ugly.json

#Silent ex mode (-es / -E)

-es is the batch scripting mode. No terminal interaction, no swap files, no output noise. Use it when you need vim's regex engine or ex commands in a script.

# In-place substitution without sed
vim -es -c '%s/OldClass/NewClass/g' -c 'wq' src/main.py

# Delete lines matching a pattern
vim -es -c 'g/^#.*debug/d' -c 'wq' config.ini

# Run an ex script file
vim -es -S fix.vim file.txt

# fix.vim contents:
# %s/localhost/prod.example.com/g
# wq

-E is improved ex mode (supports some features -es does not, like lookahead in patterns).

vim -E -s file.txt <<'EOF'
%s/\v(\w+)_id/id_\1/g
wq
EOF

#Neovim headless

# Run lua script headlessly
nvim --headless -c 'lua require("myscript").run()' -c 'qa!'

# Run a vimscript command and exit
nvim --headless +'%s/foo/bar/g | wq' file.txt

# Run with no config
nvim --headless --noplugin -u NONE -c 'wq' file.txt

# Pipe output from headless nvim
nvim --headless -c 'echo "hello"' -c 'qa!' 2>&1

#2. Vim Filter Commands (:%!)

:%!cmd replaces the entire buffer with the output of cmd, passing the current buffer as stdin. This turns any Unix tool into a vim transformation.

" Format JSON
:%!python3 -m json.tool
:%!jq .

" Format JSON with sorted keys
:%!jq --sort-keys .

" Sort buffer
:%!sort

" Sort and deduplicate
:%!sort -u

" Align columns
:%!column -t
:%!column -t -s ','       " comma-separated

" Format XML
:%!xmllint --format -

" Strip ANSI escape codes
:%!sed 's/\x1b\[[0-9;]*m//g'

" Count words (replaces buffer with wc output -- use :r ! instead for insert)
:%!wc -w

" Alphabetize CSS properties
:%!sort -t: -k1

" Base64 encode
:%!base64

" Base64 decode
:%!base64 -d

" Hex dump
:%!xxd

" Revert hex dump
:%!xxd -r

" Run buffer as shell script, replace with output
:%!bash

" Wrap long lines at 72 chars
:%!fold -s -w 72
:%!fmt -w 72

#3. :r !command (Insert Command Output)

:r !cmd inserts the output of cmd below the cursor. The buffer is not replaced; the output is appended at the cursor position.

" Insert current date
:r !date

" Insert formatted date
:r !date '+%Y-%m-%d'

" Insert ls output
:r !ls -la

" Insert file contents
:r otherfile.txt

" Insert command output at top of file
:0r !date

" Insert git log
:r !git log --oneline -20

" Insert JSON from API
:r !curl -s https://api.example.com/data | jq .

" Insert UUID
:r !uuidgen

" Insert contents of clipboard (Linux)
:r !xclip -o -selection clipboard
:r !wl-paste          " Wayland

" Insert Python expression result
:r !python3 -c "print(2**32)"

#4. :w !command (Pipe Buffer to External Command)

:w !cmd sends the buffer (or a range) to cmd as stdin. The buffer is NOT modified.

" Count words in buffer
:w !wc -w

" Copy buffer to clipboard (X11)
:w !xclip -selection clipboard

" Copy buffer to clipboard (Wayland)
:w !wl-copy

" Copy buffer to clipboard (macOS)
:w !pbcopy

" Send buffer to less for paging
:w !less

" Pipe to a formatter but don't overwrite
:w !python3 -m json.tool

" Email buffer
:w !mail -s "subject" you@example.com

" Pipe to shell and execute
:w !bash

" Count lines in current range
:1,10w !wc -l

" Diff buffer against saved file
:w !diff % -

#5. Visual Select + ! (Filter Selection Through Command)

Select lines in visual mode, then !cmd filters only those lines through the command. The selection is replaced with the output.

# In normal mode: select a range with V (linewise) then press !
" Sort selected lines
V{motion}!sort

" Sort selected lines numerically
V{motion}!sort -n

" Sort and reverse
V{motion}!sort -r

" Remove duplicates from selection
V{motion}!sort -u

" Filter selection through awk
V{motion}!awk '{print $2, $1}'

" Convert selection to uppercase
V{motion}!tr 'a-z' 'A-Z'

" Evaluate math in selection (one expression per line)
V{motion}!bc

" Align selection as table
V{motion}!column -t

" Strip trailing whitespace from selection
V{motion}!sed 's/[[:space:]]*$//'

" Number lines in selection
V{motion}!cat -n

" Reverse selected lines
V{motion}!tac

" Wrap selection at 80 chars
V{motion}!fmt -w 80

" Run selection as Python, replace with output
V{motion}!python3

Normal-mode shortcut: !!cmd filters the current line through cmd.

" Evaluate current line as math
!!bc

" Format current line as JSON
!!python3 -m json.tool

#6. Quickfix Integration

#Configure grep program

" Use ripgrep as grepprg
set grepprg=rg\ --vimgrep\ --smart-case
set grepformat=%f:%l:%c:%m

" Or ag (silver searcher)
set grepprg=ag\ --vimgrep

In Neovim config (init.lua):

vim.opt.grepprg = "rg --vimgrep --smart-case"
vim.opt.grepformat = "%f:%l:%c:%m"

#:grep usage

" Search for pattern, populate quickfix
:grep 'TODO' **/*.py

" Search word under cursor
:grep <cword>

" Search and open quickfix immediately
:grep 'TODO' **/*.py | copen

" Silent grep (suppress shell output)
:silent grep 'TODO' **/*.py | copen

" Grep in specific files
:grep 'MyClass' src/**/*.ts
:copen        " open quickfix window
:cclose       " close quickfix window
:cnext        " next item
:cprev        " previous item
:cfirst       " first item
:clast        " last item
:cc N         " jump to item N

#Project-wide find and replace with :cdo / :cfdo

:cdo runs a command on each quickfix entry (each matching line). :cfdo runs a command on each file in the quickfix list.

" Find all occurrences of 'OldName'
:grep 'OldName' **/*.py

" Replace in every matched line
:cdo s/OldName/NewName/g | update

" Replace in every matched file (more efficient)
:cfdo %s/OldName/NewName/g | update

" Delete all lines containing pattern
:cdo d | update

" After substitution, write all changed files
:cfdo update

" Full workflow: rename function across project
:grep 'old_function' **/*.py | copen
:cfdo %s/old_function/new_function/g | update

#:make integration

" Set makeprg to any build/lint command
set makeprg=python3\ -m\ mypy\ %

" Run and populate quickfix
:make

" Run with args
:make src/

" Common makeprg values
set makeprg=make
set makeprg=cargo\ build
set makeprg=go\ build\ ./...
set makeprg=pylint\ %
set makeprg=eslint\ --format\ compact\ %

#7. vimdiff as Git Merge / Diff Tool

#Setup in ~/.gitconfig

[diff]
    tool = vimdiff

[merge]
    tool = vimdiff
    conflictstyle = diff3

[difftool]
    prompt = false

[mergetool "vimdiff"]
    cmd = vim -d $LOCAL $REMOTE $MERGED -c '$wincmd w' -c 'wincmd J'

For Neovim:

[diff]
    tool = nvimdiff

[merge]
    tool = nvimdiff

[difftool "nvimdiff"]
    cmd = nvim -d $LOCAL $REMOTE $MERGED -c '$wincmd w' -c 'wincmd J'

[mergetool "nvimdiff"]
    cmd = nvim -d $LOCAL $REMOTE $MERGED -c '$wincmd w' -c 'wincmd J'

#Invoke from git

# Diff working tree against HEAD
git difftool file.txt

# Diff two commits
git difftool HEAD~2 HEAD -- file.txt

# Resolve merge conflicts
git mergetool

# Diff two branches
git difftool main..feature -- src/

#vimdiff commands

During a merge, vimdiff opens three or four panes. Standard layout: LOCAL (your changes) | BASE (common ancestor) | REMOTE (their changes), with MERGED at the bottom.

]c          " jump to next conflict/diff
[c          " jump to previous conflict/diff

do          " diff obtain (pull from other pane into current)
dp          " diff put (push from current into other pane)

" In 3-way merge, specify which buffer to get from:
:diffget RE   " get from REMOTE
:diffget BA   " get from BASE
:diffget LO   " get from LOCAL

" Or use buffer numbers (check :ls)
:diffget 2
:diffget 3

" After resolving, update diff highlights
:diffupdate

" Turn off diff mode
:diffoff
:diffoff!     " turn off in all windows

" Toggle diff mode
:diffthis

Workflow:

git mergetool            # opens vimdiff for each conflict
# resolve each hunk with :diffget LO / RE / BA
# :wqa to close and mark resolved
git commit               # finalize merge

#8. Neovim Remote

#Start Neovim with a named pipe server

# Named pipe (recommended)
nvim --listen /tmp/nvim-server.sock

# TCP socket
nvim --listen 127.0.0.1:6666

# Use NVIM_LISTEN_ADDRESS (legacy, still works)
NVIM_LISTEN_ADDRESS=/tmp/nvim.sock nvim

#nvr (neovim-remote)

Install: pip install neovim-remote

# Open file in existing nvim instance
nvr --servername /tmp/nvim-server.sock file.txt

# Open in new tab
nvr --servername /tmp/nvim-server.sock --remote-tab file.txt

# Open in split
nvr --servername /tmp/nvim-server.sock --remote-split file.txt

# Send ex command to running nvim
nvr --servername /tmp/nvim-server.sock --remote-send ':wq<CR>'

# Evaluate expression
nvr --servername /tmp/nvim-server.sock --remote-expr 'expand("%")'

# Use NVIM_LISTEN_ADDRESS so you don't have to pass --servername
export NVIM_LISTEN_ADDRESS=/tmp/nvim-server.sock
nvr file.txt

#$EDITOR patterns with nvr

Inside a running Neovim terminal, $EDITOR and $VISUAL can be set to open files in the parent Neovim instance rather than a nested one:

# In shell config, detect if inside nvim terminal
if [ -n "$NVIM" ]; then
    export EDITOR='nvr --remote-wait'
    export VISUAL='nvr --remote-tab-wait'
else
    export EDITOR='nvim'
    export VISUAL='nvim'
fi

With --remote-wait, the shell blocks until you close the buffer in the parent Neovim (useful for git commit, fc, etc.).


#9. $EDITOR Integration

The shell and many programs respect $EDITOR (and $VISUAL for full-screen editors). Set it once:

export EDITOR='nvim'
export VISUAL='nvim'

#git commit

git commit          # opens $EDITOR with commit message template
git commit --amend  # re-open last commit message

Vim tip: :set textwidth=72 in your gitcommit filetype config to auto-wrap.

" ~/.config/nvim/after/ftplugin/gitcommit.vim
setlocal textwidth=72
setlocal spell

#fc (fix command)

fc opens the last shell command in $EDITOR for editing. When you save and quit, the edited command runs.

# Fix last command
fc

# Fix a specific history entry
fc 42

# Fix the last command matching a pattern
fc grep

#Ctrl-x Ctrl-e (readline)

In bash (and zsh with the right binding), pressing Ctrl-x Ctrl-e opens the current command line in $EDITOR. Edit it, save, quit, and it runs.

# Enable in zsh if not already
autoload -U edit-command-line
zle -N edit-command-line
bindkey '^x^e' edit-command-line

This is invaluable for long pipelines. Start typing in the terminal, hit Ctrl-x Ctrl-e, add logic in vim, run.

#crontab -e

crontab -e      # opens crontab in $EDITOR

#sudoedit / sudo -e

sudoedit opens a file with your own $EDITOR (not root's), applies changes as root when you save. Safer than sudo vim because the editor process itself does not run as root.

sudoedit /etc/nginx/nginx.conf
sudo -e /etc/hosts

# sudoedit respects $EDITOR, $VISUAL, and $SUDO_EDITOR
export SUDO_EDITOR='nvim'

#Other programs

git rebase -i HEAD~5   # interactive rebase in $EDITOR
git add -p             # e to open hunk in $EDITOR
kubectl edit deploy    # edit live k8s resource
EDITOR='vim' visudo    # edit sudoers safely
vipw                   # edit /etc/passwd safely
vigr                   # edit /etc/group safely

#10. moreutils Suite

Install: pacman -S moreutils / apt install moreutils / brew install moreutils

#sponge

Reads all of stdin before writing to the output file. Solves the classic problem where cmd file > file truncates the file before cmd reads it.

# WRONG: truncates file before sort reads it
sort file.txt > file.txt

# CORRECT with sponge
sort file.txt | sponge file.txt

# In-place JSON formatting
cat data.json | jq . | sponge data.json

# In-place deduplication
sort -u file.txt | sponge file.txt

# Combine with sed for in-place editing without -i portability issues
sed 's/old/new/g' file.txt | sponge file.txt

#vipe

Inserts an interactive editor into a pipeline. Stdin is loaded into $EDITOR, you edit it, and when you save+quit the result continues downstream.

# Review/edit data mid-pipeline
curl -s api.example.com/data | vipe | jq '.results[]'

# Edit a commit message in a script
echo "initial message" | vipe | git commit -F -

# Review and trim a list before processing
cat urls.txt | vipe | xargs wget

# Edit JSON before posting
cat payload.json | vipe | curl -X POST -d @- api.example.com/endpoint

#vidir

Edit a directory listing in your editor. Rename, delete, and reorder files by editing the text. Each line is a file. Delete a line to delete the file. Edit a name to rename.

# Edit all files in current directory
vidir

# Edit a specific directory
vidir /path/to/dir

# Edit a filtered subset
ls *.jpg | vidir -

# Rename files matching a pattern
find . -name '*.jpeg' | vidir -
# In editor: change .jpeg to .jpg on each line, save and quit

#ts (timestamp)

Prepends timestamps to each line of stdin.

# Timestamp every line
tail -f /var/log/app.log | ts

# Custom format (strftime)
tail -f app.log | ts '%Y-%m-%d %H:%M:%S'

# Relative timestamps (time since last line)
tail -f app.log | ts -s

# Incremental timestamps (time since start)
tail -f app.log | ts -i

# Timestamp output of a long command
./long-running-script.sh 2>&1 | ts '[%H:%M:%S]' | tee run.log

#chronic

Runs a command silently. Only prints output (stdout + stderr combined) if the command exits with a non-zero status. Ideal for cron jobs.

# Silent backup -- only emails if it fails
chronic rsync -av /home /mnt/backup

# Silent cleanup
chronic find /tmp -mtime +7 -delete

# In crontab:
# 0 2 * * * chronic /usr/local/bin/backup.sh

#ifne (if not empty)

Runs a command only if stdin is not empty. Useful in pipelines where downstream processing should be skipped on empty input.

# Only send email if there are errors
grep ERROR app.log | ifne mail -s "Errors found" admin@example.com

# Only process file if it has content
cat queue.txt | ifne ./process-batch.sh

# Only restart service if config changed
diff new.conf current.conf | ifne systemctl reload myservice

#pee

Tees stdin to multiple commands, collecting all their outputs. Like tee but runs commands instead of files.

# Send stdin to multiple commands
echo "hello world" | pee 'wc -w' 'tr a-z A-Z' 'rev'

# Log and process simultaneously
cat data.txt | pee 'tee archive.txt' 'awk {sum+=$1} END{print sum}'

# Multiple formatters
cat file.json | pee 'jq .name' 'jq .version' 'jq .author'

#combine

Combines two files line by line using set operations.

# Lines in both files (intersection)
combine file1.txt and file2.txt

# Lines in file1 not in file2 (difference)
combine file1.txt not file2.txt

# Lines in either file (union)
combine file1.txt or file2.txt

# Lines in exactly one file (symmetric difference)
combine file1.txt xor file2.txt

# Find IPs in blocklist but not in whitelist
combine blocklist.txt not whitelist.txt

#errno

Look up errno values by name or number.

# Look up by number
errno 2
# ENOENT 2 No such file or directory

# Look up by name
errno EACCES
# EACCES 13 Permission denied

# List all
errno -l

# Useful when debugging strace output
strace ./binary 2>&1 | grep 'ENOENT'
errno ENOENT

#11. ed Scripts for Automated Editing

ed is the original Unix editor. It reads commands from stdin, making it trivially scriptable without temp files or the portability issues of sed -i.

#Basic ed invocation

# Edit file with commands from heredoc
ed file.txt <<'EOF'
commands
EOF

# Edit file with commands from a script file
ed file.txt < script.ed

# Suppress output (ed prints line counts by default)
ed -s file.txt <<'EOF'
commands
EOF

#Common ed patterns

# Uncomment lines (remove leading #)
ed -s config.ini <<'EOF'
g/^#/s/^#//
w
q
EOF

# Insert line after pattern
ed -s file.txt <<'EOF'
/^Host example/a
    IdentityFile ~/.ssh/special_key
.
w
q
EOF

# Delete all lines matching pattern
ed -s file.txt <<'EOF'
g/^#/d
w
q
EOF

# Delete blank lines
ed -s file.txt <<'EOF'
g/^$/d
w
q
EOF

# Replace on lines matching a pattern
ed -s file.txt <<'EOF'
g/TODO/s/TODO/DONE/g
w
q
EOF

# Append text at end of file
ed -s file.txt <<'EOF'
$a
# added by script
extra_option = true
.
w
q
EOF

# Insert text before first line
ed -s file.txt <<'EOF'
0a
#!/usr/bin/env bash
.
w
q
EOF

# Print lines 5 through 10 (read-only)
ed -s file.txt <<'EOF'
5,10p
q
EOF

#Pipe ed commands

# Via printf (avoids heredoc quoting issues with variables)
printf 'g/old/s/old/%s/g\nw\nq\n' "$NEW_VALUE" | ed -s file.txt

# In a loop over multiple files
for f in *.conf; do
    ed -s "$f" <<'EOF'
g/^#.*debug/d
w
q
EOF
done

#12. perl -pie One-liners

perl -pie 'expr' file is in-place editing with a backup. -p prints each line, -i edits in place, -e executes the expression. Drop the backup extension from -i to skip backups: perl -pi -e.

# Basic substitution (like sed -i)
perl -pi -e 's/old/new/g' file.txt

# With backup
perl -pi.bak -e 's/old/new/g' file.txt

# Case-insensitive substitution
perl -pi -e 's/old/new/gi' file.txt

# Delete lines matching pattern
perl -ni -e 'print unless /^#/' file.txt

# Print only matching lines (like grep)
perl -ne 'print if /pattern/' file.txt

# Conditional substitution (only on lines matching a pattern)
perl -pi -e 's/foo/bar/ if /baz/' file.txt

# Multiline match (slurp whole file)
perl -0777 -pi -e 's/start.*?end/replacement/s' file.txt

# Multiline: replace across lines
perl -0777 -pi -e 's/\[section\]\n.*?\n\n/[new_section]\noption=value\n\n/s' file.txt

# Field extraction (like awk, but with full Perl)
perl -ane 'print "$F[0] $F[2]\n"' file.txt

# Field extraction with custom separator
perl -F: -ane 'print "$F[0]\n"' /etc/passwd

# Insert line after pattern
perl -pi -e 'print "new line\n" if /pattern/' file.txt

# Append to end of file
perl -pi -e '$_ .= "appended\n" if eof' file.txt

# Number lines
perl -ne 'printf "%4d %s", $., $_' file.txt

# Remove duplicate adjacent lines (like uniq)
perl -ne 'print unless $_ eq $prev; $prev = $_' file.txt

# Strip trailing whitespace
perl -pi -e 's/[[:space:]]+$/\n/' file.txt

# Extract captures
perl -ne 'print "$1\n" if /name="(\w+)"/' file.html

# Multiple files, in-place
perl -pi -e 's/v1\.0/v2\.0/g' *.md

# Apply only to lines in range
perl -pi -e 's/foo/bar/ if 10 .. 20' file.txt

# Use BEGIN/END blocks
perl -ne 'BEGIN { $sum = 0 } $sum += $_ } END { print "$sum\n"' numbers.txt
# (correct form:)
perl -ne '$sum += $_; END { print "$sum\n" }' numbers.txt

#13. envsubst for Template Expansion

envsubst replaces $VARIABLE and ${VARIABLE} references in text with their values from the environment. Part of GNU gettext.

# Substitute all env vars in a template
envsubst < template.conf > output.conf

# Substitute only specific variables (prevents clobbering unrelated $vars)
envsubst '$HOST $PORT' < template.conf > output.conf

# Inline with echo
export NAME="world"
echo 'Hello $NAME' | envsubst

# In a deployment script
export DB_HOST=db.prod.example.com
export DB_PORT=5432
export APP_PORT=8080
envsubst '$DB_HOST $DB_PORT $APP_PORT' < nginx.conf.template | sudo tee /etc/nginx/conf.d/app.conf

# template.conf example:
# server {
#     listen ${APP_PORT};
#     proxy_pass http://${DB_HOST}:${DB_PORT};
# }

# Kubernetes manifest templating
export IMAGE_TAG=v2.3.1
envsubst '$IMAGE_TAG' < deployment.yaml.tpl | kubectl apply -f -

# Check what variables a template uses
grep -oP '\$\{?\w+\}?' template.conf | sort -u

# Combine with heredoc for inline templates
DB_NAME=myapp envsubst <<'EOF'
CREATE DATABASE $DB_NAME;
GRANT ALL ON $DB_NAME.* TO 'appuser'@'localhost';
EOF

#14. Decision Matrix: Which Tool When

Task Best tool Notes
Find lines matching pattern grep / rg rg is faster on large trees
Find and print a field from matching lines grep -oP or awk awk if you need field arithmetic
Simple string substitution, one file sed -i or perl -pi -e perl handles edge cases better
Simple substitution, many files sed -i + glob, or perl -pi -e perl more portable across OS
Multiline match/replace perl -0777 -pi -e sed cannot do multiline
In-place edit without temp file race sponge or perl -pi -e sponge simplest for pipelines
Column/field arithmetic awk awk is purpose-built for records
Complex logic, multiple conditions awk or perl awk cleaner for columnar; perl for arbitrary
Regex with lookbehind/lookahead perl or grep -P POSIX tools do not support PCRE
Edit structured file interactively vim use :%! for formatting passes
Batch edit file with known ex commands vim -es no user interaction, scriptable
Edit file mid-pipeline interactively vipe moreutils
Rename files in bulk vidir edit names as text, save
Automated edit, match/insert/delete ed predictable, POSIX, scriptable
Template with env vars envsubst simple; no logic
Template with logic envsubst + shell, or m4, or jinja2 depends on complexity
Project-wide find and replace in vim :grep + :cfdo %s///g | update keeps quickfix for review
Deduplicate lines sort -u or awk '!seen[$0]++' awk preserves order
Format JSON jq . or python3 -m json.tool jq more powerful
Format XML xmllint --format -
Align columns column -t add -s for custom delimiter
Convert between formats awk or perl depends on format complexity
One-shot REPL-style computation bc or python3 -c bc for pure math
Add timestamps to log stream ts (moreutils)
Run command only if input not empty ifne (moreutils)
Silent cron command chronic (moreutils)
Set operations on files combine (moreutils)

#When to reach for vim over sed/awk

  • The transformation requires multiple passes over the file
  • You need vim's regex (e.g. \v, \ze, \zs)
  • You want to review changes visually before writing
  • You are already in vim
  • The file is binary (:r !xxd, edit hex, :%!xxd -r)

#When to reach for perl over sed

  • The pattern spans multiple lines
  • You need conditional logic per line
  • You need backreferences more complex than \1
  • The replacement depends on a computation
  • You want consistent behavior across Linux and macOS without GNU coreutils

#When to reach for awk over perl

  • Input is naturally record/field structured (logs, CSVs, columnar data)
  • You need BEGIN / END blocks with simple accumulation
  • The script is short and you want it readable by others unfamiliar with perl
  • You are summing, counting, or averaging fields

#When to reach for ed over sed

  • You need to insert text after a matched line (sed does not do this cleanly)
  • You need to reference line numbers relative to a match
  • You want the same script to work on any POSIX system without GNU extensions
  • You are inside a script that cannot use a heredoc trick safely with sed