0

I have a user command that grep-replaces files recursively using vimgrep and cfdo:

-- Grep
vim.api.nvim_create_user_command(
  'Grep',
  function(opts)
    if #opts.fargs < 1 then
      print("Usage: :Grep(!) <pattern> <filter?>, i.e. :Grep /my_string/g **/*, or :Grep! /my_string/my_replacement/g **/*")
      return
    end
    local pattern = opts.fargs[1]
    local filter = opts.fargs[2] or "**/*"
    local segmno = 1
    local searchpatt = nil
    local replpatt = nil
    local flags = nil
    for pattsegm in string.gmatch(pattern, "([^/]+)") do
      if segmno == 1 then
        searchpatt = pattsegm
      elseif segmno == 2 then
        if not opts.bang then
          flags = pattsegm
        else
          replpatt = pattsegm
        end
      elseif segmno == 3 then
        flags = pattsegm
      else
        break
      end
      segmno = segmno + 1
    end
    if searchpatt and flags and filter then
      vim.cmd(":vimgrep /" .. searchpatt .. "/" .. flags .. "j " .. filter)
      vim.cmd(":cw")
      vim.cmd(":.cc")
      if opts.bang and replpatt then
        vim.cmd(":cfdo %s/" .. searchpatt .. "/" .. replpatt .. "/" .. flags .. "e")
      end
    end
  end,
  { nargs = '*', bang = true }
)

This works all fine until I want to replace a pattern with something that has a space in it, because it will shift my arguments, such as:

:Grep! /my\_s*string/my replacement/g **/*

Obviously, opts.fargs[1] is only /my\_s*string/my instead of /my\_s*string/my replacement/g.

I tried wrapping the first argument into '...' without success. How can I pass an argument that contains spaces to the user command?

4
  • use <f-args> or <q-args> Jun 16 at 12:53
  • Or you can escape the space with a backslash, when passing it, e.g. :Grep! /my\_s*string/my\ replacement/g **/*
    – filbranden
    Jun 16 at 12:55
  • @filbranden I tried that without luck. The command I tried was :Grep! '/dotenv\.env\['REQUEST_TIMEOUT'\]/\0\ ??\ "30"/g' **/*.dart, but it messed up complaining about Trailing characters: \0 ?? "30"e. I'm not very sure why. Jun 16 at 13:41
  • @ChristianBrabandt Ah, opts.args are all args as one string, so I can find the right split, but it seems not as trivial giving the fact that the substitute delimiter can vary. Jun 16 at 13:44

1 Answer 1

1

I rewrote the user command, so it doesn't require to escape spaces:

-- 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 }
)

This thing is a beast. It now requires to provide a bang AND the leader s for the replace functionality. It reads the delimiter and splits accordingly, but it also recovers the filter variable which might contain delimiters.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.