CertGrid CertGrid
Hands-on Lab·Linux Foundation Certified System Administrator

grep, sed and the text you have to change

Most of what an administrator does to text is find a line, count some lines, or change a string in a file. grep, sed and awk cover all three, and the mistake that costs marks is not syntax - it is using the wrong field number, which produces a confident wrong answer rather than an error. This guide counts the fields first, on purpose, because I got it wrong while writing it.

Essential Commands Guide 6 of 38 Intermediate

GNU grep, sed and awk (mawk on Ubuntu, gawk on RHEL - the programs here work on both). grep -E for extended regular expressions; the same pattern in basic regex needs backslashes, which the capture shows.

Everything on this page runs on the one Ubuntu host.
Server NameIP AddressOSRolesCPURAMHDD
LFCS-A01192.168.0.70Ubuntu 26.04 LTSPrimary host - most guides run only here2 Core4 GB50 GB

This guide includes

Use this for the text-processing marks. This matters because the wrong awk field number is a silent wrong answer - it prints something plausible, and nothing tells you it was the wrong column.

Before you start

  1. A log, and the fields it has

    Eight lines of access log. Before anything else, count the fields.

    bash Example session
    cd ~/ess/text && awk 'NR==1 {for (i = 1; i <= NF; i++) printf "$%-2d = %s\n", i, $i; print "NF =", NF}' access.log$1  = 10.0.0.1$2  = -$3  = -$4  = [24/Aug/2026:10:00:01]$5  = "GET$6  = /health$7  = HTTP/1.1"$8  = 200$9  = 45NF = 9

    Expected result$1 the IP, $4 the bracketed timestamp, $5 "GET with the quote attached, $8 the status, $9 the bytes, NF = 9.

    Success conditionYou know which field holds what before you write a condition.

  2. The wrong field number is a silent wrong answer

    What I wrote first, and what it actually matched.

    bash Example session
    cd ~/ess/text && echo "the wrong field number is a silent wrong answer, not an error:"; awk '$9 >= 500 {print "   matched:", $1, $9}' access.log; echo "   (that matched a 512-BYTE row, not a 5xx status)"the wrong field number is a silent wrong answer, not an error:   matched: 10.0.0.7 512   (that matched a 512-BYTE row, not a 5xx status)

    Expected resultmatched: 10.0.0.7 512 - and the note that 512 is a byte count, not a 5xx status.

    Success conditionYou know why counting first matters.

  3. So, with the right fields

    Status is $8, bytes is $9.

    bash Example session
    cd ~/ess/text && awk '{print $1, $8, $9}' access.log | head -410.0.0.1 200 4510.0.0.7 201 51210.0.0.1 200 4510.0.0.9 403 99cd ~/ess/text && awk '$8 >= 500 {print $1, $6, $8}' access.log10.0.0.7 /api/orders 50010.0.0.7 /api/orders 500cd ~/ess/text && awk '{bytes += $9} END {printf "total %d bytes over %d requests, mean %.1f\n", bytes, NR, bytes/NR}' access.logtotal 1220 bytes over 8 requests, mean 152.5cd ~/ess/text && awk '{n[$8]++} END {for (s in n) printf "%s -> %d\n", s, n[s]}' access.log | sort -n200 -> 2201 -> 1304 -> 1403 -> 2500 -> 2

    Expected resultTwo genuine 500s on /api/orders; 1220 bytes over 8 requests, mean 152.5; and a status histogram of 200×2, 201, 304, 403×2, 500×2.

    Success conditionYou can summarise a log.

  4. grep, and the flags that do the work

    Matching, counting, inverting and numbering.

    bash Example session
    cd ~/ess/text && grep 500 access.log10.0.0.7 - - [24/Aug/2026:10:01:30] "POST /api/orders HTTP/1.1" 500 21010.0.0.7 - - [24/Aug/2026:10:03:00] "POST /api/orders HTTP/1.1" 500 210cd ~/ess/text && grep -c 403 access.log; echo "---"; grep -n 403 access.log; echo "---"; grep -v 200 access.log | wc -l2---4:10.0.0.9 - - [24/Aug/2026:10:01:12] "GET /admin HTTP/1.1" 403 997:10.0.0.9 - - [24/Aug/2026:10:02:44] "GET /admin HTTP/1.1" 403 99---6cd ~/ess/text && grep -i GET access.log | wc -l; echo "---"; grep -o '"[A-Z]* [^ ]*' access.log | sort -u5---" 200" 201" 304" 403" 500"GET /admin"GET /health"GET /static/app.js"POST /api/orders

    Expected resultTwo 500 lines; -c giving 2; -n prefixing line numbers 4 and 7; -v 200 counting 6.

    Success conditionYou can find and count lines.

  5. Extended against basic regular expressions

    The same pattern, written twice.

    bash Example session
    cd ~/ess/text && grep -E '" (4[0-9]{2}|5[0-9]{2}) ' access.log; echo "--- and the same thing with basic regex needs backslashes:"; grep '" \(4[0-9][0-9]\|5[0-9][0-9]\) ' access.log | wc -l10.0.0.9 - - [24/Aug/2026:10:01:12] "GET /admin HTTP/1.1" 403 9910.0.0.7 - - [24/Aug/2026:10:01:30] "POST /api/orders HTTP/1.1" 500 21010.0.0.9 - - [24/Aug/2026:10:02:44] "GET /admin HTTP/1.1" 403 9910.0.0.7 - - [24/Aug/2026:10:03:00] "POST /api/orders HTTP/1.1" 500 210--- and the same thing with basic regex needs backslashes:4

    Expected resultFour 4xx and 5xx lines from -E, and the same 4 from the basic-regex form with backslashes.

    Success conditionYou can write the pattern the tool expects.

  6. cut, sort and uniq, which is the pipeline you will type

    Counting by field, without awk.

    bash Example session
    cd ~/ess/text && cut -d' ' -f1 access.log | sort | uniq -c | sort -rn      3 10.0.0.7      2 10.0.0.9      2 10.0.0.1      1 10.0.0.3cd ~/ess/text && cut -d'"' -f2 access.log | cut -d' ' -f1 | sort | uniq -c      5 GET      3 POST

    Expected result3 10.0.0.7, 2 10.0.0.9, 2 10.0.0.1, 1 10.0.0.3 - and 5 GET, 3 POST.

    Success conditionYou can produce a top-N without writing a program.

  7. sort, which is not doing what you think

    Four lines of mixed case and two numbers, sorted three ways.

    bash Example session
    cd ~/ess/text && printf 'b\na\nB\nA\n10\n9\n' > mixed.txt && echo "sort:"; sort mixed.txt | tr '\n' ' '; echo; echo "sort -n:"; sort -n mixed.txt | tr '\n' ' '; echo; echo "sort -f:"; sort -f mixed.txt | tr '\n' ' '; echosort:10 9 a A b Bsort -n:A B a b 9 10sort -f:10 9 A a B b

    Expected resultPlain sort: 10 9 a A b B. -n: A B a b 9 10. -f: 10 9 A a B b.

    Success conditionYou can sort deliberately rather than hopefully.

  8. sed, for the change you have to make

    Substitute, print a range, delete - and then edit a file in place.

    bash Example session
    cd ~/ess/text && sed 's/HTTP\/1.1/HTTP\/2/' access.log | head -210.0.0.1 - - [24/Aug/2026:10:00:01] "GET /health HTTP/2" 200 4510.0.0.7 - - [24/Aug/2026:10:00:04] "POST /api/orders HTTP/2" 201 512cd ~/ess/text && sed -n '3,5p' access.log; echo "---"; sed -n '/403/p' access.log | wc -l; echo "---"; sed '1d' access.log | wc -l10.0.0.1 - - [24/Aug/2026:10:00:09] "GET /health HTTP/1.1" 200 4510.0.0.9 - - [24/Aug/2026:10:01:12] "GET /admin HTTP/1.1" 403 9910.0.0.7 - - [24/Aug/2026:10:01:30] "POST /api/orders HTTP/1.1" 500 210---2---7cd ~/ess/text && cp access.log edit.log && sed -i 's/10\.0\.0\.9/REDACTED/g' edit.log && grep -c REDACTED edit.log && grep -c '10.0.0.9' edit.log || echo "0 remaining"200 remaining

    Expected resultHTTP/2 substituted; lines 3 to 5 printed; 1d leaving 7 lines; and sed -i leaving 2 REDACTED and 0 occurrences of the original.

    Success conditionYou can change a string in a file.

  9. sed -i.bak, and tr

    The safety net, and the smallest of the three tools.

    bash Example session
    cd ~/ess/text && cp access.log backup-test.log && sed -i.bak 's/GET/FETCH/' backup-test.log && ls backup-test.log*; echo "---"; head -1 backup-test.log; head -1 backup-test.log.bakbackup-test.logbackup-test.log.bak---10.0.0.1 - - [24/Aug/2026:10:00:01] "FETCH /health HTTP/1.1" 200 4510.0.0.1 - - [24/Aug/2026:10:00:01] "GET /health HTTP/1.1" 200 45cd ~/ess/text && echo "Hello   World" | tr 'a-z' 'A-Z'; echo "Hello   World" | tr -s ' '; echo "Hello   World" | tr -d ' '; echo "a1b2c3" | tr -cd '0-9'; echoHELLO   WORLDHello WorldHelloWorld123

    Expected resultbackup-test.log with FETCH and backup-test.log.bak with the original GET; then HELLO WORLD, Hello World, HelloWorld, 123.

    Success conditionYou can edit in place without losing the original.

Troubleshooting

Official sources