13

How can I replace the Nth (for example, the third) occurrence of a string (or pattern) in a file using the sed command?  I do mean the Nth occurrence in the file, not the Nth occurrence in a line or the first occurrence in the Nth matching line.  There may be multiple occurrences in a line, and an occurrence (a match) might be a partial word.

Example:

Change only the third occurrence of is to us in the file.

My input file contains:

hai this is linux.
hai this is unix.
hai this is mac.
hai this is unchanged.

The expected output is:

hai this is linux.
hai thus is unix.
hai this is mac.
hai this is unchanged.

Note that “this” has been replaced by “thus” in the second line.

2
  • 5
    sed is not the right tool for the job.
    – choroba
    Jan 29, 2015 at 10:37
  • 1
    @Barbaros Özhan:  Note, FYI, that I reverted your edit to this question. Nov 8, 2022 at 3:26

7 Answers 7

12

It's a lot easier done with perl.

To change the 3rd occurrence:

perl -pe 's{is}{++$n == 3 ? "us" : $&}ge'

To change every 3rd occurrence:

perl -pe 's{is}{++$n % 3 ? $& : "us"}ge'
0
5

You can use sed for that if previously newlines are replaced to any other characters, e.g.:

tr '\n' '\000' | sed 's/is/us/3' | tr '\000' '\n'

And the same with pure (GNU) sed:

sed ':a;N;$!ba;s/\n/\x0/g;s/is/us/3;s/\x0/\n/g'

(sed newline replacement shamelessly stolen from https://stackoverflow.com/a/1252191/4488514)

5
  • 2
    If you're going to use GNU sed specific syntax, you might as well use sed -z 's/is/us/3'. Jan 29, 2015 at 11:54
  • @StéphaneChazelas -z must be some brand new feature, my GNU sed version 4.2.1 doesn't know anything about this option.
    – jimmij
    Jan 29, 2015 at 12:06
  • 1
    Added in 4.2.2 (2012). In your second solution, you don't need the conversion to \x0 step. Jan 29, 2015 at 12:17
  • Sorry about the edit. I had not seen the original version of the question and someone had misunderstood it and edited the wrong line. I reverted to the previous version.
    – terdon
    Jan 29, 2015 at 12:33
  • This worked best in situation where there were no \n and it was a bash array requiring replacing of element separator data entered by user. Jul 11, 2020 at 15:04
3

When the replacement string occurs only once per line, you can combine different utilities.
When the input is in the file "input" and you are replacing " is " by " us ", you can use

LINENR=$(cat input | grep -n " is " | head -3 | tail -1 | cut -d: -f1)
cat input | sed ${LINENR}' s/ is / us /'
3
  • In the example in the question, there are more than one is per line.
    – terdon
    Jan 29, 2015 at 12:47
  • I thought you were looking for " is " with spaces. I could edit my answer with the tr command like @jimmij used, but my solution would become far inferiour to his.
    – Walter A
    Jan 29, 2015 at 13:08
  • I'm not the asker :). I thought the same thing, which is why I had upvoted your answer, but if you look at the original version of the question (click on the "Edited X minutes ago" link) you'll see that the OP expected the is in this to be changed to thus. By the way, there is no need for cat there.
    – terdon
    Jan 29, 2015 at 13:12
2

The script below (using GNU sed syntax) is usable for inplace editing not for output because it stop print lines after desired substitution:

sed -i '/is/{: 1 ; /\(.*is\)\{3\}/!{N;b1} ; s/is/us/3 ; q}' text.file

If your like choroba decision you can modify above to

sed '/is/{:1 ; /\(.*is\)\{3\}/!{N;b1} ; s/is/us/3 ; :2 ; n ; $!b2}' text.file

which outputs all lines

Or you have to put all lines in pattern space (in memory so be careful with size limitation) and do substitution

sed ': 1 ; N ; $!b1 ; s/is/us/3 ' text.file
0
1
p='[:punct:]' s='[:space:]'
sed -Ee'1!{/\n/!b' -e\}            \
     -e's/(\n*)(.*)/ \2 \1/'       \
     -e"s/is[$p]?[$s]/\n&/g"       \
     -e"s/([^$s])\n/\1/g;1G"       \
-e:c -e"s/\ni(.* )\n{3}/u\1/"      \
     -e"/\n$/!s/\n//g;/\ni/G"      \
     -e's//i/;//tc'                \
     -e's/^ (.*) /\1/;P;$d;N;D'

That bit of sed just carries a tally of is occurrences from one line to the next. It should reliably handle as many ises per line as you throw at it, and it doesn't need to buffer old lines while it does - it just retains a single newline character for every is that it encounters which is not a part of another word.

The upshot is it will modify only the third occurrence in a file - and it will carry counts per line. So if a file looks like:

1. is is isis
2. is does

...it will print...

1. is is isis
2. us does

It first handles edge cases by inserting a space at the head and tail of every line. This makes word boundaries a little easier to ascertain.

It next looks for valid ises by inserting a \newline before all occurrences of is that immediately precede zero or one punctuation characters followed by a space. It does another pass and removes all \newlines that are immediately preceded by a not-space character. This markers left behind will match is. and is but not this or ?is.

It next gathers each marker to the tail of the string - for every \ni match on a line it appends a \newline to the tail of the string and replaces it with with either i or u. If there are 3 \newlines in a row gathered at the tail of the string then it uses the u - else the i. The first time a u is used is also the last - the replacement sets off an infinite loop that boils down to get line, print line, get line, print line, and so on.

At the end of each try loop cycle it cleans up the inserted spaces, prints only up to the first occurring newline in pattern space, and goes again.

I'll add in a look command at the head of the loop like:

l; s/\ni(.* )\n{9}/u\1/...

...and take a look at what it does as it works with this input:

hai this is linux.
hai this is unix.


hai this is mac.
hai this is unchanged is.

...so here's what it does:

 hai this \nis linux. \n$        #behind the scenes
hai this is linux.               #actually printed
 hai this \nis unix. \n\n$       #it builds the marker string
hai this is unix.
  \n\n\n$                        #only for lines matching the

  \n\n\n$                        #pattern - and not otherwise.

 hai this \nis mac. \n\n\n$      #here's the match - 3 ises so far in file.
hai this us mac.                 #printed
hai this is unchanged is.        #no look here - this line is never evaled

It makes more sense maybe with more ises per line:

nthword()(  p='[:punct:]' s='[:space:]'         
    sed -e '1!{/\n/!b' -e\}             \
        -e 's/\(\n*\)\(.*\)/ \2 \1/'    \
        -e "s/$1[$p]\{0,1\}[$s]/\n&/g"  \
        -e "s/\([^$s]\)\n/\1/g;1G;:c"   \
        -e "${dbg+l;}s/\n$1\(.* \)\n\{$3\}/$2\1/" \
        -e '/\n$/!s/\n//g;/\n'"$1/G"    \
        -e "s//$1/;//tc" -e 's/^ \(.*\) /\1/'     \
        -e 'P;$d;N;D'
)        

That's practically the same thing but written w/ POSIX BRE and rudimentary argument handling.

 printf 'is is. is? this is%.0s\n' {1..4}  | nthword is us 12

...gets...

is is. is? this is
is is. is? this is
is is. is? this us
is is. is? this is

...and if I enable ${dbg}:

printf 'is is. is? this is%.0s\n' {1..4}  | 
dbg=1 nthword is us 12

...we can watch it iterate...

 \nis \nis. \nis? this \nis \n$
 is \nis. \nis? this \nis \n\n$
 is is. \nis? this \nis \n\n\n$
 is is. is? this \nis \n\n\n\n$
is is. is? this is
 \nis \nis. \nis? this \nis \n\n\n\n\n$
 is \nis. \nis? this \nis \n\n\n\n\n\n$
 is is. \nis? this \nis \n\n\n\n\n\n\n$
 is is. is? this \nis \n\n\n\n\n\n\n\n$
is is. is? this is
 \nis \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n$
 is \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n\n$
 is is. \nis? this \nis \n\n\n\n\n\n\n\n\n\n\n$
 is is. is? this \nis \n\n\n\n\n\n\n\n\n\n\n\n$
is is. is? this us
is is. is? this is
2
  • Did you realize your example says "isis"?
    – flarn2006
    Nov 1, 2016 at 21:26
  • @flarn2006 - im pretty sure it says is is.
    – mikeserv
    Nov 2, 2016 at 2:57
0

Here's a logical solution that uses sed and tr but must be written in a script for it to work. The code below replaces every 3rd occurrence of the word specified in the sed command. Replace i=3 with i=n to make this work for any n.

Code:

# replace new lines with '^' character to get everything onto a single line
tr '\n' '^' < input.txt > output.txt

# count number of occurrences of the word to be replaced
num=`grep -o "apple" "output.txt" | wc -l`

# in successive iterations, replace the i + (n-1)th occurrence
n=3
i=3
while [ $i -le $num ]
do
    sed -i '' "s/apple/lemon/${i}" 'output.txt'
    i=$(( i + (n-1) ))
done

# replace the '^' back to new line character
tr '^' '\n' < output.txt > tmp && mv tmp output.txt


Why this works:

Suppose the text file is a b b b b a c a d a b b b a b e b z b s b a b.

  • When n = 2: we want to replace every second occurrence of b.

    • a b b b b a c a d a b b b a b e b z b s b a b
      . . ^ . ^ . . . . . . ^ . . ^ . . . ^ . ^ . ^
    • First we replace the 2nd occurrence, then the 3rd occurrence, then the 4th, 5th, and so on. Count in the sequence shown above to see this for yourself.
  • When n = 3: we want to replace every third occurrence of b.

    • a b b b b a c a d a b b b a b e b z b s b a b
      . . . ^ . . . . . . . ^ . . . . ^ . . . . . ^
    • First we replace the 3rd occurrence, then the 5th, then the 7th, 9th, 11th, and so on.
  • When n = 4: we want to replace every third occurrence of b.

    • First we replace the 4th occurrence, then 7th, then 10th, 13th, and so on.
0

Use sed with -z option to define the occurrence you want to replace.

sed -i -z "s/is/us/3" pom.xml your.file
1
  • 1
    (1) You are allowed to copy a solution that somebody else already posted as a comment, and post it as an answer, but you should (a) give credit to the original author, (b) add something, like an explanation, or, ideally, both.  (2) What is pom.xml? Nov 8, 2022 at 3:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .