Reliable way to open files given by the result of find … -exec grep … {} \+

vim $(find path/ -exec grep -l 'pattern' {} +)

is an unquoted command substitution, so word splitting will be performed on whitespace on its result, as well as pathname expansion. I.e., if a file a b matches, Vim will incorrectly open a and b. If a file * matches, alas, that will be expanded to every file in the corresponding directory. An appropriate solution is

find path/ -type f -exec grep -q 'pattern' {} \; -exec vim {} +

Grep runs in quiet mode: Only its return value is used for each file. If 0, a match was found in that file and the file is passed on to Vim.

{} \; means one file will be analysed at a time by Grep. If we used {} +, all files would be passed as arguments to Grep, and a found match in any of those files would result on 0 exit status, so all those files would be opened in Vim. On the other hand, {} + is used for Vim so that it each found file goes to one buffer in a single Vim process. You can try changing them to feel the difference.

If you need to speed things up:

  • If 'pattern' is not a regular expression, but only a fixed pattern, add the -F flag to Grep.

  • grep -lZ, Xargs and special shell constructs should also speed-up the process if you have those available, see Stéphane Chazelas' answer.

And here are another similar use cases with Xargs, Find and Vim.


With GNU tools (your --color=always is a GNU extension already) and a shell with support for Ksh-style process substitution:

xargs -r0a <(grep -rlZ pattern .) vim

Or:

xargs -r0a <(find . ... -exec grep -lZ pattern {} +) vim

With zsh:

vim ${(0)"$(find . ... -exec grep -lZ pattern {} +)"}

With bash 4.4+:

readarray -td '' files < <(find . ... -exec grep -lZ pattern {} +)
vim "${files[@]}"

Those minimise the number of grep invocations that are performed. The point is to tell grep to output the file names NUL-delimited, so they can be reliably split into separate arguments for vim by GNU xargs -0 or zsh's 0 parameter expansion flag or bash's readarray -td ''.

In zsh, you could also do:

vim ./**/*(...e['grep -q pattern $REPLY'])

(where ... stands in for further qualifiers you may want to add, like for the find approach).

That means however that like the approaches that use find -exec grep -q pattern {} ';', one grep invocation would be run per file which would make it significantly slower.

Your first approach would work in zsh provided you replaced --color=always with -Z and changed the value of IFS to IFS=$'\0' from the default of IFS=$' \r\n\0'. I wouldn't work in other shells, as they don't support storing NULs in variables, let alone $IFS and would also perform filename generation on the words resulting of the splitting which you'd need to disable with set -o noglob or set -f.

Why is looping over find's output bad practice? will give you more tips on how to process the list of files found by find reliably.

Tags:

Vim

Grep

Find

Xargs