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.

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:

The three modes of vivi starts in normal mode. With i, a, o, A or O you switch to insert mode and Esc brings you back; with a colon you enter command-line mode, which runs with Enter or is cancelled with Esc.NORMAL MODEmove, delete, copy  vi opens hereINSERTtype textCOMMAND-LINEsave, quit, substitutei a o A OEsc:Enter / EscAt the bottom:— INSERT —(nothing, or the file name):wq
Everything goes through normal mode: Esc always takes you back to it, and from there you enter the other two.

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.

ModeWhat it’s forHow you enterWhat you see at the bottom
Normalmoving, deleting, copying, pastingwhen vi opens, or with Escnothing, or the last message
Inserttyping texti, a, o, A, O…-- INSERT --
Command-linesaving, quitting, substituting: from normal modewhatever 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:

KeyWhere it goes
h j k lone character left, one line down, one line up, one character right
w / bto 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 / Gto the first / last line
5Gto line 5
3jthree lines down (a number in front repeats the motion)
/text + Enterto the next occurrence of text
n / Nto 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:

StepKeyPositionCharacterWhy
1j2,1Bone line down, same column
2$2,14pBOOTPROTO=dhcp has 14 characters
302,1Bback to the start
4f=2,10=BOOTPROTO has 9 letters: the = is number 10
5w2,11dto vi, = and dhcp are separate “words”
6G6,1Glast line (the G of GATEWAY)
7gg1,1Dfirst line
83j4,1I1 + 3 = 4
9/255 Enter5,92NETMASK= takes 8 characters
10n5,132the second 255: 9 + 4 (255.) = 13

And here it is drawn over the file:

Where the cursor jumps with each motionThe file ifcfg-enp2s0 with the real path of the cursor: j, $, 0, f=, w, G, gg, 3j, /255 and n, numbered in order.1DEVICE=enp2s02BOOTPROTO=dhcp3ONBOOT=no4IPADDR=192.168.2.275NETMASK=255.255.255.06GATEWAY=192.168.2.112345678910step · key · line,col1j2,12$2,14302,14f=2,105w2,116G6,17gg1,183j4,19/255 Enter5,910n5,13
Each circle is the real cursor position after that key (taken from vim's ruler, line,column).

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:

KeyYou start typing…
i / abefore / after the cursor
I / Aat the start / end of the line
o / Oon a new line below / above
cwin place of the word (deletes it and enters insert mode)
ccin 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:

The grammar of vi commandsA vi command is an optional count, an operator (d delete, c change, y yank) and a motion. Examples: 3dd, dw, cw, 2yy and d$.counthow many times3+operatorwhat to dod c y+motionup to wherew $ j G3dd3 · d · ddeletes 3 lines (dd = the whole line)dwd · wdeletes up to the next wordcwc · wchanges the word: deletes and enters insert2yy2 · y · ycopies 2 linesd$d · $deletes to the end of the line
Know five operators and ten motions and you already know fifty commands: they combine like words.
  • 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…CommandReads as
delete the linedddelete line
delete 3 lines3dd3 times delete line
delete to the end of the lined$delete up to $
copy the lineyyyank line
paste below / abovep / P
undo / redou / 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:

CommandWhat it changes
:s/old/new/the first occurrence on the cursor line
:s/old/new/gall of them on the cursor line
:%s/old/new/gall of them in the whole file (% = every line)
:2,4s/old/new/gall 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:

CommandWhat it does
:wsaves and you stay in vi
:qquits, 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-namesaves 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:

  1. After the first dd, line 2 moves up to become line 1: vi leaves no gaps, it renumbers the lines.
  2. The second dd deletes what is now the first line (BOOTPROTO=dhcp), not the original line 2.
  3. u undoes only the last deletion: BOOTPROTO comes back, but DEVICE is still gone. The message says before #2 because it undoes change number 2.
  4. :q! throws both changes away at once: nothing happened on disk, as cat -n proves.
  5. Repeat the test with :wq instead of :q! and run cat -n again: now the line really is missing.

Summary

ConceptThe essentials
Modesnormal (commands), insert (text), command-line (:); Esc goes back to normal
Movingh j k l, w b, 0 $, gg G, 5G, f=, /text, n
Typingi a I A o O, cw, cc; x and r for quick fixes
Grammarcount + operator (d c y) + motion: 3dd, dw, cw, 2yy
Paste and undop 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:

  1. Before typing anything, ask yourself which mode you’re in. Esc always takes you back to normal.
  2. Commands are sentences: operator + motion, with a count in front if you want to repeat.
  3. vi edits a copy in memory: :w takes 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.

Next step: practicevi on Linux — practice with a real ifcfg: enable the card, clean up the file and move it to another network (without touching the mouse)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *