If you're searching on the command line for files that contain a certain word or pattern,
a tool like grep
(or one of its descendants like ripgrep
, ack
, ag
, ...) is your friend.
But what if you want to find files containing multiple words?
For example, you want to find all files containing both "foo" and "bar", possibly on different lines.
In the most simple case, you might get away with a pattern along the lines of foo.*bar|bar.*foo
,
but that approach does not scale well,
especially if the words might be multiple lines apart.
Basic Approach
An alternative is to chain multiple grep
commands together.
For example when searching for files that containing "foo", "bar" and "meh":
grep -rl foo . | xargs grep -l bar | xargs grep -l meh
The -l
flag tells grep
to only print the filenames of the files that match the pattern.
The xargs
command takes the output of the previous grep
command and feeds it as file arguments to the next grep
command.
At the end if this pipeline, you get a list of files that contain all three words.
Drop the -l
from the last grep
to see the actual lines containing the last words.
FYI, if you prefer ripgrep
, which supports the same -l
flag,
it's even a bit simpler as recursive search is its default behavior:
rg -l foo | xargs rg -l bar | xargs rg -l meh
Oh no, paths with spaces!
If you have directories or files with spaces in their names, the approach above might break down and you'll get errors like
No such file or directory (os error 2)
To work around this, you have to add some additional flags to keep the pipeline working.
With grep
:
grep -rlZ foo . | xargs -0 grep -lZ bar | xargs -0 grep -l meh
The -Z
flag (--null
in full) tells grep
to separate the filenames with a null character (instead of a newline)
and the -0
flag for xargs
makes sure it is adapted to that format.
Note that the last grep
does not need the -Z
flag, to make the final output human readable.
With ripgrep
you have to use the -0
flag instead of -Z
:
rg -l0 foo | xargs -0 rg -l0 bar | xargs -0 rg -l meh