sed - perform only first (nth) matched replacement?

4

Consider I have the following file:

echo "1
2
3
4
1
2
3
4
1
2
3
4
1
2
3
4
" > ztest

Here I'd like to change only the first 1 into 5 and leave everything else intact.

I know sed has a quit command - so I'm trying the following:

$ sed 's/1/{5;q}/' ztest 
{5;q}
2
3
4
{5;q}
2
3
4
{5;q}
2
3
4
{5;q}
2
3
4

This changes all lines, so it's no good. So, then I try this:

$ sed '{s/1/5/;q}' ztest 
5

Now, this apparently does as requested - changes the first line and exits; however, I'd like the rest of the lines intact! (since this, basically has the effect of replacing the entire file with a single '5')

So I'm at a loss at what kind of syntax is needed? Note that I need this to change an "early" line in a gigabyte file inline (using sed -i); thus I'd like sed to only search and replace in the first brief section - and after that, output lines unchanged (since sed -i anyways dumps into a temp file first, and then overwrites the original); hoping to save some processing time.

Thanks in advance for any answers,
Cheers!

EDIT: forgot subquestion: is it possible to extend this to first n matches? (say, in the file above, first two '1's are changed to 5 - rest of it is output verbatim?)

sdaau

Posted 2012-02-26T18:37:24.267

Reputation: 3 758

Are you stuck with sed only? – Mat – 2012-02-26T18:43:59.043

HI @Mat - not really; just the first one I resort to when I have t replace inline in text files - any other suggestions are welcome! – sdaau – 2012-02-26T18:46:14.617

1

same situation here

– None – 2012-02-26T19:11:00.297

Many thanks for that link, @selman - I like the trick with line 0 (as in 0,/Apple/{s/Apple/Banana/}) from there - cheers! – sdaau – 2012-02-26T19:58:22.107

Answers

8

I have seen it somewhere else on this site:

sed '/1/{s/1/5/;:a;n;ba}' ztest

So once you found the 1st occurrence, you loop till the end of the file reading and printing lines.

Update

It can be enhance to only replace the Nth match. The idea is to store a X at each match in the holdspace, and when all the Xs are there, loop till the end of file. Bellow, the script that replace the 2nd occurrence:

sed '/1/{G;s/\nX\{1\}//;tend;x;s/^/X/;x;P;d};p;d;:end;s/1/5/;:a;n;ba'

Note that you need to put (N-1) between the \{ and \}.

Update 2

I realize that the P;d above is useless if the p;d is replaced by P;d. So a simpler solution is:

sed '/1/{G;s/\nX\{1\}//;tend;x;s/^/X/;x};P;d;:end;s/1/5/;:a;n;ba'

jfg956

Posted 2012-02-26T18:37:24.267

Reputation: 1 021

One caveat. The OS/X (even on El Capitan) sed command doesn't recognize { } and will just punt with following error message: sed: 1: "/1/{s/1/5/;:a;n;ba}": unexpected EOF (pending }'s) – tdwong.star – 2016-07-28T00:04:03.730

Many thanks for that @jfgagne - I was hoping sed can refer to the n-th match; and glad to see an example (I'll need to brush up on my sed syntax, though :) ) Cheers! – sdaau – 2012-02-26T19:54:57.353

4

Don't know if that's doable with sed, but here's an awk version.

awk 'BEGIN {matches=0}
     matches < 2 && /1/ { sub(/1/,"5"); matches++ }
     { print $0 }' ztest 

This will replace 1 with 5 for the first two matches of 1. (Change the 2 in there to increase/decrease the number of matches you want.)
After that, the file is printed as-is.

Mat

Posted 2012-02-26T18:37:24.267

Reputation: 6 193

Many thanks for that, @Mat - by the way, your original sed answer led me this answer; but I think I like the direct number of matches specification better. Cheers!

– sdaau – 2012-02-26T19:53:22.363

2

This might work for you (GNU sed):

sed '0,/1/s//5/' file

potong

Posted 2012-02-26T18:37:24.267

Reputation: 156

1Thanks @potong, care to explain a bit more please? – xpt – 2013-07-22T18:53:26.750

1

Thanks to @Mat's first answer (over at StackOverflow, now deleted), the syntax for sed to replace only the match on first line is:

sed  '1s/1/5/' ztest

However, you have to know explicitly the line number where to perform the match.

So if I know the file is as above, then the first two 1's appear on lines 1 and 5; so for the first two matches, I could instruct sed to apply the expression between lines 1 and (say) 6 - the syntax for line (address) range in sed being a comma (,):

$ sed  '1,6s/1/5/' ztest
5
2
3
4
5
2
3
4
1
2
3
4
1
2
3
4

Similarly, second and third occurrence of 1 are on lines 5 and 9 - so I can similarly use the range, say, from line 3 to line 10 to change second and third occurrence of 1:

$ sed  '3,10s/1/5/' ztest
1
2
3
4
5
2
3
4
5
2
3
4
1
2
3
4

.... which I guess answers my question in terms of sed; although I hoped that I could specify the number of matches directly (as in the awk solution by @Mat).

sdaau

Posted 2012-02-26T18:37:24.267

Reputation: 3 758

1

Use the :a;n;ba pattern once again:

$ sed '/1/{s//5/;:A;/1/{s//5/;:B;n;bB};n;bA}'

kev

Posted 2012-02-26T18:37:24.267

Reputation: 9 972

Many thanks for that pattern, @kev - cheers! – sdaau – 2012-02-27T06:27:01.687