The :s command in Vim, often wielded for simple substitutions, is actually a tiny, incredibly powerful piece of a much larger, more fundamental pattern for manipulating text in bulk.
Let’s see it in action. Imagine you have a log file and you want to find all lines containing "ERROR" and replace them with "CRITICAL".
:g/ERROR/s/ERROR/CRITICAL/g
This single command does it. But understanding why it works, and the hidden power within, is the real game.
The core of Vim’s bulk editing lies in the :g (global) and :v (inverse global) commands. They are not just search-and-replace tools; they are filters that select lines for subsequent operations. :g selects lines that match a pattern, and :v selects lines that don’t match.
Here’s the breakdown of that command:
:g/ERROR/: This is the filter. It finds every line in the file that contains the string "ERROR". For each line it finds, it then executes the command that follows.s/ERROR/CRITICAL/g: This is the command executed on each filtered line. It’s the standard Vim substitute command:sfor substitute,/ERROR/for the pattern to find,/CRITICAL/for the replacement string, and/gfor global on that specific line (meaning replace all occurrences of "ERROR" on that line, not just the first).
So, :g/ERROR/s/ERROR/CRITICAL/g is read as: "Globally, on all lines matching 'ERROR', substitute 'ERROR' with 'CRITICAL', globally on that line."
This pattern, :g/<pattern>/<command>, is the bedrock of Vim’s batch editing. You can substitute :s with almost any other Ex command.
Want to delete all lines containing "DEBUG"?
:g/DEBUG/d
Here, :d is the delete command. It’s executed only on lines matched by :g/DEBUG/.
What about marking lines with a specific word using a marker? Let’s say you want to mark all lines containing "TODO" with the marker a.
:g/TODO/normal ma
The normal command allows you to execute normal mode keystrokes within an Ex command. ma in normal mode moves the cursor to the current position and sets mark a. So this command finds each "TODO" line and places mark a at the end of that line.
The :v command is the mirror image. Suppose you want to keep only the lines that contain "WARNING" and delete everything else.
:v/WARNING/d
This reads: "On all lines not matching 'WARNING', execute the delete command (d)."
You can also combine :g and :v for more complex filtering. Imagine you want to find lines containing "ERROR" but only if they don’t also contain the word "database".
:g/ERROR/v/database/s/ERROR/CRITICAL/g
This is powerful: "Globally, on lines matching 'ERROR', and also on lines not matching 'database', substitute 'ERROR' with 'CRITICAL'."
The true power of :g and :v is that they operate on the entire buffer by default but can be restricted to a range. For instance, to perform the substitution only on lines 100 through 200:
:100,200g/ERROR/s/ERROR/CRITICAL/g
This opens up immense possibilities for manipulating large files efficiently.
The one thing that trips most people up is not realizing that :s by itself is an Ex command that operates on the current line (or a specified range), but when prefixed by :g or :v, it becomes an operation applied to a set of lines identified by the filter. The g flag at the end of s/old/new/g applies to occurrences within a single line, while the :g prefix applies the s command to multiple lines in the buffer. They are distinct concepts with similar-looking syntax.
Once you’ve mastered :g and :v, the next logical step is to explore how to use them with more advanced patterns, like regular expressions that span multiple lines or how to chain multiple Ex commands together using |.