vi on Linux — theory with the three modes, commands that combine like words and :%s (and the day I couldn’t get out of the editor)
My first session in the networks lab. The task said something as simple as “set ONBOOT=yes in the card’s file and save”. I opened the file with vi, started typing ONBOOT=yes… and at the very top a new line appeared saying NBOOT=yes. I tried to leave with :q and the screen answered something like this (I reproduced it later, cleanly):
ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ ~ E37: No write since last change (add ! to override) Press ENTER or type command to continue
It wouldn’t save, it wouldn’t quit, and I had no idea why. I ended up closing the whole terminal. The lesson that day: vi isn’t a weird editor, it’s an editor with modes, and once you know which mode you’re in, everything clicks.
In the previous article we wrote scripts that automate the terminal. Today it’s the editor you’ll find on any Linux machine, even one with no mouse and no windows: vi. We’ll use a real lab file, ifcfg-enp2s0, the configuration of a network card. When you finish, the vi practice will get your fingers moving.
Table of Contents
Why vi (and why vim)
In the IC2 lab you work on CentOS machines through the terminal: no gedit, no Notepad. But vi is always there: it’s part of the POSIX standard, so every Unix ships it. What opens today when you type vi is usually vim (“vi improved”), which adds unlimited undo, highlighting and clearer messages, but keeps every vi command.
Everything in this article is real: I typed each sequence into vim 9.1 inside a 72-column terminal, with vim’s default settings, and copied the screen exactly as it was left. This is the file we’ll work with:
$ cat -n ifcfg-enp2s0
1 DEVICE=enp2s0
2 BOOTPROTO=dhcp
3 ONBOOT=no
4 IPADDR=192.168.2.27
5 NETMASK=255.255.255.0
6 GATEWAY=192.168.2.1
Six lines in KEY=value format. It has two things that need fixing (dhcp and ONBOOT=no), but for now we’ll just learn to move around it.
cat -n shows the file with each line’s number. I use it before and after every test so you can see exactly what changed on disk, not just on vi’s screen.
The three modes of vi
This is the concept that changes everything. vi has three modes and every key means something different depending on the mode you’re in:
Look at the drawing: normal mode sits in the middle and Esc always takes you back to it. If you ever get lost, press Esc (twice does no harm) and you know where you are.
| Mode | What it’s for | How you enter | What you see at the bottom |
|---|---|---|---|
| Normal | moving, deleting, copying, pasting | when vi opens, or with Esc | nothing, or the last message |
| Insert | typing text | i, a, o, A, O… | -- INSERT -- |
| Command-line | saving, quitting, substituting | : from normal mode | whatever you type after the colon |
Let’s go through all three with this key file. The notation is the one in vim’s documentation: <Esc> is the Escape key and <CR> is Enter.
# Just opened: normal mode jjA # down two lines and enter insert mode at the end of the line <Esc>:wq # back to normal and start a command (without pressing Enter) <Esc>:q<CR> # cancel the command and quit: we had changed nothing
Right after opening, vi is in normal mode and the last line tells you which file it loaded:
DEVICE=enp2s0 BOOTPROTO=dhcp ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ "ifcfg-enp2s0" 6L, 101B 1,1 All
6L, 101B means 6 lines and 101 bytes. The ~ at the end aren’t part of the file: they mark “empty” screen lines past the end. Bottom right, 1,1 is the cursor position (line 1, column 1) and All says the whole file fits on screen.
After jjA (two lines down and A = append at the end of the line) we’re in insert mode:
DEVICE=enp2s0 BOOTPROTO=dhcp ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ -- INSERT -- 3,10 All
-- INSERT -- is your best friend: if you see it, whatever you type goes into the file. Position 3,10 is right after ONBOOT=no (9 characters, so column 10 is the next one).
Finally, Esc takes us back to normal mode and : opens command-line mode. What you type shows up on the last line, not yet run:
DEVICE=enp2s0 BOOTPROTO=dhcp ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ :wq
If we pressed Enter it would save and quit. Instead, Esc cancels the command and :q quits without touching anything (we hadn’t changed a single character, so vi doesn’t complain). The file is the same:
$ cat -n ifcfg-enp2s0
1 DEVICE=enp2s0
2 BOOTPROTO=dhcp
3 ONBOOT=no
4 IPADDR=192.168.2.27
5 NETMASK=255.255.255.0
6 GATEWAY=192.168.2.1
Starting to type as soon as vi opens. You’re in normal mode and every letter is a command: d, x, o, u… do things. Before typing text, press i and look for -- INSERT --.
Moving without a mouse
In normal mode you move with the keyboard. The four basics are h (left), j (down), k (up) and l (right): they sit on the home row so your hand never moves. Arrow keys also work in vim, but the big jumps are what save you time:
| Key | Where it goes |
|---|---|
h j k l | one character left, one line down, one line up, one character right |
w / b | to the start of the next / previous word |
0 / $ | to the start / end of the line |
f= | to the next = on the line (works with any character: f., fy…) |
gg / G | to the first / last line |
5G | to line 5 |
3j | three lines down (a number in front repeats the motion) |
/text + Enter | to the next occurrence of text |
n / N | to the next / previous match of the last search |
I typed this route and noted, after each key, the position vim showed in the corner (the “ruler”, line,column):
j # one line down $ # to the end of the line 0 # to the start of the line f= # jump to the next = on the line w # to the next word G # to the last line gg # to the first line 3j # three lines down /255<CR> # search forward for 255 n # next match :q<CR>
This is the real trace, with the character under the cursor:
| Step | Key | Position | Character | Why |
|---|---|---|---|---|
| 1 | j | 2,1 | B | one line down, same column |
| 2 | $ | 2,14 | p | BOOTPROTO=dhcp has 14 characters |
| 3 | 0 | 2,1 | B | back to the start |
| 4 | f= | 2,10 | = | BOOTPROTO has 9 letters: the = is number 10 |
| 5 | w | 2,11 | d | to vi, = and dhcp are separate “words” |
| 6 | G | 6,1 | G | last line (the G of GATEWAY) |
| 7 | gg | 1,1 | D | first line |
| 8 | 3j | 4,1 | I | 1 + 3 = 4 |
| 9 | /255 Enter | 5,9 | 2 | NETMASK= takes 8 characters |
| 10 | n | 5,13 | 2 | the second 255: 9 + 4 (255.) = 13 |
And here it is drawn over the file:
Look at step 5: w doesn’t jump to the next line but from = to dhcp, because punctuation counts as its own word. And in step 10, n repeats the search without typing it again.
/ searches forward and ? backward. If the search hits the end, vim wraps around and carries on from the top (it tells you with search hit BOTTOM, continuing at TOP).
Changing text: i, a, o, x and cw
There are several doors into insert mode. They all do the same thing (let you type); what changes is where you start:
| Key | You start typing… |
|---|---|
i / a | before / after the cursor |
I / A | at the start / end of the line |
o / O | on a new line below / above |
cw | in place of the word (deletes it and enters insert mode) |
cc | in place of the whole line |
And for small fixes without entering insert mode: x deletes the character under the cursor and r followed by a letter replaces it (rY turns whatever is there into Y).
With this we can already fix the first thing in the file: dhcp must become none (the IP is written by hand, not handed out by a DHCP server).
/dhcp<CR> # the cursor jumps to the d of dhcp cwnone<Esc> # change the whole word to none u # undo the change <C-r> # redo it :wq<CR> # save and quit
/dhcp leaves the cursor on the d and cw replaces the whole word. After Esc:
DEVICE=enp2s0
BOOTPROTO=none
ONBOOT=no
IPADDR=192.168.2.27
NETMASK=255.255.255.0
GATEWAY=192.168.2.1
~
~
~
2,14 All
The cursor ended at 2,14, on the last letter typed: when you leave insert mode, vi steps back one position. Now u (undo) undoes the last change:
DEVICE=enp2s0 BOOTPROTO=dhcp ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ 1 change; before #1 2 seconds ago 2,11 All
dhcp is back and vim tells you what it undid: 1 change; before #1 (it undid 1 change, number 1 in the history) and how long ago it was made (on your computer it will show another time). Ctrl-r (redo) redoes it, and :wq saves. The result on disk:
$ cat -n ifcfg-enp2s0
1 DEVICE=enp2s0
2 BOOTPROTO=none
3 ONBOOT=no
4 IPADDR=192.168.2.27
5 NETMASK=255.255.255.0
6 GATEWAY=192.168.2.1
cw is c (change) + w (up to the next word). It isn’t a standalone command to memorise: it’s a combination. That’s what comes next.
Deleting, copying and pasting: the grammar of vi
Here’s the idea that stops vi looking like an endless list of shortcuts. Commands are built like sentences:
- Operator:
d(delete),c(change),y(yank, copy). - Motion: any of the ones from the previous section (
w,$,j,G…). - Count in front: how many times.
If you double the operator (dd, cc, yy), it acts on the whole line. Whatever you delete or copy goes to a vi “clipboard” (the unnamed register) and you paste it with p (below or after the cursor) or P (above or before).
| I want to… | Command | Reads as |
|---|---|---|
| delete the line | dd | delete line |
| delete 3 lines | 3dd | 3 times delete line |
| delete to the end of the line | d$ | delete up to $ |
| copy the line | yy | yank line |
| paste below / above | p / P | |
| undo / redo | u / Ctrl-r | |
| repeat the last change | . |
Let’s try it:
4G # go to line 4 (IPADDR) yyp # copy the line and paste it below dd # delete the copy gg3dd # back to the top and delete three lines u # undo the deletion of the three :q!<CR> # quit without saving anything
4G takes us to the IPADDR line; yy copies it and p pastes it below:
DEVICE=enp2s0
BOOTPROTO=dhcp
ONBOOT=no
IPADDR=192.168.2.27
IPADDR=192.168.2.27
NETMASK=255.255.255.0
GATEWAY=192.168.2.1
~
~
5,1 All
Now there are 7 lines and the cursor is on the copy (5,1). dd deletes it; gg goes up to the first line and 3dd deletes three at once:
IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ ~ ~ ~ 3 fewer lines 1,1 All
3 fewer lines: lines 1, 2 and 3 are gone and 6 − 3 = 3 remain. A single u undoes all of it, because 3dd is one single change:
DEVICE=enp2s0 BOOTPROTO=dhcp ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ 3 more lines; before #3 1 second ago 1,1 All
To finish, :q! quits without saving: everything we did stays in vi’s memory and the disk never finds out.
$ cat -n ifcfg-enp2s0
1 DEVICE=enp2s0
2 BOOTPROTO=dhcp
3 ONBOOT=no
4 IPADDR=192.168.2.27
5 NETMASK=255.255.255.0
6 GATEWAY=192.168.2.1
Vi works on a copy of the file in memory (the buffer). Nothing reaches the disk until you type :w. That’s why :q! is so useful: it’s the “nothing happened here” button.
Searching and substituting
For searching you already know /text, n and N. For substituting, command-line mode has :s:
| Command | What it changes |
|---|---|
:s/old/new/ | the first occurrence on the cursor line |
:s/old/new/g | all of them on the cursor line |
:%s/old/new/g | all of them in the whole file (% = every line) |
:2,4s/old/new/g | all of them on lines 2 to 4 |
Imagine the lab changes network: the workstation moves from 192.168.2.x to 10.0.1.x. Two lines carry it:
:%s/192.168.2/10.0.1/g<CR> :wq<CR>
This is the screen right after pressing Enter:
DEVICE=enp2s0 BOOTPROTO=dhcp ONBOOT=no IPADDR=10.0.1.27 NETMASK=255.255.255.0 GATEWAY=10.0.1.1 ~ ~ ~ :%s/192.168.2/10.0.1/g 6,1 All
The command stays written at the bottom and the file already shows 10.0.1.27 and 10.0.1.1. After :wq:
$ cat -n ifcfg-enp2s0
1 DEVICE=enp2s0
2 BOOTPROTO=dhcp
3 ONBOOT=no
4 IPADDR=10.0.1.27
5 NETMASK=255.255.255.0
6 GATEWAY=10.0.1.1
Forgetting that the pattern is a regular expression. In 192.168.2 each . means “any character”, so it would also match 192x168y2. Nothing goes wrong here, but if you want a real dot, write it as \.. You’ll see it in the practice.
Saving and quitting (without getting trapped)
The great vi meme. These are all the exits you need:
| Command | What it does |
|---|---|
:w | saves and you stay in vi |
:q | quits, only if there are no unsaved changes |
:wq (or :x, or ZZ in normal mode) | saves and quits |
:q! | quits throwing the changes away |
:w other-name | saves a copy under another name |
What happens if there are changes and you try :q? This:
dd # any change :q<CR> # try to quit <CR> # Enter to carry on :q!<CR> # quit throwing the change away
The dd deletes the first line and :q runs into this:
ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ ~ E37: No write since last change (add ! to override) Press ENTER or type command to continue
E37: No write since last change (add ! to override). vi protects you: it won’t let you lose your work by accident. The message is long and the screen moves up one line, which is why Press ENTER appears: press Enter and you’re back in normal mode. Then you choose: :wq if you want to save, :q! if not. We chose :q!, so the dd never reached the disk:
$ cat -n ifcfg-enp2s0
1 DEVICE=enp2s0
2 BOOTPROTO=dhcp
3 ONBOOT=no
4 IPADDR=192.168.2.27
5 NETMASK=255.255.255.0
6 GATEWAY=192.168.2.1
If you see a strange screen and don’t know what you did, the emergency exit is Esc, Esc, :q! and Enter. You leave without saving anything and reopen calmly.
Common mistakes
Every letter is a command. ONBOOT=yes typed without pressing i first opens a line above (the O) and writes NBOOT=yes. Always look for -- INSERT --.
You get E37. It isn’t a failure: choose between :wq and :q!.
:s/a/b/g only touches the cursor line. For the whole file it’s :%s/a/b/g.
:%s/0/1/ changes only the first 0 on each line.
The ifcfg files in /etc belong to root. If you open one as a normal user, vim warns you it’s read-only and :w fails. Open it with sudo vi (you’ll see it in the networking topic).
Try it in your terminal
Make a copy to play with fearlessly and repeat this session. I called mine ifcfg-test, with the same content as the file above:
$ vi ifcfg-test
Inside vi, type this (no insert mode: all in normal mode):
dd # delete the first line dd # and the next one u # undo one deletion :q!<CR> # quit without saving
After the two dd you should see:
ONBOOT=no
IPADDR=192.168.2.27
NETMASK=255.255.255.0
GATEWAY=192.168.2.1
~
~
~
~
~
1,1 All
And after the u:
BOOTPROTO=dhcp ONBOOT=no IPADDR=192.168.2.27 NETMASK=255.255.255.0 GATEWAY=192.168.2.1 ~ ~ ~ ~ 1 more line; before #2 1 second ago 1,1 All
When you quit with :q!, check the file hasn’t changed:
$ cat -n ifcfg-test
1 DEVICE=enp2s0
2 BOOTPROTO=dhcp
3 ONBOOT=no
4 IPADDR=192.168.2.27
5 NETMASK=255.255.255.0
6 GATEWAY=192.168.2.1
What to watch for:
- After the first
dd, line 2 moves up to become line 1: vi leaves no gaps, it renumbers the lines. - The second
dddeletes what is now the first line (BOOTPROTO=dhcp), not the original line 2. uundoes only the last deletion:BOOTPROTOcomes back, butDEVICEis still gone. The message saysbefore #2because it undoes change number 2.:q!throws both changes away at once: nothing happened on disk, ascat -nproves.- Repeat the test with
:wqinstead of:q!and runcat -nagain: now the line really is missing.
Summary
| Concept | The essentials |
|---|---|
| Modes | normal (commands), insert (text), command-line (:); Esc goes back to normal |
| Moving | h j k l, w b, 0 $, gg G, 5G, f=, /text, n |
| Typing | i a I A o O, cw, cc; x and r for quick fixes |
| Grammar | count + operator (d c y) + motion: 3dd, dw, cw, 2yy |
| Paste and undo | p P, u, Ctrl-r, . |
| Substitute | :s/a/b/ (line), :%s/a/b/g (everything, every time) |
| Quit | :w, :wq, :q, :q!, ZZ |
Three ideas to take away:
- Before typing anything, ask yourself which mode you’re in. Esc always takes you back to normal.
- Commands are sentences: operator + motion, with a count in front if you want to repeat.
- vi edits a copy in memory:
:wtakes it to disk and:q!throws it away.
Now it’s your turn: in the vi practice we get ifcfg-enp2s0 ready for the lab with three complete edits. And with vi under control, the next step is what goes inside that file: network configuration on Linux.
Did this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)