TechEarl

How to Count Matches with grep -c (and the Line-vs-Occurrence Trap)

grep -c counts matching LINES, not occurrences. A line with three hits still counts as 1. The fix is grep -o piped into wc -l, which puts every match on its own line first. Per-file counts, filtering out the :0 noise, counting non-matching lines, and the BSD vs GNU differences.

Ishan KarunaratneIshan Karunaratne⏱️ 12 min readUpdated
grep -c counts matching lines, not occurrences. Use grep -o piped into wc -l for the true count, grep -rc for per-file counts, grep -vc to count non-matching lines, plus the macOS BSD vs GNU differences.

grep -c 'pattern' file counts how many lines match. That word matters more than it looks. If one line contains the pattern three times, grep -c adds 1, not 3. The flag is a line counter wearing a match-counter costume, and the gap between the two is the single most common surprise I see people hit with grep.

When you actually want the number of occurrences, the pattern is grep -o 'pattern' file | wc -l: -o prints each match on its own line, and wc -l counts those lines. This page covers both, plus per-file counts for recursive searches, counting the lines that do not match, totalling across a whole tree, and the macOS BSD vs GNU differences that change the numbers underneath you.

Set your values

Try it with your own values

Set your OS, search path, and the pattern. Every grep example below updates with your values.

The one-liner

Count matching lines in a single file.

bash· Linux (GNU)
grep -c ':pattern' app.log

That prints a single integer: the number of lines in app.log that contain at least one match. It is fast, it is what most quick checks want, and it is correct as long as you remember the word "lines".

The line-vs-occurrence trap

Here is the trap in two commands. Take a file quote.txt with one line:

code
the cat sat on the mat near the door

The word the appears three times, all on that one line.

bash
grep -c 'the' quote.txt
# 1

grep -c returns 1, because exactly one line matched. It stops counting that line the moment it finds the first hit. The other two thes on the same line are invisible to -c.

To count occurrences, you need each match on its own line first. That is what -o does:

bash
grep -o 'the' quote.txt
# the
# the
# the
grep -o 'the' quote.txt | wc -l
# 3

-o ("only matching") prints just the matched substring, once per match, each on its own line. Pipe that into wc -l and you get the true occurrence count. This is the pattern to memorise:

bash· Linux (GNU)
grep -o ':pattern' app.log | wc -l

The Windows side has the same split. (Select-String ...).Count counts matching lines like grep -c. Adding -AllMatches and counting .Matches counts occurrences like grep -o | wc -l. Same trap, same fix, different syntax.

One subtlety with -o: it counts non-overlapping matches. The pattern aa against the string aaaa matches twice (aa then aa), not three times. grep consumes each match before looking for the next, so overlapping hits are never double-counted. For literal words this rarely matters; for regex with repeated characters, it can.

Per-file counts when searching recursively

Add -r and -c together and grep prints one file:count line per file it searched.

bash· Linux (GNU)
grep -rc ':pattern' :search_path

The output looks like this:

code
./src/api.ts:4
./src/utils.ts:0
./src/index.ts:1
./README.md:0

Every count is still a line count. ./src/api.ts:4 means four matching lines in that file, not four occurrences. If you need recursive occurrence counts, switch -c for -o and total with awk (see the totalling section below).

Filter out the :0 noise

The annoying part of grep -rc is that it prints a :0 line for every file with no matches. In a large tree that is most of the output. The fix is to pipe through grep again and drop the zero-count lines:

bash
grep -rc 'TODO' . | grep -v ':0$'

The -v inverts the match, so grep -v ':0$' keeps only the lines that do not end in :0. Now you see just the files that actually contain a match, each with its count.

A cleaner alternative on GNU grep is to skip counting non-matching files entirely. Pair -r with -l to list matching files, but if you want counts, the -v ':0$' filter above is the portable answer that works on BSD grep too.

Count the lines that do NOT match

-v inverts the match; combine it with -c and you count non-matching lines.

bash· Linux (GNU)
grep -vc ':pattern' app.log

This answers questions like "how many lines in this log are not errors" or "how many config lines are not comments". A useful sanity check: grep -c plus grep -vc for the same pattern should equal the total line count of the file (wc -l). If it does not, something is reading the file differently, often a missing trailing newline.

Total across all files

grep -rc gives you per-file counts but no grand total. To sum them, pipe into awk and split each file:count line on the colon:

bash
grep -rc 'TODO' . | awk -F: '{s+=$2} END {print s}'

-F: sets the field separator to a colon, so $2 is the count. awk accumulates it into s and prints the sum at the end. This still totals matching lines, because -rc counts lines.

For a true occurrence total across the tree, count with -o and -r together, which prints each match prefixed with its filename, then count those output lines:

bash
grep -roc 'TODO' . | awk -F: '{s+=$2} END {print s}'

A note on -roc: GNU grep accepts -o and -c together and -c wins, so that exact combination still gives line counts. The reliable occurrence total is to skip -c entirely and let wc do the counting:

bash
grep -ro 'TODO' . | wc -l

grep -ro prints one file:match line per occurrence, and wc -l counts every one of them. That is the recursive equivalent of the grep -o ... | wc -l one-liner.

macOS BSD grep vs GNU grep

The counting flags themselves are portable, but the surrounding behaviour diverges on macOS, which ships BSD grep by default.

BehaviourGNU grep (Linux)BSD grep (macOS default)
-c counts matching linesYesYes
-o prints each match on its own lineYesYes
-v inverts the matchYesYes
-c with -o-c wins, line count-c wins, line count
-r follows symlinksNo (use -R)Yes, by default
--include / --exclude for scoped countsSupportedNot supported
-P (PCRE) patternsSupportedNot supported
Empty-pattern occurrence count with -oCounts nothing for ''Same

The counting commands on this page work identically on both. The thing that changes the numbers is recursion: BSD -r follows symlinks and GNU -r does not, so a tree with symlinked directories can produce different totals depending on the host. If a script's counts must match across platforms, be explicit: use -R to force symlink-following everywhere, or install GNU grep on macOS with brew install grep and call it as ggrep.

Common grep counting mistakes

1. Expecting -c to count occurrences. It counts lines. A line with five matches contributes 1. This is the headline trap. When you want occurrences, use grep -o 'pattern' file | wc -l.

2. Thinking -c with -o multiplies. Passing both does not give you per-line occurrence counts. -c takes priority and you still get a line count. The two flags do not combine into "occurrences"; you have to drop -c and count the -o output yourself.

3. Ignoring the :0 lines in recursive counts. grep -rc prints a zero for every file it searched, matched or not. In a real repository that is pages of path:0. Always filter with grep -v ':0$' when you only care about files that have hits.

4. Forgetting -c returns exit status separately from the count. grep -c prints 0 and exits with status 1 when there are no matches. In a script with set -e, a legitimate grep -c that finds nothing will abort the script. Wrap it with || true or test the count value instead of relying on exit status.

5. Counting overlapping matches and being surprised by -o. -o finds non-overlapping matches. grep -o 'aa' against aaaa reports two, not three. For patterns where overlap matters, grep is the wrong tool; reach for a script with an explicit overlapping search.

6. Counting before checking line endings. A file with \r\n (Windows) line endings or no final newline can make grep -c and wc -l disagree. If your counts look off by one, inspect the file with file or cat -A.

When NOT to use this

Counting is the right move when you need a number. It is the wrong move when you actually need the matched text:

  • You need to see the matches, not count them. If the goal is "show me every TODO and where it is", use grep -rn 'TODO' . for line numbers, or grep -rl to list filenames. A bare count tells you how many but not which.
  • You want distinct values, not a raw total. grep -o 'pattern' file | wc -l counts every occurrence including duplicates. For the count of unique matched strings, pipe through sort -u first: that is its own pattern, covered in grep count unique matches.
  • You want a frequency breakdown. "Top 10 error codes" is not one number; it is grep -oE 'pattern' file | sort | uniq -c | sort -rn. A single count collapses the distribution you actually care about.
  • You are counting structured data. For JSON or CSV, a line-oriented or substring-oriented count is fragile. Use jq for JSON and a proper CSV parser for CSV; grep counting will silently miscount nested or quoted fields.

See also

FAQ

TagsgrepCLILinuxmacOSBSDShell ScriptingLog Analysis
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

The grep -o | sort | uniq -c | sort -rn pipeline counts unique matches and ranks them. Why sort comes before uniq, worked log-analysis examples, sort -u, uniq -d, and the awk one-pass alternative.

How to Count Unique Matches with grep, sort, and uniq

The grep -o 'pattern' file | sort | uniq -c | sort -rn pipeline is the classic log-analysis one-liner. Why sort must come before uniq, how each stage works, worked examples for top IPs and status codes, the awk one-pass alternative for huge files, and the BSD vs GNU notes.

Use grep -v 'pattern' file to print every line that does not match. Exclude multiple patterns with -e or -vE, strip comments and blank lines, count with -vc, and avoid the OR-becomes-AND double-negative trap.

How to Exclude Matches with grep -v (Invert Match)

grep -v 'pattern' file prints every line that does NOT match. The flag reference, how to exclude multiple patterns, the strip-comments-and-blank-lines pipeline, the double-negative trap where -v of an OR becomes an AND of negations, and the macOS BSD vs GNU differences.