Signal9Signal9 — home
online
all lessons

Searching Inside Files: grep & Regular Expressions

Beginner25 min5 steps
What you'll learn
  • Use grep to print the lines that match a pattern, and reach for -i, -n, -r, -v, -c, -l when you need them
  • Explain why grep's pattern is a regular expression, not a shell glob — and why . and * mean different things than you expect
  • Build patterns from the regex toolkit: anchors (^ $), character classes ([...]), the any-char dot, and quantifiers (* + ? with -E)
  • Combine grep with pipes and find to filter command output and search a whole tree of files
  1. grep prints the lines that match

    Learn grep's basic shape and the everyday flags — and see how it differs from find.

    You've seen how find locates files by their attributes. `grep` answers the other half of the question: it looks inside files and prints the lines that match a pattern. Where find hands you filenames, grep hands you the matching text.

    The basic shape is `grep PATTERN [file…]`. grep reads the file line by line and prints every line that contains the pattern:

    • Give it a file and it searches that file.
    • Give it no file (or a -) and it reads from standard input — which is what makes some-command | grep PATTERN the everyday way to filter another command's output.

    A handful of flags cover almost everything you'll do:

    • `-i` — ignore case (error also matches Error, ERROR).
    • `-n` — prefix each hit with its line number.
    • `-r` — search recursively through a whole directory tree (like find + grep in one).
    • `-v` — invert: print the lines that don't match.
    • `-c` — print a count of matching lines instead of the lines.
    • `-l` — print just the filenames that contain a match, not the lines.

    Example — not runnable on your lab box

    grep -n 'error' /var/log/app.log
    42:error: failed to open socket
    87:error: retry limit reached
    Print every line of app.log containing 'error', each with its line number (-n). grep read the whole file and kept only the two matching lines — the fastest way to pull the signal out of a noisy log.
    ps aux | grep -v grep | grep nginx
    www-data  8123  0.0  0.4 nginx: worker process
    Filter another command's output through a pipe: take the process list, drop grep's own line (-v grep), then keep only nginx rows. With no filename, grep reads standard input — that's why it pairs so naturally with |.
  2. The pattern is a regex, not a glob

    Grasp the single most important idea: grep's pattern is a regular expression, where the metacharacters mean different things than shell globs.

    Here is the idea that trips up everyone coming from shell globbing. In the Quoting & Globbing lesson you learned the shell's * means "any run of characters" in a filename. *grep's pattern is not a glob — it's a regular expression (regex)*, and the same symbols mean something different:

    • `.` matches any single character (in a glob, . is just a literal dot).
    • *`** means **"zero or more of the thing before it"** — it modifies the *previous* character, it does not stand alone for "anything". So a` means "zero or more a's", and `.` (dot-star) is the regex way to say "any run of characters".

    That one difference is the whole gotcha. In a glob, a* matches names starting with a. In a regex, a* matches zero or more as — which every line technically has, so grep 'a*' matches everything. To mean "any characters" in grep you write *`.**, not *`.

    And just like with globs, you quote the pattern — grep 'a.*b' file — so the shell doesn't try to expand any * or other special characters before grep ever sees them. Quoting hands the pattern to grep untouched; grep then interprets it as a regex.

    printf 'cat\ncot\ncart\ndog\n' | grep 'c.t'
    cat
    cot
    The regex c.t means 'c, then any single character, then t'. It matches 'cat' and 'cot' but NOT 'cart' (two characters between c and t) or 'dog'. The dot is exactly one character — that's the regex . in action.

    Check yourself

    In a grep pattern, what does the dot ( . ) match?

  3. The regex toolkit: anchors, classes, quantifiers

    Build real patterns from the handful of pieces that cover most searches.

    Most useful patterns come from combining a small set of pieces:

    • Anchors tie a match to a position. `^` means "start of line", `$` means "end of line". So ^error matches lines that begin with error, and done$ matches lines that end with done. ^$ matches an empty line.
    • Character classes match one character from a set. `[abc]` matches an a, b, or c; `[0-9]` any digit; `[a-z]` any lowercase letter. Put `^` first inside the brackets to negate: `[^0-9]` matches any character that is not a digit.
    • The dot `.` matches any single character (from the last step).
    • Quantifiers say how many. *`** means "zero or more of the previous". With **-E** (extended regex) you also get **+** (one or more), **?** (zero or one), **{2,4}** (between 2 and 4), **|** (either/or), and **( )`** to group.
    • Word boundaries `\b` match the empty position at the edge of a word — where a word character (a letter, digit, or _) meets a non-word character or the start or end of the line. \bthe\b matches the word the on its own but not the the inside there, other, or theme — the way to match a whole word without pinning to the whole line as ^…$ does.

    A note on -E: basic grep supports ^ $ . [] *. The handier operators + ? | ( ) {} require `grep -E` (or the older name egrep). When a pattern isn't matching what you expect, a missing -E is a common reason.

    printf 'GET /a 200\nPOST /b 404\nGET /c 500\n' | grep -E '(404|500)$'
    POST /b 404
    GET /c 500
    The extended pattern (404|500)$ means 'either 404 or 500, at the end of the line'. The | alternation and the ( ) grouping both need -E; the $ anchors the match to the line's end, so a 404 appearing mid-line wouldn't count.
    printf 'id: 7\nid: 42\nid: x\n' | grep -E 'id: [0-9]+'
    id: 7
    id: 42
    [0-9]+ means 'one or more digits' (the + needs -E). It matches the numeric ids and rejects 'id: x', because x is not a digit. Character class plus quantifier is the everyday way to match 'a number'.
  4. grep with pipes and find: searching at scale

    Filter command output and search whole trees by combining grep with | and find.

    grep earns its keep when you point it at many lines or many files:

    • Filter a command's output — command | grep PATTERN. Because grep reads standard input, it drops into any pipeline to keep only the interesting lines. Stack them: command | grep X | grep -v Y.
    • Search a whole tree — `grep -r PATTERN dir` walks dir and every subdirectory, searching all files. Add -n for line numbers and -l to list just the files. This is the "where in my project is this string?" command.
    • Pair with find when you want to search only certain files: find . -name '*.conf' -print0 | xargs -0 grep -l PATTERN finds config files, then greps inside them for the ones containing the pattern (the -print0 | xargs -0 pair keeps it safe when names have spaces, straight from the Finding Files lesson).
    • `-o` prints only the matched text, not the whole line — handy for pulling values out: grep -oE '[0-9]+' prints just the numbers it finds.

    Between grep -r for "search everything here" and find … | xargs grep for "search a chosen set", you can locate any string across a codebase or a log directory in one line.

    Example — not runnable on your lab box

    grep -rn 'TODO' src/
    src/parser.c:88:  // TODO: handle empty input
    src/main.c:12:  // TODO: parse flags
    Recurse through src/ (-r), show line numbers (-n), and print every line containing TODO along with its file and line. This one command is how you find every loose end in a whole project at once.
    find . -name '*.log' -print0 | xargs -0 grep -l 'timeout'
    Find the .log files, then hand them to grep -l to print only those that CONTAIN 'timeout'. find chooses which files; grep searches inside them. The -print0/-0 pair is the space-safe hand-off. Your terminal supplies the matching filenames.

    Check yourself

    You want to search every file under the current directory for the word 'password' and see line numbers. Which single grep command does it?

  5. Now do it — search text with flags and a regex

    Put it together: make a file, then pull lines out of it by flag and by pattern.

    One continuous experiment. You'll build a small log, then search it by plain text, by flag, and by regular expression — the everyday grep workflow end to end.

    printf 'INFO start\nERROR disk full\ninfo retry\nERROR timeout\nWARN slow\n' > demo.log && grep 'ERROR' demo.log
    ERROR disk full
    ERROR timeout
    Create a five-line log, then print the lines containing ERROR. Plain grep is case-sensitive, so the lowercase 'info' line and the WARN line are left out — only the exact-case ERROR lines match.
    grep -in 'info' demo.log; echo '--- count of ERROR lines ---'; grep -c 'ERROR' demo.log
    1:INFO start
    3:info retry
    --- count of ERROR lines ---
    2
    grep -in 'info' matches BOTH 'INFO' and 'info' (-i ignores case) and shows line numbers (-n). Then grep -c 'ERROR' reports 2 — the number of matching lines, not the lines themselves.
    grep -E '^(ERROR|WARN)' demo.log; echo '--- lines NOT starting with a capital ---'; grep -v '^[A-Z]' demo.log
    ERROR disk full
    ERROR timeout
    WARN slow
    --- lines NOT starting with a capital ---
    info retry
    ^(ERROR|WARN) anchors the match to the start of the line and accepts either word (alternation needs -E). Then -v '^[A-Z]' inverts the match to show lines that do NOT begin with a capital letter — only the lowercase 'info retry' line qualifies.
    grep -oE '[a-z]+$' demo.log; echo '--- cleaning up ---'; rm demo.log
    start
    full
    retry
    timeout
    slow
    --- cleaning up ---
    -oE '[a-z]+$' prints ONLY the matched text (-o): a run of one-or-more lowercase letters anchored to the end of the line. You've now searched by text, case-insensitively, by count, by anchored alternation, by inversion, and pulled out just the match — the whole grep toolkit in one sitting.
Recap
  • grep PATTERN [file] prints the lines that match; with no file it reads standard input, which is why command | grep PATTERN filters output. Key flags: -i (ignore case), -n (line numbers), -r (recurse), -v (invert), -c (count), -l (list filenames).
  • grep's pattern is a regular expression, not a shell glob. Regex . = any single character; regex = zero-or-more of the PREVIOUS character. To mean 'any run of characters' write . — quote the pattern so the shell doesn't touch it.
  • Build patterns from anchors (^ start, $ end), character classes ([0-9], [a-z], [^…] to negate), the dot, and quantifiers (* everywhere; + ? | ( ) {} with -E / egrep). Anchors are how you stop a pattern from over-matching.
  • Search at scale by combining grep with pipes (cmd | grep) and with find (find … -print0 | xargs -0 grep); grep -rn PATTERN . searches a whole tree, and grep -o prints just the matched text.
grep, at a glance
You writeEffect
grep 'error' fileprint lines of file containing error
cmd | grep -v skipkeep the output lines that DON'T match skip
grep -rn 'TODO' .recurse the tree, print matches with line numbers
grep -i 'warn' filematch warn/WARN/Warn (case-insensitive)
grep '^error' / 'done$'lines starting with error / ending with done
grep -E '[0-9]+'one or more digits (+ needs -E)
grep -oE '[a-z]+'print only the matched runs of lowercase letters