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 q
uiet 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.