50

I found out about Vim's substitute command...

:%s/replaceme/replacement/gi

And vimgrep...

:vimgrep  /findme/gj  project/**/*.rb

Is there a way to combine them in order to do a replacement across all the files under a directory?

Keith Pinson
  • 7,835
  • 7
  • 61
  • 104
Ethan
  • 57,819
  • 63
  • 187
  • 237
  • [Here](http://stackoverflow.com/a/13944042/834176)'s a way to search and replace recursively, but only on files under version control (it could be generalized easily if you really do want to search *all* files under a directory). – Keith Pinson Dec 19 '12 at 01:06

8 Answers8

81

args and argdo should do what you need, e.g.

:args spec/javascripts/**/*.* 
:argdo %s/foo/bar/g

See this page.

Jonas G. Drange
  • 8,749
  • 2
  • 27
  • 38
meleyal
  • 32,252
  • 24
  • 73
  • 79
77

Even though I'm a Vim user, I generally use find and sed for this sort of thing. The regex syntax for sed is similar to Vim's (they're both descendants of ed), though not identical. With GNU sed you can also use -i for in-place edits (normally sed emits the modified version to stdout).

For example:

find project -name '*.rb' -type f -exec sed -i -e 's/regex/replacement/g' -- {} +

Piece by piece:

  • project = search in "project" tree
  • -name '*.rb' = for things whose names match '*.rb'
  • -type f = and are regular files (not directories, pipes, symlinks, etc.)
  • -exec sed = run sed with these arguments:
    • -i = with in-place edits
    • -e 's/regex/replacement/g' = execute a "substitute" command (which is almost identical to Vim's :s, but note lack of % -- it's the default in sed)
    • -- = end of flags, filenames start here
    • {} = this tells find that the filenames it found should be placed on sed's command-line here
    • + = this tells find that the -exec action is finished, and we want to group the arguments into as few sed invocations as possible (generally more efficient than running it once per filename). To run it once per filename you can use \; instead of +.

This is the general solution with GNU sed and find. This can be shortened a bit in special cases. For example, if you know that your name pattern will not match any directories you can leave out -type f. If you know that none of your files start with a - you can leave out --. Here's an answer I posted to another question with more details on passing filenames found with find to other commands.

Laurence Gonsalves
  • 137,896
  • 35
  • 246
  • 299
  • 9
    Make sure and test your `find` before giving it to `sed` - this can be really bad if it's overly inclusive. It's also a good idea to give `sed` the `-c` (`--copy`) option, which will keep it from messing with read-only files. – Cascabel Jan 21 '10 at 23:06
  • @BroSlow Fair enough. I've updated the answer to be safer/more-general. (That said, on Linux at least ARG_MAX is pretty huge, and for the use case described in the question, filenames with spaces or special characters would be pretty odd.) – Laurence Gonsalves Nov 10 '14 at 00:49
  • 2
    If you use this method you can't use the conformation because sed does not support it, you can use `vim -c '%/regex/replacement/gc' -c 'wq' -- {} \;` that way you can confirm your changes also – Funonly Apr 23 '15 at 09:22
  • @LaurenceGonsalves Can I integrate this command into Vim, so it is there when I need it? Maybe a shorcut or custom function? – Gjaa Aug 22 '19 at 18:31
  • @Gjaa yes, but how would you want to parameterize it? Personally, if I did it that often I'd probably stick it into a shell script, and then maybe call that script from Vim. – Laurence Gonsalves Aug 22 '19 at 19:39
  • @LaurenceGonsalves I want it in a function in my `.vimrc` for uses in future projects (I reuse my current customized `.vimrc` in other computers), so it would be more convenient to have it in there. I want something like `:replaceall path/to/folder 'filecriteria*.php' 'replace this' 'with this'` – Gjaa Aug 22 '19 at 22:21
8

You could run vimgrep then record a macro that goes to each line in the vimgrep output and does the replace, something like this: (dont type in the # comments!)

:set autowrite                  #automatically save the file when we change buffers
:vimgrep /pattern/ ./**/files  
qa                              #start recording macro in register a
:s/pattern/replace/g            #replace on the current line
:cnext                          #go to next matching line
q                               #stop recording
10000@a                          #repeat the macro 10000 times, or until stopped by 
                                #an "error" such as reaching the end of the list
:set noautowrite

I have not tested it, so it may need a little tweaking - test it on some files that you dont mind getting messed up first.

The advantage of this over sed is that Vim regex are more powerful, and you can add the c option to :s to verify each replacement.

Edit: I modified my original post since it was wrong. I was using :1vimgrep to find the first match in each file, but I misread the docs - it only finds one match across all files.

Dave Kirby
  • 25,806
  • 5
  • 67
  • 84
  • Cool! In searching for a single expression solution I never stopped to consider a macro... Makes me feel kind of silly now. +1. :-) – Michał Marczyk Jan 23 '10 at 14:36
  • You just saved me HOURS of work, thank you!!! :D I needed to copy a value in an xml file to another part of the file, then lower case it - across 200+ files. – Jason May 04 '12 at 15:32
  • This can be greatly simplified by using `:cfdo %s/pattern/replace/g | update` after doing vimgrep - no need to use macros. –  Jun 21 '22 at 09:30
8

To answer the original question of combining vimgrep and substitute:

:vimgrep /pattern/ ./**/files              # results go to quickfix list
:set autowrite                             # auto-save when changing buffers
:silent! cdo %s/replaceme/replacement/gic  # cdo draws filenames from quickfix list
:set noautowrite

Leave out the c of gic if you don't want to confirm each replacement (in which case you could also use find and sed as suggested by @LaurenceGonsalves).

proton1
  • 81
  • 1
  • 3
  • Very useful! This recursive search/replace also works in iVim on iOS, which cannot fork and so cannot use other command line solutions. – arcdale Dec 20 '19 at 07:44
6

One way would be to open all the files you want to run the substitution on (or any command), and run :bufdo <cmd>, which will run <cmd> on all open buffers.

hgmnz
  • 13,208
  • 4
  • 37
  • 41
4

There's a plugin which endeavours to do just that: Vimgrep Replace.

Oh, wait... there are others: Global Replace, EasyGrep.

For a no plugin solution, perhaps argdo would help if you could get vimgrep's output onto the argument list (which can be set with args), but I can't seem to figure out the details. I'd be happy if somebody took the idea and improved upon it... so here it is.

The basic idea behind the first plugin (I'd guess the others too...) is to use vimgrep first, then cycle through the matches with :cnext and apply the substitution command on each line. A function to accomplish this could be small enough to put it in .vimrc. (Maybe you can lift one from the plugins' sources?)

(I suppose hgimenez might be on to a solution, but whether or not it's appropriate would probably depend on the number of files to be processed... The plugins should be ok regardless.)

Michał Marczyk
  • 83,634
  • 13
  • 201
  • 212
  • 1
    Thanks, but arrrrgh! Is there a way to do it without installing another #!*&% plugin? – Ethan Jan 21 '10 at 21:48
  • @Ethan: I feel for you. :-) I think there must be a way involving :args, :argdo, :copen or whatever... I just don't use vimgrep enough to have thought much about it before and can't seem to be able to put together the perfect fix for you just yet. But I'm still trying, so perhaps I'll come up with something and edit. – Michał Marczyk Jan 21 '10 at 21:52
  • Oh well... I really can't seem to find a clean & simple no-plugin solution or conclusively assure myself that there isn't any. My Vim-fu is failing me! :-( The plugins do seem nice, though, perhaps they'd be worth a try... – Michał Marczyk Jan 21 '10 at 22:32
  • If you really want to do it inside vim, just launch it with all the files you want on the command line, then use `bufdo` or `windo` as appropriate. – Cascabel Jan 21 '10 at 23:04
  • @Michal: Thanks, I guess it isn't really essential to do the operation within Vim. – Ethan Jan 22 '10 at 06:30
  • @Ethan: Too late, you got me curious... ;-) Good to know your problem's solved, though. – Michał Marczyk Jan 22 '10 at 06:45
  • I just tested out EasyGrep on a large project. Wow, it is slow. – Gavin Oct 13 '11 at 04:36
  • @Gavin Yes, that thing seem to not support using a filter while doing grep. Check my answer for a simplified solution to the problem that allows you to supply a filter (Neovim only). – Martin Braun Jun 16 '23 at 17:09
1

This works for a small number of files.

vim ` find . -type f `

:bufdo %s/pattern/replacement/gec | update

rado
  • 4,040
  • 3
  • 32
  • 26
0

Here is my Neovim user command that will either grep or grep and replace. It will :vimgrep and show results in the quickfix list window, so you can navigate and review changes that have been made. You can apply all changes afterwards with :wall:

-- Grep
vim.api.nvim_create_user_command(
'Grep',
function(opts)
    local usage = "Usage: :Grep <regex> <filter?>, i.e. :Grep /my_string/g **/*, or :Grep! s/my_string/my_replacement/g **/*"
    if not opts.args or string.len(opts.args) < 1 then
        print(usage)
        return
    end
    local delim = string.sub(opts.args, 1, 1)
    if delim == "s" then
        delim = string.sub(opts.args, 2, 2)
    end
    local pargs = vim.split(opts.args, delim)
    if #pargs < 3 then
        print(usage)
        return
    end
    local searchpatt = pargs[2]
    if pargs[1] == "s" and #pargs < 4 then
        print(usage)
        return
    end
    local replpatt = nil
    local flags = nil
    local filter = ""
    if pargs[1] == "s" then
        if not opts.bang then
            print(usage)
            return
        end
        replpatt = pargs[3]
        local bridge = vim.split(pargs[4], " ")
        flags = bridge[1]
        filter = bridge[2] or ""
        for i = 5, #pargs do
            filter = filter .. delim.. pargs[i]
        end
    else
        if opts.bang then
            print(usage)
            return
        end
        local bridge = vim.split(pargs[3], " ")
        flags = bridge[1]
        filter = bridge[2] or ""
        for i = 4, #pargs do
            filter = filter .. delim .. pargs[i]
        end
    end
    -- print("d:" .. delim .. "\ns:" .. searchpatt .. "\nr:" .. (replpatt or "nil") .. "\nm:" .. flags .. "\nf:" .. filter)
    if searchpatt and flags and filter then
        vim.cmd(":vimgrep /" .. searchpatt .. "/" .. flags .. "j " .. filter)
        vim.cmd(":cw")
        vim.cmd(":.cc")
        if replpatt then
            vim.cmd(":cfdo %s/" .. searchpatt .. "/" .. replpatt .. "/" .. flags .. "e")
        end
    end
end,
{ nargs = '*', bang = true }
)

You should not escape spaces in the regex expression. The g flag is mandatory and the s flag is mandatory in combination with the bang (!) which will introduce the destructive replace operation.

You can navigate the quickfix list using :cn / :cp and you can close it with :ccl.

This article helped me to build this: https://jdhao.github.io/2020/03/14/nvim_search_replace_multiple_file/

Martin Braun
  • 10,906
  • 9
  • 64
  • 105