15

I have a list of files:

./a.temp.txt     ./a.temp.txt
./a/b.temp.txt   ./a/b.temp.txt
./a/b/c.temp.txt ./a/b/c.temp.txt

And I want to remove the temp. on each line, but only the second occurence, thus, the file should look like:

./a.temp.txt     ./a.txt
./a/b.temp.txt   ./a/b.txt
./a/b/c.temp.txt ./a/b/c.txt

How should I do this?

2
  • Is it possible that there will be a third or fourth occurrence on any line? Do these have to remain intact?
    – DJMcMayhem
    Jul 7, 2016 at 16:15
  • My case was to match old filename to new filename, so only 2 occurrences will be on each line. And only the second one should change.
    – nobe4
    Jul 7, 2016 at 16:16

11 Answers 11

17

In general you can match the Nth occurrence of something using \zs and \{N}. There's an example given at :help \zs.

In your case the command would be:

:%s/\(.\{-}\zstemp\.\)\{2}//
1
  • 4
    with the verymagic flag you can reduce the regex: %s/\v(.{-}\zstemp.){2}//
    – nobe4
    Jul 8, 2016 at 6:40
14

This is much easier done with sed:

sed 's/\.temp\././2'

With Vim you need non-greedy matching, and it isn't easy to extend the method to replacing the 3rd, 4th, etc. occurrence of temp. But it can be done if you insist:

:%s/\.temp\..\{-}\.\zstemp\.//
4
  • Thanks for giving the sed solution, do you think it's possible to apply this on each line ?
    – nobe4
    Jul 7, 2016 at 11:28
  • @nobe4 Not sure what you mean by that. sed applies scripts to each line of input in turn. Jul 7, 2016 at 11:30
  • yes, but you can pass lines into external program and get back the result.
    – nobe4
    Jul 7, 2016 at 11:32
  • 2
    You mean from Vim? Sure, you can pipe lines to sed just as you can pipe them to any other program. F.i.: :%!sed 's/\.temp\././2'. sed in UNIX-friendly, it reads input from stdin and writes results to stdout. Jul 7, 2016 at 11:35
10

This solution is similar to TessellatingHeckler's but is more easily adapted to whatever pattern has to be deleted.

:g/temp\./normal 2ngnd

Here's how it works:

  1. :g/temp\./ for every line matching "temp."
  2. normal execute the following in normal mode
    1. 2n find the second ocurrence of the pattern
    2. gn select it
    3. d and delete it.

This will work for whatever pattern is given to :g. normal may be abbreviated to norm. gnd is equivalent to dgn. Edit: In fact 2ngnd is equivalent to d2gn.

See the Vim Tips Wiki for more examples of the global command.

1
  • 1
    TIL! This is my new favorite editing method. Thanks for sharing. Apr 20, 2022 at 14:35
8

You can also do this with a lookbehind.

:%s/\(temp\..*\)\@<=temp\.//

If you want to remove ALL occurrences after the first, you can append the g flag at the end.

\(\)\@<= Will search for any pattern between \( and \) but will not add the found text to the match.

See :h \@<= for more info.

7

You can use a macro as @Meshpi already suggested. But, this is another way you can accomplish the same thing.

02/temp^Mdwx+

0 - Go to the starting of a line

2/temp - Search for temp and go to the second occurance

^M - This is just pressing the Enter key

dw - delete the word (temp)

x - delete the '.'

+ - Go to the beginning of the next line

And, then you can just repeat it till the end. And, there will be a lot more ways to do the same.

5

In Vim, you can define the column before/at/after which you want your matching to happen with :help \%c:

:%s/\%>17c\.temp/g

This will get rid of every .temp found after column 17 of every line in the current buffer.


Note 1: the /g is not necessary with your sample.

Note 2: I use that handy feature very often with qmv. Recommended!

4
  • Neat, I was thinking of making a plugin, but this is nice!
    – nobe4
    Jul 7, 2016 at 12:35
  • Vim already does so many things…
    – romainl
    Jul 7, 2016 at 12:37
  • Sure, but I was not aware of this tool. And a little bit of scripting is always fun!
    – nobe4
    Jul 7, 2016 at 12:38
  • 6
    @nobe4 If you care about this kind of things, see also vidir and vipe from the moreutils package. Jul 7, 2016 at 13:17
5

This is actually pretty simple. To match the second temp, allow anything followed by "temp" followed by anything, then look for temp again.

%s/.*temp.*\zstemp\.

The \zs means "Start the selection here" so that the first part of the match (everything before the second temp) isn't removed. It's actually an extremely useful feature that I just learned about! Without it, the regex would have to look like this:

:%s/\(.*temp.*\)temp\./\1
2
  • If there are more than two temp on a line your recipe deletes the last one, rather than the second. Blame the greed. :)
    – lcd047
    Jul 7, 2016 at 20:13
  • 2
    Yes, this is greedy, and so it deletes the last one. However, OP said My case was to match old filename to new filename, so only 2 occurrences will be on each line. And only the second one should change.
    – DJMcMayhem
    Jul 7, 2016 at 20:15
3

Instead of regex, I'd go with a command which does:

  • Jump to end of line
  • Search backwards for temp
  • Delete 5 characters

and run it on every line:

:g//normal $?temp^[5x

NB. ^[ is special and represents pressing the Escape key; you type it with: Ctrl-q <Esc>


But a regex option, which I don't think has been suggested yet, is to match temp. followed by anything except a ., anchored to the end of the line.

:%s/temp\.\([^.]\+\)$/\1/

Which will match temp.txt at the end of a line and replace it with just the txt bit.

1

I'd write a macro with the cursor starting at the top left. qq4f.dfpq

Then I'd select the rest of the lines using V and run the macro on those lines with :'<,'>norm!@q

This will only work if the text format stays the same in relation to the number of dots before the second temp.

1
  • 5
    To make your macro more robust, you should use a search and n instead of 4f. because if a filename has more dots, your macro could fail or delete stuff which shouldn't be deleted.
    – statox
    Jul 7, 2016 at 11:28
1

There is nothing preventing you from using two regexes instead of just one, i.e. change the first occurrence and then swap the columns:

:%s/\.temp//|%s/^\(\S\+\)\(\s\+\)\(\S\+\)/\3\2\1/

Might not be extra clever but I find it very readable.

1

My attempt

:%norm 4ftdwx

:%norm ......... execute in normal mode over the whole file  
4ft ............ the 4th t matches the start of the second temp
dw ............. deletes the current word
x .............. deletes the dot

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.