grep: Searching Text with Patterns

Per GNU grep’s grep(1) man page, grep “print[s] lines that match patterns” — it searches each line of its input against a pattern and, by default, prints the lines that match. Beyond a plain substring search, its flags change what counts as a match and what gets printed. This post runs five of them against a synthetic access log.

The data

All five grep calls below run against the same access.log, 30 lines, one simulated HTTP request per line in a Combined-Log-Format-style layout: client IP, two placeholder dashes, [timestamp], "METHOD PATH HTTP/1.1", status code, response size in bytes — fields separated by single spaces.

203.0.113.5 - - [24/Aug/2026:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:02 +0000] "GET /index.html HTTP/1.1" 200 532
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
198.51.100.23 - - [24/Aug/2026:10:00:05 +0000] "GET /static/app.js HTTP/1.1" 200 8213
192.0.2.77 - - [24/Aug/2026:10:00:07 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:09 +0000] "POST /login HTTP/1.1" 200 356
192.0.2.77 - - [24/Aug/2026:10:00:11 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.23 - - [24/Aug/2026:10:00:12 +0000] "GET /favicon.ico HTTP/1.1" 404 0
203.0.113.5 - - [24/Aug/2026:10:00:14 +0000] "GET /api/users HTTP/1.1" 200 964
198.51.100.9 - - [24/Aug/2026:10:00:16 +0000] "GET /index.html HTTP/1.1" 200 532
192.0.2.77 - - [24/Aug/2026:10:00:18 +0000] "GET /api/orders HTTP/1.1" 500 89
203.0.113.5 - - [24/Aug/2026:10:00:20 +0000] "DELETE /api/orders/44 HTTP/1.1" 204 0
198.51.100.23 - - [24/Aug/2026:10:00:22 +0000] "GET /old-page HTTP/1.1" 301 178
192.0.2.77 - - [24/Aug/2026:10:00:24 +0000] "GET /api/orders HTTP/1.1" 200 1180
203.0.113.5 - - [24/Aug/2026:10:00:26 +0000] "GET /static/app.js HTTP/1.1" 200 8213
198.51.100.9 - - [24/Aug/2026:10:00:28 +0000] "POST /login HTTP/1.1" 401 128
192.0.2.77 - - [24/Aug/2026:10:00:30 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:32 +0000] "GET /admin HTTP/1.1" 403 96
198.51.100.23 - - [24/Aug/2026:10:00:34 +0000] "GET /index.html HTTP/1.1" 200 532
192.0.2.77 - - [24/Aug/2026:10:00:36 +0000] "PUT /api/users/9 HTTP/1.1" 200 210
203.0.113.5 - - [24/Aug/2026:10:00:38 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.9 - - [24/Aug/2026:10:00:40 +0000] "GET /favicon.ico HTTP/1.1" 404 0
192.0.2.77 - - [24/Aug/2026:10:00:42 +0000] "GET /static/app.js HTTP/1.1" 200 8213
203.0.113.5 - - [24/Aug/2026:10:00:44 +0000] "GET /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:46 +0000] "POST /login HTTP/1.1" 200 356
192.0.2.77 - - [24/Aug/2026:10:00:48 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:50 +0000] "GET /old-page HTTP/1.1" 301 178
198.51.100.9 - - [24/Aug/2026:10:00:52 +0000] "GET /api/orders HTTP/1.1" 500 89
192.0.2.77 - - [24/Aug/2026:10:00:54 +0000] "GET /admin HTTP/1.1" 403 96
203.0.113.5 - - [24/Aug/2026:10:00:56 +0000] "GET /index.html HTTP/1.1" 200 532

The code

#!/usr/bin/env bash
set -e

echo "== plain match: lines with /login =="
grep '/login' access.log

echo
echo "== -c: count matching lines =="
grep -c ' 500 ' access.log

echo
echo "== -v: lines that do NOT match 200 =="
grep -v ' 200 ' access.log | head -5

echo
echo "== -E: extended regex, 401 or 403 or 500 =="
grep -E ' (401|403|500) ' access.log

echo
echo "== -o: print only the matched text (status code) =="
grep -oE 'HTTP/1.1" [0-9]+' access.log | head -5

Per the man page:

  • Plain grep 'pattern' — matches lines containing the pattern as a basic regular expression; a literal string like /login matches itself.
  • -c, --count — “Suppress normal output; instead print a count of matching lines.”
  • -v, --invert-match — “Invert the sense of matching, to select non-matching lines.”
  • -E, --extended-regexp — “Interpret PATTERNS as extended regular expressions,” which is what enables (401|403|500)-style alternation without escaping the parentheses/pipe.
  • -o, --only-matching — “Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line,” instead of the whole line.

Running it

Real output:

== plain match: lines with /login ==
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
203.0.113.5 - - [24/Aug/2026:10:00:09 +0000] "POST /login HTTP/1.1" 200 356
198.51.100.9 - - [24/Aug/2026:10:00:28 +0000] "POST /login HTTP/1.1" 401 128
198.51.100.23 - - [24/Aug/2026:10:00:46 +0000] "POST /login HTTP/1.1" 200 356

== -c: count matching lines ==
4

== -v: lines that do NOT match 200 ==
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
192.0.2.77 - - [24/Aug/2026:10:00:11 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.23 - - [24/Aug/2026:10:00:12 +0000] "GET /favicon.ico HTTP/1.1" 404 0
192.0.2.77 - - [24/Aug/2026:10:00:18 +0000] "GET /api/orders HTTP/1.1" 500 89
203.0.113.5 - - [24/Aug/2026:10:00:20 +0000] "DELETE /api/orders/44 HTTP/1.1" 204 0

== -E: extended regex, 401 or 403 or 500 ==
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
192.0.2.77 - - [24/Aug/2026:10:00:11 +0000] "GET /api/orders HTTP/1.1" 500 89
192.0.2.77 - - [24/Aug/2026:10:00:18 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.9 - - [24/Aug/2026:10:00:28 +0000] "POST /login HTTP/1.1" 401 128
203.0.113.5 - - [24/Aug/2026:10:00:32 +0000] "GET /admin HTTP/1.1" 403 96
203.0.113.5 - - [24/Aug/2026:10:00:38 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.9 - - [24/Aug/2026:10:00:52 +0000] "GET /api/orders HTTP/1.1" 500 89
192.0.2.77 - - [24/Aug/2026:10:00:54 +0000] "GET /admin HTTP/1.1" 403 96

== -o: print only the matched text (status code) ==
HTTP/1.1" 200
HTTP/1.1" 200
HTTP/1.1" 401
HTTP/1.1" 200
HTTP/1.1" 200

The plain match found all four /login lines regardless of their status code. -c counted 4 lines matching 500. -v printed every line without 200 in it — 401s, 404s, 500s, a 301 and a 204 all came through. The -E alternation (401|403|500) pulled 8 lines together, matching all three status codes in one pass instead of running grep three separate times. -o didn’t print full lines at all — it printed just the matched HTTP/1.1" <code> fragment from each line.

Takeaway

Plain grep and -v both act on whole lines — one keeping matches, the other keeping everything else — while -E widened what counts as a single pattern and -o narrowed what gets printed down to just the matched text instead of the surrounding line.