Vim
In Vim you can match any character including newline with \_.
.
You can use this to construct a pattern that matches a whole line,
any amount of stuff, and then that same line:
/\(^.*$\)\_.*\n\1$/
Now you want to delete all lines in
a file that match the first, not including the first. The
substitution to delete the last line that matches the first is:
:1 s/\(^.*$\)\_.*\zs\n\1$//
You can use :global
to make sure that the substitution is repeated
enough times to delete all the lines:
:g/^/ 1s/\(^.*$\)\_.*\zs\n\1$//
POSIX ex
@saginaw shows a neater way to do this in Vim in a comment to your question,
but we can adapt the above technique for POSIX ex.
To do this in a POSIX-compatible way, you have to disallow
multi-line matching, but you can still make use of
backreferences. This requires some extra work:
:g/^/ t- | s/^/@@@/ | 1t- | s/^/"/ | j! | s/^"\(.*\)@@@\1$/d/ | d x | @x
Here's the breakdown:
:g/^/ for each line
t- | copy it above
s/^/@@@/ | prefix it with something unique (@@@)
(do a search in the buffer first to make
sure it really is unique)
1t- | copy the first line above this one
s/^/"/ | prefix with "
j! | join those two lines (no spaces)
s/^"\(.*\)@@@\1$/d/ | if the part after the " and before the @@@
matches the part after the @@@, replace the line
with d
d x | delete the line into register x
@x execute it
So if the current line is a duplicate of line 1, register x will contain d
.
Executing it will delete the current line. If it isn't a duplicate, it will
contain nonsense prefixed with "
which when executed is a no-op, since "
starts a comment. I don't know if this is the neatest way to accomplish this,
it's just the first that came to mind!
It just so happens that the first line can't be deleted because the copying
process temporarily changes what line 1 is. If this hadn't been the case you
could prefix the :g
with a 2,$
range instead.
Tested in Vim and ex-vi version 4.0.
EDIT
And a simpler way, which escapes special characters to create a search
pattern (with 'nomagic'
set), builds a :global
command, then executes it:
:set nomagic
:1t1 | .g/^/ s#\[$^\/]#\\\&#g | s#\.\*#2,$g/^\&$/d# | d x
:@x
:set magic
You can't do this as a one-liner though, since you'd have a nested
:global
, which isn't allowed.
:execute '2,$g/\V' . escape(getline(1), '\') . '/d'
sed
as a filter from withinex
, and run my entiresed
answer on the whole buffer...which would work, of course (and is actually portable unlikesed -i
).<C-r>0
very good. I'm not sure you could do better with only Ex commands because you have to protect special characters. Without the POSIX compliant constraint I think you would use the very nomagic switch\V
and then you would protect the backslash (because it keeps its special meaning even with\V
) with theescape()
function whose 2nd argument is a string containing all the characters you want to escape/protect.:execute '2,$g/\V' . escape(getline(1), '\/') . '/d'
Or you could use another character for the pattern delimiter like a semicolon. In this case you wouldn't need to protect a forward slash in the pattern. It would give something like::execute '2,$g;\V' . escape(getline(1), '\') . ';d'
sed
also very good. With Vim, you often delegate certain special tasks to other programs, andsed
is probably a good example of that. By the way, you don't have to runsed
on your whole buffer. If you want to run it only on a portion of the buffer, you can give a range. For example, if you want to filter only the lines between 50 and 100, you could type::50,100!<your sed command>
.