Everything here assumes bash/zsh/fish-compatible shell and Neovim or Vim 8+. Commands that differ between the two are noted explicitly.
Vim is not just an interactive editor. It can run non-interactively as a scripting engine.
# 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
# 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
-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
# 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
:%!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
: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)"
: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 % -
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
" 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"
" 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
: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
" 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\ %
[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'
# 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/
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
# 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
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
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.).
The shell and many programs respect $EDITOR (and $VISUAL for full-screen editors). Set it once:
export EDITOR='nvim'
export VISUAL='nvim'
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 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
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 # opens crontab in $EDITOR
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'
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
Install: pacman -S moreutils / apt install moreutils / brew install moreutils
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
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
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
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
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
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
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'
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
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
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.
# 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
# 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
# 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
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
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
| 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) |
\v, \ze, \zs):r !xxd, edit hex, :%!xxd -r)\1BEGIN / END blocks with simple accumulation