6

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

  • How about g/foo/ .,/foo/d (replacing foo with what works for you, of course)? – Peter Lewerin Oct 25 '15 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 Oct 25 '15 at 21:09
  • 1
    How about a replace by nothing :%s/<body>\(.|\n\)*<\/body>// ? – JJoao Oct 27 '15 at 0:04
  • @JJoao Yes, it works (:%s/<body\_.\{-}body>//). Can you post the answer then? – kenorb Oct 28 '15 at 13:45
8

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.

  • Life. Saver. Love the extra info. – Tom Russell Mar 21 '18 at 9:56
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. :)

1

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}

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.