Как я могу найти и заменить рекурсивно в каталоге в Vim?

Я узнал о заместительной команде Вима...

:%s/replaceme/replacement/gi

И вимгреп...

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

Есть ли способ объединить их, чтобы сделать замену для всех файлов в каталоге?

8 ответов

Решение

Несмотря на то, что я пользователь Vim, я обычно использую find а также sed для такого рода вещей. Синтаксис регулярного выражения для sed похож на Вима (они оба потомки ed), хотя и не идентичны. С GNU sed вы также можете использовать -i для редактирования на месте (обычно sed испускает модифицированную версию в стандартный вывод).

Например:

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

Кусочек за кусочком:

  • project = поиск в дереве проекта
  • -name '*.rb' = для вещей, имена которых соответствуют '*.rb'
  • -type f = и являются обычными файлами (не каталогами, каналами, символическими ссылками и т. д.)
  • -exec sed = бежать sed с этими аргументами:
    • -i = с правками на месте
    • -e 's/regex/replacement/g' = выполнить команду "заменить" (которая почти идентична команде Vim :s, но обратите внимание на отсутствие % - это по умолчанию в sed)
    • -- = конец флагов, имена файлов начинаются здесь
    • {} = это говорит find что найденные им имена должны быть помещены в командную строку sed здесь
    • + = это говорит find что -exec действие завершено, и мы хотим сгруппировать аргументы как можно меньше sed вызовы, насколько это возможно (обычно более эффективно, чем запускать их один раз для каждого имени файла). Чтобы запустить его один раз для каждого файла, вы можете использовать \; вместо +,

Это общее решение с GNU sed а также find, Это может быть немного сокращено в особых случаях. Например, если вы знаете, что ваш шаблон имени не будет совпадать ни с одним каталогом, вы можете пропустить -type f, Если вы знаете, что ни один из ваших файлов не начинается с - Вы можете оставить --, Вот ответ, который я отправил на другой вопрос с более подробной информацией о передаче имен файлов, найденных с find другим командам.

args а также argdo должен делать то, что вам нужно, например

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

Смотрите эту страницу.

Вы можете запустить vimgrep, затем записать макрос, который идет к каждой строке в выводе vimgrep и выполняет замену, что-то вроде этого: (не вводите # комментариев!)

: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

Я не проверял его, поэтому может потребоваться небольшая настройка - протестируйте его на некоторых файлах, которые вы не против сначала испортить.

Преимущество этого перед sed в том, что регулярные выражения Vim более мощные, и вы можете добавить опцию c в: s, чтобы проверить каждую замену.

Изменить: я изменил свой оригинальный пост, так как это было неправильно. Я использовал:1vimgrep, чтобы найти первое совпадение в каждом файле, но я неправильно прочитал документы - он находит только одно совпадение во всех файлах.

Чтобы ответить на исходный вопрос о сочетании vimgrep и заменителя:

: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

Оставьте c из gic если вы не хотите подтверждать каждую замену (в этом случае вы также можете использовать find а также sed как было предложено @LaurenceGonsalves).

Один из способов - открыть все файлы, на которых вы хотите выполнить подстановку (или любую команду), и запустить :bufdo <cmd>, который будет работать <cmd> на всех открытых буферах.

Есть плагин, который пытается это сделать: Vimgrep Replace.

Ой, подождите... есть другие: Global Replace, EasyGrep.

Для решения без плагинов, возможно argdo помогло бы, если бы вы могли получить vimgrepвывод на список аргументов (который может быть установлен с помощью args), но я не могу выяснить детали. Я был бы счастлив, если бы кто-то взял идею и улучшил ее... так что вот она.

Основная идея первого плагина (я думаю, другие тоже) заключается в использовании vimgrep сначала, а затем циклически проходить матчи с :cnext и примените команду замещения в каждой строке. Функция для этого может быть достаточно маленькой, чтобы поместить ее в.vimrc. (Может быть, вы можете поднять один из источников плагинов?)

(Я полагаю, что hgimenez может решить проблему, но, возможно, это будет зависеть от количества обрабатываемых файлов... Плагины должны быть в порядке, независимо от того.)

Это работает для небольшого количества файлов.

vim `найди. Тип F `

: bufdo% s / pattern / replace /gec | Обновить

Вот моя пользовательская команда Neovim, которая выполняет либо grep, либо grep и замену. Это будет:vimgrepи отображать результаты в окне списка быстрых исправлений, чтобы вы могли перемещаться и просматривать внесенные изменения. Вы можете применить все изменения впоследствии с помощью: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 }
)

Не следует избегать пробелов в выражении регулярного выражения. gфлаг является обязательным иsфлаг обязателен в сочетании с челкой (!), который введет деструктивную операцию замены.

Вы можете перемещаться по списку быстрых исправлений, используя:cn/:cpи вы можете закрыть его с помощью:ccl.

Эта статья помогла мне создать это: https://jdhao.github.io/2020/03/14/nvim_search_replace_multiple_file/

Другие вопросы по тегам