185

grep -c is useful for finding how many times a string occurs in a file, but it only counts each occurence once per line. How to count multiple occurences per line?

I'm looking for something more elegant than:

perl -e '$_ = <>; print scalar ( () = m/needle/g ), "\n"'
  • 2
    I know grep is specified, but for anyone using ack, the answer is simply ack -ch <pattern>. – Kyle Strand May 19 '16 at 15:56
264

grep's -o will only output the matches, ignoring lines; wc can count them:

grep -o 'needle' file | wc -l

This will also match 'needles' or 'multineedle'.
Only single words:

grep -o '\bneedle\B' file | wc -l
# or:
grep -o '\<needle\>' file | wc -l
  • 5
    Note that this requires GNU grep (Linux, Cygwin, FreeBSD, OSX). – Gilles May 15 '11 at 14:37
  • @wag What magic does \b and \B do here? – Geek Jun 12 '14 at 8:36
  • 5
    @Geek \b matches a word boundary, \B matches NOT a word boundary. The answer above would be more correct if it used \b at both ends. – Liam Sep 25 '15 at 21:02
  • For a count of occurrences per line, combine with grep -n option and uniq -c ... grep -no '\<needle\>' file | uniq -c – jameswarren Oct 7 '16 at 13:56
  • @jameswarren uniq only removes adjacent identical lines, you need to sort before feeding to uniq if you are not already sure that duplicates will always be immediately adjacent. – tripleee Nov 3 '16 at 12:21
14

If you have GNU grep (always on Linux and Cygwin, occasionally elsewhere), you can count the output lines from grep -o: grep -o needle | wc -l.

With Perl, here are a few ways I find more elegant than yours (even after it's fixed).

perl -lne 'END {print $c} map ++$c, /needle/g'
perl -lne 'END {print $c} $c += s/needle//g'
perl -lne 'END {print $c} ++$c while /needle/g'

With only POSIX tools, one approach, if possible, is to split the input into lines with a single match before passing it to grep. For example, if you're looking for whole words, then first turn every non-word character into a newline.

# equivalent to grep -ow 'needle' | wc -l
tr -c '[:alnum:]' '[\n*]' | grep -c '^needle$'

Otherwise, there's no standard command to do this particular bit of text processing, so you need to turn to sed (if you're a masochist) or awk.

awk '{while (match($0, /set/)) {++c; $0=substr($0, RSTART+RLENGTH)}}
     END {print c}'
sed -n -e 's/set/\n&\n/g' -e 's/^/\n/' -e 's/$/\n/' \
       -e 's/\n[^\n]*\n/\n/g' -e 's/^\n//' -e 's/\n$//' \
       -e '/./p' | wc -l

Here's a simpler solution using sed and grep, which works for strings or even by-the-book regular expressions but fails in a few corner cases with anchored patterns (e.g. it finds two occurrences of ^needle or \bneedle in needleneedle).

sed 's/needle/\n&\n/g' | grep -cx 'needle'

Note that in the sed substitutions above, I used \n to mean a newline. This is standard in the pattern part, but in the replacement text, for portability, substitute backslash-newline for \n.

3

If, like me, you actually wanted "both; each exactly once", (this is actually "either; twice") then it's simple:

grep -E "thing1|thing2" -c

and check for the output 2.

The benefit of this approach (if exactly once is what you want) is that it scales easily.

  • I'm not sure you're actually checking it's only appearing once? All you're looking for there is that either one of those words exist at least once. – Steve Gore Jul 11 '18 at 2:29
2

Another solution using awk and needle as field separator:

awk -F'^needle | needle | needle$' '{c+=NF-1}END{print c}'

If you want to match needle followed by punctuation, change the field separator accordingly i.e.

awk -F'^needle[ ,.?]|[ ,.?]needle[ ,.?]|[ ,.?]needle$' '{c+=NF-1}END{print c}'

Or use the class: [^[:alnum:]] to encompass all non alpha characters.

  • Note that this requires an awk that supports regexp field separators (such as GNU awk). – Gilles May 15 '11 at 14:38
1

Your example only prints out the number of occurrences per-line, and not the total in the file. If that's what you want, something like this might work:

perl -nle '$c+=scalar(()=m/needle/g);END{print $c}' 
  • You are right -- my example only counts the occurences in the first line. – user4518 Feb 6 '11 at 15:49
1

This is my pure bash solution

#!/bin/bash

B=$(for i in $(cat /tmp/a | sort -u); do
echo "$(grep $i /tmp/a | wc -l) $i"
done)

echo "$B" | sort --reverse

protected by Community Jul 7 '17 at 15:06

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?