2

In normal usage :%s/A/B/g is a simple way to substitute a pattern across the entire buffer. However the command mode implementation of :substitute is subject to many user configurable variables and as such linters rightfully complain about it's usage in scripts.

I'm trying to use call substitute() instead since this is a documented best practice in scripts. Using it against a string is easy, but I can't figure out how to make it operate on a buffer. I seem to be able to to call setline('1', substitute(getline('1'), 'A', 'B', '')) to change one line at a time, but it doesn't work across a whole buffer.

What is the correct robust way to run a substitution across a whole buffer from a script or plugin?

4

The only problems I see with :substitute are:

  • it messes a few registers
  • we may have to be wary of the separator character used that'll need to be escaped
  • its behaviour depends on 'magic' option (as well as substitute()'s one)

If you really want to operate on the whole buffer without :s, you could also use the following oneliner (that'll be more efficient than any loop)

call setline(1, map(getline(1,'$'), {k,v -> substitute(v, 'PATTERN', 'REPLACEMENT', FLAGS)}))
  • Messing with registers is definitely one reason I DON'T want to use :substitute. I can work around the magic, ignorecase, and other unknown settings, but I don't want to either cause or rely on side effects, which :s does. – Caleb Nov 7 at 14:56
  • Thanks for the map() idea, but are you sure that's correct? Shouldn't the setline() be inside the map referencing v too? – Caleb Nov 7 at 14:56
  • 1
    setline() can take a list as well. And Yes I'm sure as I've tested it with: call setline(1, map(getline(1,'$'), {k,v -> substitute(v, '\d\+', '\=(1+submatch(0))', 'g')})) – Luc Hermitte Nov 7 at 14:58
  • 1
    I did a lot of measurements for the list functions of my lh-vim-lib library plugin. Every time map() and filter() were magnitude faster than raw loops. It was totalement independant of redrawing anything. – Luc Hermitte Nov 7 at 15:41
  • 1
    map() is indeed faster as raw loops. However, you might be running into memory limits here depending on the buffer content. For that reason :s might still be more efficient. – Christian Brabandt Nov 7 at 17:24
2

since this is documented best-practice in scripts

Documented where?

In vimscript (in which plugins are written), every "statement" is an Ex command, you can do

function MyFunc() abort
  %substitute/pattern/repl/g
endfunction

or whatever it is you're trying to do. This is in fact the simplest mechanism. You can add modifiers (keepjumps, etc.) to the command, or build it up programmatically, or any of the usual things. But don't make this painful by trying to use a substitute() function—that works on a single string—on the entire buffer.


If you insist...

You could do a Schwarzian transform from list of lines to line, substitute, and reverse.

Or you could loop

for line in range(0, line('$'))
  call setline(line, substitute(getline(line), 'A', 'B', ''))
endfor

But, why?


See also this Q&A

  • Documented in, for example, vint which cites this guide. Also the vim help documentation also hints at the issues with :substitute in scripts. – Caleb Nov 7 at 14:49
  • Just because every statement is an ex command does not mean they are all safe to use in scripts. Many of them create side effects and many of them change behavior based on user configuration. The simplest mechanism is not what I want here, I specifically want the most robust that will not break if people have things setup weird and won't break their stuff. The ex command :substitute is known to have both issues (relies on user settings, and creates side effects). – Caleb Nov 7 at 14:59
  • 1
    @Caleb the side effects are avoidable, and the user settings which affect it are well documented. You can always save-restore those. Maxim even mentions controls for some of them which don’t need that—case and magic have pattern special items – D. Ben Knoble Nov 7 at 15:17
0

One way could be using regular for loop:

let idx = 1
for line in getbufline(bufnr('%'), 1, "$")
    call setline(idx, substitute(line, 'A', 'B', 'g'))
    let idx += 1
endfor

or

for line in range(0, line('$'))
  call setline(line, substitute(getline(line), 'A', 'B', 'g'))
endfor

which is pretty obvious.

Another one could be:

let buflines = getbufline(bufnr('%'), 1, "$")
%delete _
call append(0, map(buflines, {_, line -> substitute(line, 'A', 'B', 'g')}))
$delete _

It takes all current buffer lines, then deletes whole buffer and appends transformed by map saved buffer lines. With append you going to have additional newline at the end, so the last line deletes it.

There probably more ways to do it, not sure.

But!

I would still go with regular %s// command. Remember you can set it up the way you like using :execute command:

let pattern = 'A'
let sub = 'B'
execute "%s/" . pattern . "/" . sub . "/g"

Note, instead of dot concatenation ppl suggest using printf.

  • Using :execute %s does not solve the problem of not being in control of the ignorecase, smartcase, cpoptions, magic or other options that affect :substitute. – Caleb Nov 7 at 14:52
  • magic and ignorecase and smartcase is easy, just use :%s/\m\cA/B/g – Maxim Kim Nov 7 at 14:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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