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
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.
grep -c ':pattern' app.logThat 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:
the cat sat on the mat near the door
The word the appears three times, all on that one line.
grep -c 'the' quote.txt
# 1grep -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:
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:
grep -o ':pattern' app.log | wc -lThe 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.
grep -rc ':pattern' :search_pathThe output looks like this:
./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:
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.
grep -vc ':pattern' app.logThis 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:
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:
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:
grep -ro 'TODO' . | wc -lgrep -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.
| Behaviour | GNU grep (Linux) | BSD grep (macOS default) |
|---|---|---|
-c counts matching lines | Yes | Yes |
-o prints each match on its own line | Yes | Yes |
-v inverts the match | Yes | Yes |
-c with -o | -c wins, line count | -c wins, line count |
-r follows symlinks | No (use -R) | Yes, by default |
--include / --exclude for scoped counts | Supported | Not supported |
-P (PCRE) patterns | Supported | Not supported |
Empty-pattern occurrence count with -o | Counts 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, orgrep -rlto 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 -lcounts every occurrence including duplicates. For the count of unique matched strings, pipe throughsort -ufirst: 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
jqfor JSON and a proper CSV parser for CSV; grep counting will silently miscount nested or quoted fields.
See also
- grep cheat sheet: the full grep reference covering flags, regex flavors, recursive search, and context lines
- Count unique matches with grep: when you need distinct values, not a raw occurrence total
- List filenames that match with grep -l: when you want which files matched, not how many lines
- External: GNU grep manual, FreeBSD grep(1) man page





