CoursesAdvanced Linux internals & toolingPower editing & text processing

Power editing & text processing

vim/vi mastery, and advanced sed & awk.

Advanced16 min · lesson 17 of 17

When you get paged at 2am and SSH (Secure Shell, the encrypted remote-login protocol) into a box to fix a broken config or read a log that is racing past, there is no mouse and no graphical editor waiting for you. There is a terminal, and three tools that ship on almost every Linux machine ever built: vim (a modal text editor, and its near-identical ancestor vi), sed (the stream editor), and awk (a tiny programming language for columns of text, named after its authors Aho, Weinberger, and Kernighan). The engineer who is fluent with them is several times faster than the one who fights them, and speed during a live incident is not a luxury.

This lesson is the power-user layer. There is no long list of commands to memorize here. What you want is the grammar and the idioms that let you build the exact edit or the exact report you need, on the spot, from parts you already know.

Vim Speaks In Verbs And Motions

Vim's real power is that its commands compose like a sentence: a verb plus a target. You do not learn ten thousand commands. You learn a handful of verbs, a handful of ways to describe a target, and you snap them together. The verbs are operators: d to delete, c to change (delete, then drop you into insert mode to type the replacement), y to yank (copy). Targets come in two flavors. Motions say 'from here to there': w to the next word, $ to the end of the line, } down to the next blank line. Text objects name a whole thing regardless of where your cursor sits inside it: iw is the inner word, i" is the text inside a pair of quotes, ip is a paragraph.

Snap a verb onto a target and you get a precise edit. dw deletes to the next word. ci" changes whatever is inside the quotes, like telling a mover to take everything inside the box no matter where you are standing in the room. dap deletes a whole paragraph. A number in front multiplies the whole thing, so 3dd deletes three lines. The single most valuable key is the dot: . repeats your last change. u undoes it, Ctrl-R redoes it. Learn this grammar and you stop recalling commands and start composing them.

vim
d c y verbs: delete, change (delete + insert), yank (copy)
w $ } 0 motions: next word, end of line, next blank line, start of line
iw i" ip a( text objects: inner word, inside quotes, a paragraph, a parens pair
dw delete from the cursor to the next word
ciw change the whole word under the cursor
ci" change the text inside the quotes
d$ delete to the end of the line
dap delete a paragraph (and its trailing blank line)
yi( yank everything inside the parentheses
3dd delete 3 lines
>ip indent this paragraph
. repeat the last change
u Ctrl-R undo redo
How a vim edit is built: verb + target
Verbs (operators)
d
delete
c
change (delete + insert)
y
yank / copy
Motions (here to there)
w
to next word
$
to end of line
}
to next blank line
f x
to next character x
Text objects (a whole thing)
iw
inner word
i"
inside quotes
ip
a paragraph
i(
inside parentheses
Pick one verb, then one target (a motion or a text object). dw, ci", dap, yi(. Put a count in front to multiply it: 3dd deletes three lines.

Moving And Editing At Speed

Building the edit is half the job. Getting your cursor to the right spot without hammering the arrow keys is the other half. Jump a word at a time with w and b. Fly to a character on the current line with f followed by that character. Go to line 42 with :42 or 42G. Land on the matching bracket with %, which is how you check that a JSON (JavaScript Object Notation, a common structured-data format) block or a shell if/fi pair is balanced. Search with /pattern, then n for the next hit and N for the previous. To start typing fast, A appends at the end of the line and o opens a fresh line below.

Visual mode selects first, then acts. v selects characters, V selects whole lines, and Ctrl-V selects a block, a rectangle of columns. Block-select down a file, press I, type once, hit Esc, and your text appears at the front of every selected line at once, which is how you comment out twenty lines in a few keystrokes. When you want to change every occurrence in the file, drop to the command line. :%s/old/new/g replaces all of them, adding c (:%s/old/new/gc) makes vim confirm each one, :g/DEBUG/d deletes every line matching DEBUG, and :10,20s/^/# / comments out lines 10 through 20.

vim
w b jump forward / back one word
f{char} jump to the next {char} on this line
:42 42G go to line 42
% jump to the matching ( ) { } [ ]
/error search; n = next match, N = previous
A append at end of line o open a new line below
v V Ctrl-V select: characters / whole lines / a block (column)
then I types on every selected line, Esc applies it
:%s/old/new/g replace every match in the file
:%s/old/new/gc ...asking you to confirm each one
:g/DEBUG/d delete every line that matches DEBUG
:10,20s/^/# / comment out lines 10 through 20

A short config file makes all of this stick, and it lives in your home directory as .vimrc. The setting worth calling out for defenders is set list, which paints normally invisible characters onto the screen. A configuration file written in YAML (a whitespace-sensitive settings format where spaces and tabs actually change the meaning) that 'looks correct' but refuses to parse often has a literal tab where spaces belong, a trailing space, or a non-breaking space that rode in from a copy-paste out of a web page. Turn on set list and those characters stop hiding.

~/.vimrc
set number " show line numbers
set incsearch hlsearch " search as you type, highlight all matches
set list listchars=tab:»·,trail:·,nbsp:␣
syntax on
Vim is not a forensics tool
Opening a file in a normal vim session drops a swap file (a hidden .filename.swp beside it), and a single stray :w rewrites the file and overwrites the modification time you might need as evidence. If a file could be evidence, record its hash first with sha256sum, then read it without writing. less is the safe default; it never leaves a swap file or alters the contents. If you must use vim, pass both flags together as vim -Rn: -R opens it read-only and -n skips the swap file, so nothing new lands next to the original.

Sed: One Scripted Pass Over The Stream

sed is a worker on an assembly line. Text flows past one line at a time, sed performs the scripted operation you handed it on each line, and the line moves on. It never loads the whole file into memory, so it chews through a multi-gigabyte log without breaking a sweat. The operation you already know is substitution, s/old/new/, but sed does far more than swap words.

You can restrict any operation to an address: a single line number, a pattern, or a range between two patterns. The d command deletes matching lines. Stripping the comment and blank lines out of a config shows you what is actually set, which is the fastest way to audit a service you did not configure yourself. Here is every real setting in a stock SSH server config, with the noise removed.

~/secopslog — bash
$ sed -E '/^\s*#/d; /^\s*$/d' /etc/ssh/sshd_config
Include /etc/ssh/sshd_config.d/*.conf KbdInteractiveAuthentication no UsePAM yes X11Forwarding yes PrintMotd no AcceptEnv LANG LC_* Subsystem sftp /usr/lib/openssh/sftp-server

Give sed a range of two patterns with -n (quiet) and p (print) and it prints only the slice between them. On a log where dozens of requests interleave, that pulls one request's whole story out of the noise without opening the file.

~/secopslog — bash
$ sed -n '/req-7f3a START/,/req-7f3a END/p' app.log
2026-03-10 14:22:03 req-7f3a START POST /checkout 2026-03-10 14:22:03 req-7f3a validating cart items=3 2026-03-10 14:22:04 req-7f3a ERROR payment gateway timeout 2026-03-10 14:22:04 req-7f3a END status=502

Backreferences are how sed remembers pieces of a match and rebuilds them in a new order. Wrap parts of the pattern in parentheses (in extended mode, -E) and refer back to them as \1, \2, \3. Reordering a date is the classic drill, but the same move reshapes log fields, config keys, anything with structure. GNU sed (the version of sed that ships on nearly every Linux distribution) also takes an I flag for case-insensitive matching, so s/error/ERROR/gI catches Error, ERROR, and error in a single pass.

~/secopslog — bash
$ echo '2026-03-10' | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'
10/03/2026

One sed pass also scrubs secrets before you paste a log into a ticket or a chat channel. Match the sensitive value, keep its label, replace the rest. The character class [^ &]+ says 'everything up to the next space or ampersand', so the value dies but the surrounding fields survive.

~/secopslog — bash
$ printf 'user=alice password=secret123\ntoken=abc123&status=200\n' \ | sed -E 's/(password=|token=)[^ &]+/\1REDACTED/g'
user=alice password=REDACTED token=REDACTED&status=200
sed -i has no undo
Without -i, sed prints to the screen and your file is untouched, so you can eyeball the result before committing to it. With -i it rewrites the file in place, and there is no way back. Always run the command once without -i, read the output, then add the flag. Safer still, use -i.bak, which writes the change and leaves the original as file.bak. Test the pattern on a copy before you point it at anything that matters.

Awk: A Spreadsheet That Runs A Program

awk treats text like a spreadsheet and runs a tiny program on every row. Each line is a record, split into fields you reach as $1, $2, and so on up to $NF (the last field on the line). NR is the current record number, the running line count. You hand awk a pattern and an action in braces: for every line matching the pattern, run the action. That is the entire model, and it retires a surprising number of throwaway shell scripts.

Here is the one-liner every defender should keep in muscle memory. On Linux an account whose user ID number is 0 has root's powers, no exceptions, and there should be exactly one such account: root. The file /etc/passwd stores that ID as the third colon-separated field, so tell awk the separator is a colon with -F: and print the name of any account whose third field is 0.

~/secopslog — bash
$ awk -F: '($3 == 0) { print $1 }' /etc/passwd
root

If a second name ever shows up in that output, someone gave an account root's powers behind your back, and you have just found the backdoor. Flip the test to $3 >= 1000 and you get the human login accounts instead, because everything below 1000 is reserved for the system's own daemons and service accounts. Those low-numbered accounts are the ones to inspect next: a daemon account carrying a login shell of /bin/bash rather than /usr/sbin/nologin is worth a second look, because a background service has no reason to log in interactively.

Associative arrays are where awk earns its keep. An associative array is a bucket you can index by any string, including text like an IP (Internet Protocol) address, not only numbers. That lets you total something per address, per user, or per status code in a single pass over a file. Sum the bytes sent (field 10 of a standard web access log) grouped by the client address (field 1) to find who is pulling the most data off your server.

~/secopslog — bash
$ awk '{ bytes[$1] += $10 } END { for (ip in bytes) print bytes[ip], ip }' access.log \ | sort -rn | head -3
45329281 10.0.2.1 812004 10.0.1.9 203344 10.0.3.7

A BEGIN block runs before the first line and an END block runs after the last, which is where you print a summary. Count your server errors and total your traffic in one sweep, then format it with printf the way a C program would. In a standard access log the status code lands in field 9 and the byte count in field 10, so a single pass gives you both numbers.

~/secopslog — bash
$ awk '$9 == 500 { errs++ } { total += $10 } END { printf "requests=%d 500s=%d MB=%.1f\n", NR, errs, total/1048576 }' access.log
requests=48213 500s=37 MB=812.4

Point the same thinking at an authentication log and you get a brute-force detector for free. Every failed SSH login writes a line reading 'Failed password ... from <IP> ...'. Walk the fields, grab the address that follows the word from, and rank the offenders by how many times they knocked.

~/secopslog — bash
$ awk '/Failed password/ { for (i=1;i<=NF;i++) if ($i=="from") print $(i+1) }' /var/log/auth.log \ | sort | uniq -c | sort -rn | head -3
128 203.0.113.7 47 198.51.100.23 9 192.0.2.44

That ranked list is your block-list candidate and your incident timeline in one shot. The attacker sees a wall of failed guesses. You see the source address, sorted by how hard they tried, ready to feed into a firewall rule.

Quick check
01You want one command that names every account carrying root's powers on a host, including one an intruder added under a different name. Which command does that?
Incorrect — awk splits on whitespace unless you tell it otherwise, so a colon-joined passwd line lands entirely in $1 and $3 holds nothing at all. An empty field tests equal to 0, so every line matches and you get the whole file back.
Incorrect — This reports the ID that root itself carries, which you already know is 0. It tells you nothing about a second account that was quietly handed the same ID under another name.
Correct — The colon separator lines field 3 up with the ID column, and the print names every account holding 0, whatever someone chose to call it. A second name in that output is the finding.
Incorrect — 1000 and up is the range handed out to human logins, so this is the right shape for a different audit. It skips the low IDs entirely, which is exactly where a planted root account sits.
02A YAML file reads fine on screen but the service keeps rejecting it. You have set list listchars=tab:»·,trail:·,nbsp:␣ in your .vimrc, and you reopen the file. What has that bought you?
Correct — Each listchars entry assigns a glyph to one kind of whitespace, so the character that broke the parse is now on screen instead of sitting there as empty space.
Incorrect — Vim is a text editor and knows nothing about YAML structure. Nothing in that line teaches it the format's rules or how to judge a document as malformed.
Incorrect — Display and file contents are separate concerns. This setting changes only what you see, and the bytes on disk stay exactly as they were until you edit them yourself.
Incorrect — That is what you would get from asking vim to dump its options, which is a different request. Here the word list names a display mode for whitespace, not a report.
03You have a sed substitution ready to point at a config file on a production host. Which way of running it leaves you able to recover if the pattern matches more than you meant?
Incorrect — Version control saves you only if that exact file was committed and is still current, which on a box you are hotfixing rarely holds. By then -i has replaced the only copy on disk.
Correct — Without -i, sed prints its result and leaves the file alone, so you judge the edit before it is real. The suffix then keeps the previous version sitting next to the new one.
Incorrect — Opening it in vim drops a swap file beside it, and one save moves the modification time you might need as evidence. Your undo history also dies when you close the session.
Incorrect — sed keeps no record of what it replaced. If the pattern swallowed more than you intended, that text is gone and no reverse substitution can reconstruct it.

Two habits make all of this permanent. Run vimtutor once, start to finish. It is a built-in, thirty-minute guided practice of the verb-plus-target grammar, and it pays back for the rest of your career. And keep every sed and awk transformation honest: print to the screen before you write with -i, and the moment a one-liner grows past two operations, move it into a small script file where a teammate can read it, review it, and run it again during the next incident. These tools do exactly what you tell them, no more and no less, so the pattern you verify is the pattern you can trust.

Try this

Work through “Awk: A Spreadsheet That Runs A Program” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: vim is not a forensics tool. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related