Vim Search and Replace: The Ultimate Guide

In this article
- The substitute command in one line
- Search first, because replace builds on it
- Replace text on a line or across the whole file
- Pick your lines with ranges
- Confirm each change before it happens
- Control case sensitivity
- Use regex to match patterns, not just words
- Reuse matches with capture groups
- The fastest interactive replace is cgn and dot
- Vim find and replace across multiple files
- See your changes live in Neovim
- Modern tooling worth adding
- Real-world examples
- Troubleshooting
- Quick reference
- Frequently asked questions
- Fast developers are a hiring signal
- Start with one command and build from there
Every developer runs into the same chore: change one string into another, fifty times, across a file or a whole project, without missing one or breaking the ones that look similar. In most editors you reach for a dialog box. In Vim you type a short command and it's done.
Vim search and replace runs on one command, :substitute, and a handful of flags. Learn the pattern once and you can rename a variable everywhere, strip trailing whitespace, comment out a block, or reshape data in seconds.
I've spent more than twenty years writing software and running engineering teams at Full Scale, and the editors change but this skill doesn't. Even now, with AI assistants writing code beside us, a deterministic :%s across a codebase is faster and safer than asking a model to make the same mechanical edit.
It does exactly what you tell it, every time, and you can see every change.
That kind of predictability is one of the development habits that still hold up as AI writes more of the code.
This guide starts with the basics and works up to multi-file replaces, regular expressions, capture groups, and the modern tooling worth adding on top. Try each command as you read it. The muscle memory sticks faster that way.
The substitute command in one line
Vim search and replace runs on one command, :substitute (the vim substitute command), almost always shortened to :s. Here is the full shape:
:[range]s/{pattern}/{replacement}/[flags] [count]
Each part has a job:
| Part | What it does |
|---|---|
range | Which lines to act on. Leave it off and Vim only touches the current line. |
pattern | The text or regular expression to find. |
replacement | The text to put in its place. |
flags | Options that change the behavior, like g for every match or c to confirm. |
count | A number that repeats the command across that many lines. |
Make sure you're in Normal mode before you start. That's the default mode when Vim opens. If you've been typing text, press Esc first, then type the colon.
Everything else in this guide is a variation on that one line.
Search first, because replace builds on it
Replacing text is just searching with a second step. So it pays to be good at the search half.
To search forward in a file, press /, type your pattern, and press Enter. To search backward, use ? instead. Once you have a match, press n to jump to the next one and N to go back. If you want to repeat your last search, an empty // reuses it.
To search for the word under your cursor, put the cursor on it and press *. That's the fastest way to start a rename, since the word is already in your search history.
Highlighting helps you see every match at once. Turn it on with:
:set hlsearch
When the highlight gets in the way, clear it with :noh. Many developers map that to a key in their config so they don't type it all day.
The search pattern you build here is the same pattern you'll drop into substitute next.
Replace text on a line or across the whole file
This is the heart of vim search and replace. Start small: this command replaces the first match of foo on the current line with bar:
:s/foo/bar/
Add the g (global) flag to catch every match on the line, not just the first:
:s/foo/bar/g
To replace all occurrences across the entire file, add % as the range. The % means "all lines, top to bottom":
:%s/foo/bar/g
That last one is the command you'll use most. Read it out loud: substitute, every line, change foo to bar, every time.
You can also delete text by leaving the replacement empty. This wipes out every foo in the file:
:%s/foo//g
If you just searched for something with / or *, leave the pattern empty and Vim reuses your last search. So :%s//bar/g replaces whatever you last searched for with bar, which saves retyping a long pattern.
Pick your lines with ranges
The range in front of s decides where the command runs. Skip it and you're stuck on one line. Set it well and you can target exactly the section you mean.
A few ways to write a range:
:3,10s/foo/bar/g " lines 3 through 10
:.,$s/foo/bar/g " current line to the end of the file
:.,+4s/foo/bar/g " current line plus the next four
The . stands for the current line and $ stands for the last line. The + and - symbols count relative to where you are. You can also add a count after the command, so :s/foo/bar/g 5 runs the substitute on the current line and the next four.
The most practical range is the one you select by eye. Enter Visual mode with V, highlight the lines you want, then type :s/foo/bar/g. Vim fills in the range for you as '<,'>, which means "the visual selection." You only replace inside what you picked.
Confirm each change before it happens
On a big replace you don't always trust the pattern. The c flag makes Vim stop and ask at every match:
:%s/foo/bar/gc
Vim then prompts you for each one:
replace with bar (y/n/a/q/l/^E/^Y)?
The answers are worth memorizing:
yreplaces this match.nskips it and moves on.areplaces this one and all the rest without asking again.lreplaces this one, then quits.qorEscquits without touching anything else.
I reach for gc any time the pattern is even a little risky. Watching each change go by is cheaper than undoing a bad replace later.
Control case sensitivity
By default Vim cares about case. Searching for foo will not match Foo. You can change that per command or for the whole session.
For a single substitute, add the i flag to ignore case or I to force a case match:
:s/foo/bar/gi " matches foo, Foo, FOO
:s/foo/bar/gI " matches foo only
You can also pin it inside the pattern with \c for insensitive and \C for sensitive. So /Linux\c searches without regard to case no matter your settings.
To set the default for every search, put these in your config:
set ignorecase
set smartcase
Together they do the smart thing: searches ignore case until you type a capital letter, and then they respect it. smartcase only works when ignorecase is also on.
One word of warning: the flag has to be a real flag. Stick to i and I on the command, or \c and \C inside the pattern. Those are the only case controls you need.
Use regex to match patterns, not just words
The replacement half is plain text. The pattern half is a full regular expression, and that's where the power lives.
A few anchors and wildcards cover most jobs:
^matches the start of a line.$matches the end of a line..matches any single character, and.*matches any run of characters.\<and\>mark word boundaries, so you match a whole word and not a fragment.
Whole-word matching saves you constantly. Searching for gnu without boundaries also hits cygnus and magnum. Add the boundaries and it stops:
:%s/\<gnu\>/linux/g
Vim's default regex syntax makes you escape a lot of characters with backslashes. Very magic mode fixes that. Put \v at the front of the pattern and most special characters work without escaping:
:%s/\v(foo|bar)/baz/g
When your pattern itself contains a slash, like a file path, switch the delimiter to something else. Any single non-alphanumeric character works:
:s#/old/path#/new/path#g
Reuse matches with capture groups
Capture groups let the replacement reuse pieces of what you matched. Wrap part of the pattern in \( and \), then refer to it in the replacement as \1, \2, and so on.
Say you want to swap firstname lastname into lastname, firstname:
:%s/\(\w\+\) \(\w\+\)/\2, \1/
You can also change the case of what you captured. \U upcases everything that follows, \L downcases it, and \u or \l change just the next character:
:%s/\<\(\w\)/\u\1/g " capitalize the first letter of every word
The & symbol in the replacement stands for the whole match, which is handy when you want to wrap text rather than swap it. And \zs and \ze let you set where the match really starts and ends, so you can match around something without replacing it.
For the rare case where you need logic, \= runs an expression as the replacement. This renumbers matched lines using the line number:
:%s/^/\=line('.') . '. '/
The fastest interactive replace is cgn and dot
There's a slicker way to do a hand-picked replace that doesn't use :substitute at all.
Search for the word with /pattern or *. Then press cgn, which jumps to the next match, selects it, and drops you into insert mode to type the replacement. Press Esc. Now n skips to the next match and . repeats the exact change you just made.
So the rhythm is: cgn, type the new word, Esc, then tap . on each match you want to change and n to skip the ones you don't. Swap cgn for dgn and the same flow deletes the next match instead of changing it. It's the most natural find-and-replace flow in Vim once it's in your fingers, because you decide one match at a time without a confirmation prompt.
Vim find and replace across multiple files
Single-file replace is the warm-up. The real time-saver is changing something everywhere in a project at once. Vim has a few ways to do it.
The first uses the argument list. Load the files you want, then run one substitute across all of them:
:args **/*.js
:argdo %s/oldName/newName/ge | update
:args **/*.js loads every JavaScript file in the current folder and its subfolders. :argdo runs the command that follows on each one. The e flag keeps Vim quiet when a file has no match, and update saves any file that changed.
If you already have files open in buffers, :bufdo does the same across all of them:
:bufdo %s/oldName/newName/ge | update
The other approach searches first, then replaces only the hits. Vim's :vimgrep populates the quickfix list with every match:
:vimgrep /oldName/ **/*.js
:cdo s/oldName/newName/g | update
:cdo runs your substitute on each entry in the quickfix list. For big codebases, point Vim's :grep at ripgrep instead, which is much faster. Add this to your config:
if executable("rg")
set grepprg=rg\ --vimgrep
endif
One more tool worth knowing is the global command, :g. It runs any command on every line that matches a pattern. This deletes every line containing DEBUG:
:g/DEBUG/d
And :g pairs with substitute for conditional replaces, like changing x to y only on lines that also contain foo:
:g/foo/s/x/y/g
Its inverse is :v (or :g!), which runs on the lines that do not match. So :v/foo/d deletes every line that doesn't contain foo.
See your changes live in Neovim
This is the modern upgrade most older guides skip. Neovim can preview a substitute as you type it, before you press Enter. Turn it on with:
set inccommand=split
Now when you start typing :%s/foo/bar, Neovim highlights every match and shows the result in a preview window in real time. You see exactly what will change while you're still building the command. Set it to nosplit if you want the inline highlight without the extra window.
I think this is the single best reason to run Neovim over classic Vim for daily editing. Seeing a risky regex resolve live takes the guesswork out of large replaces.
Modern tooling worth adding
Vanilla Vim handles everything above with no plugins. For project-wide search and replace, a few modern tools make it nicer:
- ripgrep is a blazing-fast search tool written in Rust. It's the engine most of the others sit on top of.
- fzf is a fuzzy finder that turns searching files, buffers, and history into an interactive list.
- Telescope is the go-to fuzzy finder for Neovim, with live grep built in.
- nvim-spectre gives Neovim a dedicated search-and-replace panel for project-wide edits, with a preview of every change before you commit it.
You don't need any of these to be productive. But once you're replacing across large projects every day, they earn their place.
Real-world examples
The commands click once you see them on a real task, and the seconds they save add up to real development velocity over a week. Here are a few I run all the time.
Renaming a variable across a file is the classic refactoring move, the kind of cleanup that keeps technical debt from piling up:
:%s/\<oldVarName\>/newVarName/g
Stripping trailing whitespace from a whole file, which keeps diffs clean:
:%s/\s\+$//e
Splitting a comma-separated line onto separate lines, where \r in the replacement inserts a newline:
:%s/, /\r/g
Commenting out a block of lines by adding # to the front of lines 5 through 20:
:5,20s/^/#/
Collapsing several spellings into one, useful when cleaning up data:
:%s/\v(apple|orange|mango)/fruit/g
Troubleshooting
A few problems come up again and again.
If you get "Pattern not found" when you expected a match, the cause is usually case. Vim searches are case-sensitive by default, so add the i flag or \c to the pattern.
If the replace only changed one line, you forgot the range. A bare :s works on the current line only. Add % to cover the whole file.
If your pattern has a slash in it and Vim throws an error, either escape the slash with a backslash or switch to a different delimiter like # or |.
And if a special character like . or * is matching too much, remember those have meaning in regex. Escape them with a backslash to match them literally, so \. matches a real period.
Quick reference
| Command | What it does |
|---|---|
:s/foo/bar/ | Replace the first match on the current line |
:s/foo/bar/g | Replace every match on the current line |
:%s/foo/bar/g | Replace every match in the file |
:%s/foo/bar/gc | Replace in the whole file, confirming each one |
:3,10s/foo/bar/g | Replace in lines 3 through 10 |
:.,$s/foo/bar/g | Replace from the current line to the end |
:%s/foo//g | Delete every match in the file |
:s/\<foo\>/bar/g | Replace the whole word foo only |
:%s/foo/bar/gi | Replace ignoring case |
:argdo %s/foo/bar/ge | update | Replace across every file in the arg list |
Frequently asked questions
What is the command for search and replace in Vim?
The command is :substitute, usually shortened to :s. The most common form is :%s/old/new/g, which replaces every occurrence of old with new in the entire file. Drop the % to work on just the current line.
How do I replace all occurrences in a file in Vim?
Use :%s/old/new/g. The % tells Vim to act on every line, and the g flag tells it to replace every match on each line instead of only the first one. Without g, Vim changes just the first match per line.
How do I confirm each replacement in Vim?
Add the c flag, as in :%s/old/new/gc. Vim stops at each match and asks what to do. Press y to replace it, n to skip, a to replace all remaining matches, or q to quit.
How do I make a Vim search and replace case-insensitive?
Add the i flag, like :%s/old/new/gi, or put \c anywhere in the pattern. You can also set ignorecase and smartcase in your config so all searches ignore case until you type a capital letter.
How do I find and replace across multiple files in Vim?
Load the files into the argument list with :args **/*.js, then run :argdo %s/old/new/ge | update to replace across all of them and save. For buffers you already have open, use :bufdo the same way. The e flag prevents errors on files with no match.
How do I undo a search and replace in Vim?
Press u in Normal mode to undo the last change, including a whole substitute command. Press u again to keep stepping back through your history, and Ctrl+r to redo if you went too far.
Fast developers are a hiring signal
Tool fluency like this is a small thing on its own. But it's a decent proxy for a bigger one: a developer who automates the boring parts and thinks before they type is exactly the kind of developer worth hiring. The hard part is finding people like that at scale.
That's the whole job at Full Scale. We staff senior offshore developers from the Philippines who work as a direct extension of your team, trained on the product thinking from Product Driven and the current AI toolkit. Our staff augmentation model drops them straight into your standups, your repo, and your code reviews, with no middleman in the way.
It holds up over time, too. Our developer retention runs north of 93%, so the person who learned your codebase and your product is still there a year later instead of starting over with someone new.
Start with one command and build from there
Vim search and replace looks cryptic for about a day, and then it becomes one of the fastest editing tools you own. Learn :%s/old/new/g first. Add the g, c, and i flags. Pick up ranges, then regular expressions, then multi-file replaces when you need them. Each piece is a small addition to that same one-line pattern.
The developers who move fastest aren't the ones with the most plugins. They're the ones who know their tools cold and waste no motion, which is most of what real developer productivity comes down to. That's the same mindset we hire for and build on every team.
If you're scaling an engineering team and want developers who already work this way, we hire senior developers in the Philippines who join your team directly. Book a discovery call and we'll help you put the right people in place.



