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