10

How to replace the third occurrence of the string in the file using sed command.

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.

I am expecting output is:

hai this is linux.
hai thus is unix.
hai this is mac.
hai this is unchanged.
  • 3
    Input and output are the same. – Hauke Laging Jan 29 '15 at 10:26
  • 4
    sed is not the right tool for the job. – choroba Jan 29 '15 at 10:37
  • @don_crissti I fixed it. The OP had not used the formatting tools (by the way, Sureshkumar, see here for help on editing your questions) and successive editors had misunderstood what was wanted. – terdon Jan 29 '15 at 12:36
11

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'
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 /'
  • In the example in the question, there are more than one is per line. – terdon Jan 29 '15 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 '15 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 '15 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
2

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)

  • If you're going to use GNU sed specific syntax, you might as well use sed -z 's/is/us/3'. – Stéphane Chazelas Jan 29 '15 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 '15 at 12:06
  • 1
    Added in 4.2.2 (2012). In your second solution, you don't need the conversion to \x0 step. – Stéphane Chazelas Jan 29 '15 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 '15 at 12:33
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
  • Did you realize your example says "isis"? – flarn2006 Nov 1 '16 at 21:26
  • @flarn2006 - im pretty sure it says is is. – mikeserv Nov 2 '16 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.

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.