8

For testing purposes I've opened the following file:

vim http://example.com/

And I'm using the following command to delete a highlighted multi-line pattern:

:/<body\_.\{-}body>/d

which matches multiple lines, but only removes for the first line.

As for workaround (for this particular case) it works however when I delete the inner tag block:

:/<body\_.\{-}body>/norm dit

But it doesn't work when working on non-tag blocks, for example:

:/body {\_.\{-}}/d

but again, this particular case can be solved by:

:/body {\_.\{-}}/norm d%

So the question is:

How do I remove a multi-line match at one go in more universal way (which would work in all cases)?

4
  • How about g/foo/ .,/foo/d (replacing foo with what works for you, of course)? Commented Oct 25, 2015 at 21:05
  • @PeterLewerin Yes, :g/<body/,/body>/d seems to work (as a side-effect, it'll remove the whole lines with it, but it almost works as expected). You can add that into your answer.
    – kenorb
    Commented Oct 25, 2015 at 21:09
  • 1
    How about a replace by nothing :%s/<body>\(.|\n\)*<\/body>// ?
    – JJoao
    Commented Oct 27, 2015 at 0:04
  • @JJoao Yes, it works (:%s/<body\_.\{-}body>//). Can you post the answer then?
    – kenorb
    Commented Oct 28, 2015 at 13:45

4 Answers 4

16

The feasibility of deleting parts of lines is elusive and a better vimmer than I will have to explain that. If anyone cares to donate an explanation I'd be happy to add it to this answer.

But it's quite possible to delete groups of rows that match a beginning pattern and an ending pattern. The most obvious way, I think, is to combine the :global and :delete commands like this:

:g/<body/ .,/body>/d

which searches every line in the buffer for the pattern <body. When it finds a match, it constructs the range .,/body>/, i.e. from the line that matched to the next line that matches body> and invokes the :delete command on that range.

This works too:

:g/<body/,/body>/d

It's the same command, omitting the current-line ..

Also, there's of course the dat operation, but since you didn't use that, I assume it's somehow inadequate. Now, there's usually only one "body" element in a html-like document, but if one were to delete all "foo" elements, one could use this command:

:g/<foo/ norm! dvatx

That is, globally search for the pattern <foo, at every occurrence invoke the normal mode command (! disallow mappings here) delete v (force character-wise, omit or replace with V if line-wise is preferred) around tag, and x remove leftover ">".

As they say, anyone processing sgml/html/xml using regular expressions is of course in a state of sin. Oversimplified schemes like this will blow up from time to time. The only way to be sure is to invoke a filter that runs a dom processor.

1
  • Life. Saver. Love the extra info. Commented Mar 21, 2018 at 9:56
4

A possible approach to delete the multi-line pattern is replace it by nothing:

:%s/<body>\(.|\n\)*<\/body>//

or in a more vim-ish, not greedy way:

:%s/<body\_.\{-}body>//

\thanks{@kenorb}

2
  • Can you explain the magic between the patterns in the second form, please?
    – fortboise
    Commented Jan 18, 2021 at 19:04
  • second option works well, thanks
    – Kes
    Commented Jul 28, 2021 at 23:16
2

You can use a filter. I would probably use sed for arbitrary multi-line matching.

There is a drawback in that there is no way in sed to do non-greedy matching, but in certain cases you can work around that. (For example, add only one line at a time until you get a match, then do the substitute, as below.) Your examples aren't using non-greedy matching in the first place, so:

:.,$!sed '/<body/{;:loop;s@<body.*/body>@@;t;$b;N;bloop;}'

Here's an example command. This will filter lines from the current line through the end of the file. Until it finds the match <body it will output each line exactly as it inputs. When it hits <body it enters a loop (conveniently labeled with :loop. The label itself does nothing but can be branched to later.) sed runs a substitute command, greedily matching <body.*/body> and deleting it if found (using @ as delimiter to avoid needing to escape the /.) If the substitute command actually did something, t exits the loop and sed outputs the text as it was changed by the substitute command, and then proceeds to the next line. Otherwise, sed continues: the $b command is ignored unless sed is on the last line to be filtered. N adds in the next line to what is being considered, with a newline in between, and the branch command branches to the label loop. Then the substitute command is run again, now on two lines, and if no match then it is run on three lines, until it either finds a match (at which point the t command breaks the loop) or until the end of the lines to be filtered, at which point $b causes a branch and exits the loop, outputting the lines without filtering.

(sed is a whole language in itself. It's not very complicated—actually if you're familiar with Ex commands you will recognize a lot of vim's heritage in sed's syntax—but it is very concise. I've heard it called the "assembly language of text editing" and it is a very apt description.)

Hope this is helpful. I'm happy to clarify anything more you'd like on this, or tailor the example better for your needs. :)

2

If the match needs to be only linewise, it's easy with a range:

:/match1/,/match2/-1d

-1 stops the action on line preceding /match2/ or it will delete /match2/ line inclusively. Non-greedy by default it will stop at the first match for either.

If your cursor is at the start of match1, normal mode command 'd' will delete until match2:

d/match2<CR>

Your Answer

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

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