3

I have a usecase where I need to search and replace the last occurrence of a string in a file and write the changes back to the file. The case below is a simplified version of that usecase:

I'm attempting to reverse the file, make some changes reverse it back again and write to the file. I've tried the following snippet for this:

tac test | sed s/a/b/ | sed -i '1!G;h;$!d' test

test is a text file with contents:

a
1 
2 
3
4
5

I was expecting this command to make no changes to the order of the file, but it has actually reversed the contents to:

5
4
3
2
1
b

How can i make the substitution as well as retain the order of the file?

seriousgeek
  • 992
  • 3
  • 13
  • 29
  • See: [Bash: how to use sed to replace only the last occurence in a file?](https://stackoverflow.com/q/10106292/3776858) or https://www.google.com/#q=bash+replace+last+occurrence+in+file – Cyrus Jul 17 '17 at 19:11
  • The difference here is I need to write back to the file. – seriousgeek Jul 17 '17 at 19:38

5 Answers5

4

You can tac your file, apply substitution on first occurrence of desired pattern, tac again and tee result to a temporary file before you rename it with the original name:

tac file | sed '0,/a/{s//b/}' | tac > tmp && mv tmp file
SLePort
  • 15,211
  • 3
  • 34
  • 44
2

Try to cat test | rev | sed -i '1!G;h;$!d' | rev

Or you can use only sed coomand: For example you want to replace ABC on DEF:

You need to add 'g' to the end of your sed:

sed -e 's/\(.*\)ABC/\1DEF/g'

This tells sed to replace every occurrence of your regex ("globally") instead of only the first occurrence.

You should also add a $, if you want to ensure that it is replacing the last occurrence of ABC on the line:

sed -e 's/\(.*\)ABC$/\1DEF/g'

EDIT

Or simply add another | tac to your command: tac test | sed s/a/b/ | sed -i '1!G;h;$!d' | tac

Vadim Beskrovnov
  • 941
  • 6
  • 18
2

Another way is to user grep to get the number of the last line that contains the text you want to change, then use sed to change that line:

$ linno=$( grep -n 'abc' <file> | tail -1 | cut -d: -f1 )

$ sed -i "${linno}s/abc/def/" <file>
Jack
  • 5,801
  • 1
  • 15
  • 20
1

Here is a way to do this in a single command using awk.

First input file:

cat file
a
1
2
3
4
a
5

Now this awk command:

awk '{a[i++]=$0} END{p=i; while(i--) if (sub(/a/, "b", a[i])) break;
      for(i=0; i<p; i++) print a[i]}' file
a
1
2
3
4
b
5

To save output back into original file use:

awk '{a[i++]=$0} END{p=i; while(i--) if (sub(/a/, "b", a[i])) break;
for(i=0; i<p; i++) print a[i]}' file >> $$.tmp && mv $$.tmp f
anubhava
  • 761,203
  • 64
  • 569
  • 643
1

Another in awk. First a test file:

$ cat file
a
1
a
2
a

and solution:

$ awk '
$0=="a" && NR>1 {             # when we meet "a"
    print b; b=""             # output and clear buffer b
}
{
    b=b (b==""?"":ORS) $0     # gether the buffer
}
END {                         # in the end
    sub(/^a/,"b",b)           # replace the leading "a" in buffer b with "b"
    print b                   # output buffer 
}' file
a
1
a
2
b

Writing back the happens by redirecting the output to a temp file which replaces the original file (awk ... file > tmp && mv tmp file) or if you are using GNU awk v. 4.1.0+ you can use inplace edit (awk -i inplace ...).

James Brown
  • 36,089
  • 7
  • 43
  • 59