Vim’s regular expression engine is powerful enough to make you forget about your shell’s grep or sed, but its syntax is a peculiar beast.
Let’s see what this looks like in practice. Imagine you have a file with a bunch of lines like this:
user_id:12345,status:active,timestamp:1678886400
user_id:67890,status:inactive,timestamp:1678887000
user_id:11223,status:active,timestamp:1678887600
You want to change all instances of status:active to status:enabled. In Vim, this is done with the :substitute command, often shortened to :s.
:s/status:active/status:enabled/
This command tells Vim: "On the current line, substitute the first occurrence of status:active with status:enabled."
To do this for all occurrences on the current line, you add the g (global) flag:
:s/status:active/status:enabled/g
Now, what if you want to apply this to the entire file? You’d preface the command with a range. :.,$ means "from the current line (.) to the last line ($)".
:.,$s/status:active/status:enabled/g
Or, more commonly, you can just use % as a shorthand for the entire file:
:%s/status:active/status:enabled/g
This is the fundamental Vim regex search and substitute syntax: [range]s/pattern/replacement/[flags].
The pattern is where the real power lies. Vim’s regex flavor is a bit different from Perl-compatible regular expressions (PCRE) you might be used to. It’s closer to POSIX Extended Regular Expressions (ERE) but with its own quirks.
For instance, to match a literal dot (.), which normally means "any character," you need to escape it: \..
Let’s say you have lines like config.file.backup and you want to change them to config_file_backup.
:%s/config\.file\.backup/config_file_backup/g
The replacement string has its own special characters. & in the replacement string represents the entire matched pattern. If you wanted to wrap all status:active entries with parentheses, you’d do:
:%s/status:active/(&)/g
This would turn status:active into (status:active).
You can also use captured groups. Parentheses () in the pattern create capture groups. You can refer to these groups in the replacement string using \1, \2, etc., for the first, second, and subsequent groups.
Suppose you have lines like user_id:12345 and you want to extract just the ID. You could use:
:%s/user_id:\(\d\+\)/ID:\1/g
Here, \(\d\+\) captures one or more digits (\d\+) into group 1. The replacement ID:\1 then uses that captured digit sequence.
Vim’s regex also has special characters for matching word boundaries (\< and \>), start of line (^), end of line ($), and more.
To match the word "the" but not "there" or "their", you’d use:
:%s/\<the\>/THE/g
The \< matches the beginning of a word, and \> matches the end of a word.
One of the most powerful, yet often overlooked, aspects of Vim’s substitution is its ability to prompt for confirmation for each change. If you omit the g flag, it only replaces the first instance per line and prompts you. To get confirmation for every substitution, you use the c flag:
:%s/status:active/status:enabled/gc
When you run this, Vim will show you each match and ask replace with 'status:enabled' (y/n/a/q/l/^E/^Y)?. Pressing y replaces it and moves to the next, n skips it, a replaces all remaining without asking, q quits the substitute command, l replaces this one and quits, ^E scrolls up, and ^Y scrolls down. This c flag is invaluable for complex or potentially destructive substitutions.
Many users struggle with escaping characters. For example, if your search pattern itself contains a forward slash /, you need to escape it. So, to replace /usr/local with /opt/local, you’d write:
:%s/\/usr\/local/\/opt\/local/g
Alternatively, you can use a different delimiter for the s command, which is often cleaner. Any character can be used as a delimiter after s. Common alternatives are #, |, or :.
:%s#/usr/local#/opt/local#g
This avoids the need to escape the slashes within the paths themselves.
The \v (very magic) modifier can also simplify patterns by making most characters non-special. For example, \v(foo|bar) is equivalent to \(foo\|bar\).
:%s/\v(user_id:\d+,)status:active/\1status:enabled/g
This captures user_id:12345, into group 1 and then replaces status:active while keeping the captured part.
The next hurdle most users face is understanding Vim’s different regex "modes" and how they interact with the \v modifier.