# 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 ```bash # 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) ```bash # 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. ```bash # 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). ```bash vim -E -s file.txt <<'EOF' %s/\v(\w+)_id/id_\1/g wq EOF ``` ### Neovim headless ```bash # 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. ```vim " 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. ```vim " 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. ```vim " 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. ```bash # In normal mode: select a range with V (linewise) then press ! ``` ```vim " 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`. ```vim " Evaluate current line as math !!bc " Format current line as JSON !!python3 -m json.tool ``` --- ## 6. Quickfix Integration ### Configure grep program ```vim " 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): ```lua vim.opt.grepprg = "rg --vimgrep --smart-case" vim.opt.grepformat = "%f:%l:%c:%m" ``` ### :grep usage ```vim " Search for pattern, populate quickfix :grep 'TODO' **/*.py " Search word under cursor :grep " 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 ``` ### Navigating quickfix ```vim :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. ```vim " 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 ```vim " 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 ```ini [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: ```ini [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 ```bash # 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. ```vim ]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: ```bash 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 ```bash # 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` ```bash # 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' # 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: ```bash # 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: ```bash export EDITOR='nvim' export VISUAL='nvim' ``` ### git commit ```bash 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. ```vim " ~/.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. ```bash # 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. ```bash # 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 ```bash 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. ```bash sudoedit /etc/nginx/nginx.conf sudo -e /etc/hosts # sudoedit respects $EDITOR, $VISUAL, and $SUDO_EDITOR export SUDO_EDITOR='nvim' ``` ### Other programs ```bash 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. ```bash # 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. ```bash # 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. ```bash # 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. ```bash # 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. ```bash # 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. ```bash # 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. ```bash # 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. ```bash # 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. ```bash # 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 ```bash # 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 ```bash # 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 ```bash # 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`. ```bash # 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. ```bash # 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