5

I often run commands like this:

find … -exec grep … --color=always -l {} \+

and sometimes I need to open the matching files in Vim.

But what is the most reliable way to do so?

One way seems to be

vim $(find … -exec grep … {} \+)

Another way seems to make use of xargs.

Are there advantages/disvantages/concerns to be aware for these two, and others, if any, methods?

3
  • 4
    You have already found that colored output is not supposed to be processed. Can't you simply remove --color=always? With -l grep only lists matched files anyway, so you really shouldn't need colors.
    – Quasímodo
    Commented Sep 7, 2020 at 15:26
  • After all you're right, adding -l and removing --color=always takes more or less the same fraction of time. And changing the latter to the former takes even less time. I've modified the question to get some more knowledge about the command substitution approach vs the xargs approach.
    – Enlico
    Commented Sep 7, 2020 at 15:37
  • Does this answer your question? How to edit a list of generated files whose names contain spaces Commented Sep 7, 2020 at 16:50

3 Answers 3

13
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.

0
6

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.

2

Two different approaches:

  1. Put the results in a quickfix list:
find ... -exec grep -Hn {} + >results
vim -q results

Or the cleaner (but non-sh)

vim -q <(find ... -exec grep -Hn {} +)
  1. Use :grep -R and :Cfilter:
vim
:grep -R ...
:packadd cfilter
:Cfilter[!]

See the relevant :help for some of the commands.

Either way, you can navigate matches with :cnext and the other :help quickfix commands.

2
  • Thank you Ben for sharing a Vim-centric approach. I believe it assumes some restrictions in the filenames (for example, no newline characters), correct?
    – Quasímodo
    Commented Sep 9, 2020 at 12:29
  • @Quasímodo that’s probably correct—but then again, it’s a vim assumption. One might be able to play with errorformat to avoid said assumption. Commented Sep 9, 2020 at 12:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .