vi on Linux — exercises with resolv.conf, repeated lines and a cloned network card (and the NBOOT=yes nobody had typed)

On the day of the networks practical exam, a classmate in my row put his hand up: “Sir, vi has written a line that says NBOOT=yes and it wasn’t me”. It was him: he typed ONBOOT=yes in normal mode and the O turned into a command. The whole class laughed… and half of us secretly checked our own file was fine. The lesson: in vi, before typing, look at the bottom; if it doesn’t say -- INSERT --, your letters are commands.

These exercises close the vi topic after the theory and the practice. Now you choose the keys.

How to use this article

  1. Read the task and the expected output (what cat -n must show at the end).
  2. Make a copy of the file and solve it in vi without looking at the solution. If you get stuck, open the hint.
  3. Before looking at the solutions, do the three “What’s wrong here?” snippets: predict what will happen.
  4. Compare your keys with mine. There are many valid sequences: what counts is the result and knowing why it works.

Level 1: create resolv.conf

The /etc/resolv.conf file tells the machine which DNS servers to ask to translate names (like ulpgc.es) into IP addresses. Create from scratch a resolv.conf file with the university’s first two DNS servers, one per line, in the format nameserver IP: 193.145.147.22 and 193.145.138.100.

You start with the file not existing:

$ cat -n resolv.conf
cat: resolv.conf: No such file or directory

Expected output:

$ cat -n resolv.conf
     1	nameserver 193.145.147.22
     2	nameserver 193.145.138.100

vi resolv.conf opens an empty file (vi creates it when you save). Enter insert mode, type the first line, Enter, the second, Esc and save.

Level 2: out with the duplicates

This resolv.conf has the first server repeated three times and two public servers the lab doesn’t want to use (8.8.8.8 and 8.8.4.4). Keep only the university’s three, without deleting line by line: use commands with a count.

$ cat -n resolv.conf
     1	nameserver 193.145.147.22
     2	nameserver 193.145.147.22
     3	nameserver 193.145.147.22
     4	nameserver 193.145.138.100
     5	nameserver 8.8.8.8
     6	nameserver 8.8.4.4
     7	nameserver 193.145.138.200

Expected output:

$ cat -n resolv.conf
     1	nameserver 193.145.147.22
     2	nameserver 193.145.138.100
     3	nameserver 193.145.138.200

It’s two deletions of two lines each. Watch where the cursor ends up after the first one.

Level 3: clone the card

The workstation is getting a second card, enp3s0, with the same configuration as enp2s0 but the next IP (.28 instead of .27). Starting from ifcfg-enp2s0, create ifcfg-enp3s0 without modifying the original and without typing the IP by hand.

$ cat -n ifcfg-enp2s0
     1	DEVICE=enp2s0
     2	BOOTPROTO=none
     3	ONBOOT=yes
     4	IPADDR=10.0.1.27
     5	NETMASK=255.255.255.0
     6	GATEWAY=10.0.1.1
     7	NETWORK=10.0.1.0
$ cat -n ifcfg-enp3s0
cat: ifcfg-enp3s0: No such file or directory

Expected output:

$ cat -n ifcfg-enp2s0
     1	DEVICE=enp2s0
     2	BOOTPROTO=none
     3	ONBOOT=yes
     4	IPADDR=10.0.1.27
     5	NETMASK=255.255.255.0
     6	GATEWAY=10.0.1.1
     7	NETWORK=10.0.1.0
$ cat -n ifcfg-enp3s0
     1	DEVICE=enp3s0
     2	BOOTPROTO=none
     3	ONBOOT=yes
     4	IPADDR=10.0.1.28
     5	NETMASK=255.255.255.0
     6	GATEWAY=10.0.1.1
     7	NETWORK=10.0.1.0

You need three vim things that aren’t classic vi or from the practice: :w name saves under another name, Ctrl-a adds 1 to the number under or to the right of the cursor, and :q! quits without touching the file you opened.

What’s wrong here?

Three snippets on the theory’s ifcfg-enp2s0 (BOOTPROTO=dhcp, ONBOOT=no, network 192.168.2.x). Before reading the explanation, predict what will show on screen.

Snippet 1: typing without entering insert mode

You open the file and, without pressing anything else, type:

ONBOOT=yes     # typed in normal mode, as if you were in insert mode
<Esc>:q!<CR>

Predict: does ONBOOT=yes show up anywhere?

The real screen after typing ONBOOT=yes:

NBOOT=yes
DEVICE=enp2s0
BOOTPROTO=dhcp
ONBOOT=no
IPADDR=192.168.2.27
NETMASK=255.255.255.0
GATEWAY=192.168.2.1
~
~
-- INSERT --                                          1,10          All

There’s a new line at the very top with NBOOT=yes and vim is in insert mode. Each letter was interpreted according to the mode vi was in at that moment:

What vi does with ONBOOT=yes typed in normal modeThe O is a command that opens a line above the cursor and enters insert mode; the next letters, NBOOT=yes, are typed as text on that new line.ONBOOT=yes  command: opens a line ABOVE and enters insert modealready in insert: typed as textWhat ends up on line 1:NBOOT=yesDEVICE=enp2s0BOOTPROTO=dhcp— INSERT —
The first letter is a command and gets 'eaten': that is why the new line starts with NBOOT.

In normal mode, O is the command “open a line above and enter insert mode”. From then on you’re in insert mode and the rest (NBOOT=yes) is typed as is. Position 1,10 is the end of that new line.

Trusting that vi “was already typing”. With other words the damage is different, because each letter is a different command (d deletes, x deletes, u undoes, p pastes…). Always look at the last line.

The fix: Esc, u to undo, go to the right line and change it whole with cc: 3GccONBOOT=yes<Esc>. That’s exactly the first command of snippet 2, and you’ll see its result there.

Snippet 2: quitting without saving… or not

3GccONBOOT=yes<Esc>   # change the whole of line 3
:q<CR>         # try to quit
<CR>
:wq<CR>        # save and quit

Predict: what does :q do?

BOOTPROTO=dhcp
ONBOOT=yes
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: there’s an unsaved change (line 3) and :q refuses to quit. The screen has moved up one line to make room for the message and vim is waiting for Enter. After that Enter, :wq does save and quit:

$ cat -n ifcfg-enp2s0
     1	DEVICE=enp2s0
     2	BOOTPROTO=dhcp
     3	ONBOOT=yes
     4	IPADDR=192.168.2.27
     5	NETMASK=255.255.255.0
     6	GATEWAY=192.168.2.1

Read the whole message: add ! to override tells you :q! would quit throwing the change away. vi never leaves you trapped; it makes you decide.

Snippet 3: the substitution that finds nothing

The cursor is on line 1 (just opened) and you want to move the file from network 192.168.2 to 10.0.1:

:s/192.168.2/10.0.1/g<CR>    # without % : only the cursor line (line 1)
:%s/192.168.2/10.0.1/g<CR>   # with % : the whole file
:wq<CR>

Predict: how many lines does the first command change?

DEVICE=enp2s0
BOOTPROTO=dhcp
ONBOOT=no
IPADDR=192.168.2.27
NETMASK=255.255.255.0
GATEWAY=192.168.2.1
~
~
~
E486: Pattern not found: 192.168.2                    1,1           All

E486: Pattern not found: none. Without %, :s only looks at the cursor line, and line 1 (DEVICE=enp2s0) doesn’t contain 192.168.2. vim doesn’t search the rest of the file. With % it does:

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

And 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

If :s gives you E486 and you’re sure the text is in the file, the % (or a range like :4,6s) is almost always missing.

Solutions

Solution to Level 1

inameserver 193.145.147.22<CR>nameserver 193.145.138.100<Esc>
:wq<CR>

The screen before saving:

nameserver 193.145.147.22
nameserver 193.145.138.100
~
~
~
~
~
~
~
                                                      2,26          All

Why it’s done this way:

  • i enters insert mode in an empty file (a or o would also do).
  • <CR> inside insert mode is a normal line break: it doesn’t run anything.
  • Esc before :wq, because the colon only opens command-line mode from normal mode. Without Esc, you’d type :wq into the file.
  • Check: the ruler says 2,26 because nameserver 193.145.138.100 has 26 characters (10 + 1 space + 15). It no longer says -- INSERT --: Esc took us back to normal mode.

Solution to Level 2

j2dd           # on line 2, delete the two copies
j2dd           # down to 8.8.8.8 and delete that line and 8.8.4.4
:wq<CR>

After the first j2dd:

nameserver 193.145.147.22
nameserver 193.145.138.100
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 193.145.138.200
~
~
~
~
                                                      2,1           All

Why it’s done this way:

  • j goes down to line 2 and 2dd deletes lines 2 and 3: the two copies. The first occurrence stays. 7 − 2 = 5 lines remain.
  • After the deletion the cursor is on line 2 (193.145.138.100): what was below moved up. One more j and we’re on 8.8.8.8.

After the second j2dd:

nameserver 193.145.147.22
nameserver 193.145.138.100
nameserver 193.145.138.200
~
~
~
~
~
~
                                                      3,1           All
  • 2dd takes 8.8.8.8 and 8.8.4.4: 5 − 2 = 3 lines. The cursor ends on line 3 (193.145.138.200), the one that moved up.
  • The obvious alternative, dd five times moving in between, works, but it’s more keys and more chances to delete the wrong one.

Solution to Level 3

:%s/enp2s0/enp3s0/g<CR>   # the name of the card
/IPADDR<CR>    # the IP line
$<C-a>         # to the end of the line and add 1 to the number: 27 -> 28
:w ifcfg-enp3s0<CR>   # save under ANOTHER name
:q!<CR>        # the original is not touched

After the substitution, the search and Ctrl-a:

DEVICE=enp3s0
BOOTPROTO=none
ONBOOT=yes
IPADDR=10.0.1.28
NETMASK=255.255.255.0
GATEWAY=10.0.1.1
NETWORK=10.0.1.0
~
~
/IPADDR                                               4,16          All

IPADDR=10.0.1.28: Ctrl-a looked for the number from the cursor and added 1 to it (27 + 1 = 28). The ruler says 4,16, the last digit of the line. Then :w ifcfg-enp3s0:

DEVICE=enp3s0
BOOTPROTO=none
ONBOOT=yes
IPADDR=10.0.1.28
NETMASK=255.255.255.0
GATEWAY=10.0.1.1
NETWORK=10.0.1.0
~
~
"ifcfg-enp3s0" [New] 7L, 113B written                 4,16          All

[New] 7L, 113B written: the new file has 7 lines and 113 bytes, the same as the original (we changed 2 to 3 and 7 to 8, same size). The message doesn’t fit in full next to the ruler and vim trims it on the left with <.

Why it’s done this way:

  • :%s/enp2s0/enp3s0/g changes the name on any line where it appears (here only DEVICE, but in other ifcfg files it’s also in NAME).
  • $ before Ctrl-a puts the cursor on the 7: so it adds 1 to 27. From the start of the line, Ctrl-a would have found the first number on the line, the 10 of 10.0.1.27, and you’d have created 11.0.1.27.
  • :w ifcfg-enp3s0 writes the buffer to another file, but vi is still editing ifcfg-enp2s0.
  • :q! is key: the buffer has changes compared with ifcfg-enp2s0 and we don’t want to save them there. The original comes out untouched, as the task asked.
The result of level 3: two filesThe original ifcfg-enp2s0 does not change because you quit with :q!; the new ifcfg-enp3s0 has DEVICE=enp3s0 and IPADDR=10.0.1.28.ifcfg-enp2s0 (untouched, :q!)ifcfg-enp3s0 (new, :w name)DEVICE=enp2s0DEVICE=enp3s0:%s/enp2s0/enp3s0/gBOOTPROTO=noneBOOTPROTO=noneONBOOT=yesONBOOT=yesIPADDR=10.0.1.27IPADDR=10.0.1.28$ Ctrl-a (27 → 28)NETMASK=255.255.255.0NETMASK=255.255.255.0GATEWAY=10.0.1.1GATEWAY=10.0.1.1NETWORK=10.0.1.0NETWORK=10.0.1.0
The substitution and Ctrl-a only live in the buffer: :w name saves them to another file and :q! leaves the original as it was.

The drawing shows the two files at the end: the untouched original and the clone with its two differences.

Try it in your terminal

Snippet 3 has an even sneakier version: inside a script, with vim -es. This session is real, on the ifcfg-enp2s0 with network 192.168.2:

# Snippet 3 in silent mode: without % the command only looks at line 1
vim -u NONE -es -c 's/192.168.2/10.0.1/g' -c 'wq' ifcfg-enp2s0
echo $?                                                         # not 0: something failed
grep -c 192.168.2 ifcfg-enp2s0                                  # how many lines still have the old network
vim -u NONE -es -c '%s/192.168.2/10.0.1/g' -c 'wq' ifcfg-enp2s0
echo $?
grep -c 192.168.2 ifcfg-enp2s0

What came out:

$ vim -u NONE -es -c 's/192.168.2/10.0.1/g' -c 'wq' ifcfg-enp2s0
$ echo $?
0
$ grep -c 192.168.2 ifcfg-enp2s0
1
$ vim -u NONE -es -c '%s/192.168.2/10.0.1/g' -c 'wq' ifcfg-enp2s0
$ echo $?
0
$ grep -c 192.168.2 ifcfg-enp2s0
0

What to watch for:

  1. The first command, without %, doesn’t give an error: echo $? is 0. In ex mode, the current line when it starts is the last line of the file (GATEWAY=192.168.2.1), and there was a match there.
  2. That’s why grep -c counts 1: of the two lines with 192.168.2, only one changed. IPADDR still has the old network.
  3. The same slip gives a different result depending on where the cursor is: on screen (snippet 3) it failed with E486; here it “works” halfway and silently.
  4. With %, the second command fixes what was left and the count drops to 0.
  5. Moral for your scripts: check the result (grep -c), not just $?.

Cheat sheet: vi

The essentials in 30 seconds

vi has three modes: normal (commands), insert (text) and command-line (:). Esc always goes back to normal. Commands combine: count + operator (d, c, y) + motion. Nothing reaches the disk until :w, and :q! quits without saving.

Moving

I want to…KeysResult
go down / up a linej / ksame column (if it exists)
go to the start / end of the line0 / $
go to line 55G
first / last linegg / G
next / previous wordw / b= counts as a word
search for ONBOOT/ONBOOT Entercursor on the O; n next, N previous
jump to the next = on the linef=

Editing

I want to…KeysResult
type before the cursor / at the end of the linei / A-- INSERT --
new line below / aboveo / O
change a wordcwdeletes and enters insert mode
change the whole linecc
delete character / line / 3 linesx / dd / 3dd3 fewer lines
copy a line and paste it belowyy p
swap with the line belowddp
undo / redo / repeatu / Ctrl-r / .
add 1 to the numberCtrl-a27 → 28

: commands

I want to…CommandResult
save / save and quit:w / :wq"file" 7L, 113B written
quit without saving:q!the disk doesn’t change
save under another name:w ifcfg-enp3s0[New]
substitute in the whole file:%s/old/new/gN substitutions on M lines
substitute on lines 4 to 6:4,6s/old/new/g
delete the lines starting with #:g/^#/d
without opening the screen (scripts)vim -es -c '%s/a/b/g' -c wq file

Exercise templates

ExerciseKeysResult
Level 1i text <CR> text <Esc> :wqnew 2-line file
Level 2j2dd j2dd :wqfrom 7 to 3 lines
Level 3:%s/enp2s0/enp3s0/g /IPADDR $ Ctrl-a :w ifcfg-enp3s0 :q!untouched original + clone with .28

Symptom / likely cause

SymptomLikely cause
stray letters or odd lines appearyou typed in normal mode; Esc and u
E37: No write since last change:q with changes: :wq or :q!
E486: Pattern not found with :sthe % is missing or the pattern doesn’t match (an unescaped dot?)
:wq appears written inside the fileyou were in insert mode: Esc before :
E45: 'readonly' option is setthe file belongs to another user (root): sudo vi
vim says Press ENTERthe message didn’t fit: press Enter and carry on

Before you hand it in, ask yourself

  1. Have I checked the file with cat -n after leaving vi, not just on the editor’s screen?
  2. Is the number of substitutions or deleted lines the one I expected?
  3. Did I quit with :wq when I wanted to save and with :q! when I didn’t?
  4. Did I work on a copy until I was sure?

With vi under control, the next topic finally opens up what’s inside the ifcfg: network configuration on Linux.

Keep goingAll Linux articles

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 *