0
  1. macOS sed command to replace last occurrence of a word in a file
  2. Replace with the content from another file
  3. Replace only the last occurrence, means only once.
  4. The word can be a substring abchello or helloabc
  5. There can be trailing whitespace or newline after the word

sample_file_path = "/Users/saurav/sample.text" sample_file_path_1 = "/Users/saurav/sample1.text"

sample.txt:

hello
hi
hello
ok

sample1.txt:

I
am 
doing
great

Expected output (sample.txt):

hello
hi
I
am 
doing
great
ok

Need to use file path variable

8
  • Please edit your question and give us some more details. Should this be done for all words that occur more than once? What if a word occurs more than twice, should we only change the last instance? Can multiple words occur more than once? Can a word be a substring? Can you have othello for example and should that count as a match of hello? Does it absolutely need to be sed or are you open to tools like awk or perl? – terdon May 25 at 15:45
  • @terdon Updated the question, with all your queries – Saurav May 25 at 16:01
  • I don't think this is even possible in sed. Or, if it is, would probably require a very, very convoluted script. You would need to read the file twice (once to get the words and another to get the last occurrence) and then replace it and repeat for every repeated word. Sed just isn't the right tool here. Both awk and perl are installed by default on macOS, are you sure you don't want to use something more suited to the task? Also, you haven't clarified if "last" always means "second" or if it can be Nth, nor if you can have multiple duplicated words. – terdon May 25 at 16:17
  • Understood, I am new to shell scripting. I tried with awk and perl which works fine on osx. @terdon please suggest a command based on awk or perl to achieve that – Saurav May 25 at 16:28
  • OK, but you still haven't told us what words to replace. Is it any word that occurs more than once in the file? Only some words that occur more than once? Is the last time always the second time or can there be more? – terdon May 25 at 16:55
6

In three steps, using sed syntax compatible with /usr/bin/sed on macOS, and either bash or zsh (the two main shells on current macOS systems):

sed -n '/hello/=' sample.txt |
sed -e '$!d' -e $'s/.*/&r sample1.txt\\\n&d/' |
sed -f /dev/stdin sample.txt

This uses sed in three steps:

  1. Finds all lines in sample.txt that matches hello and output the line numbers corresponding to those lines.

  2. Deletes all but the last line number outputted by the first step (using $!d, "if this is not the last line, delete it"), and creates a two-line sed script that would modify the last matching line by first reading sample1.txt and then deleting the original line. Given that the last match of hello is on line 3 in the original file, this script would look like

    3r sample1.txt
    3d
    
  3. Applies the constructed sed script on the file sample.txt.

Would you want to make the edit "in-place", so that the original sample.txt is modified, then use

sed -n '/hello/=' sample.txt |
sed -e '$!d' -e $'s/.*/&r sample1.txt\\\n&d/' |
sed -i '' -f /dev/stdin sample.txt

The same set of commands, but using your variables $sample_file_path and $sample_file_path_1 for the two file paths:

sed -n '/hello/=' "$sample_file_path" |
sed -e '$!d' -e 's,.*,&r '"$sample_file_path_1"$'\\\n&d,' |
sed -i '' -f /dev/stdin "$sample_file_path"

Note that I have changed the delimiters in the second command from / to , as the file path contains slashes. You may use any character as a delimiter in the s/// command that is not otherwise part of the regular expression or replacement text.

12
  • @Kushalananda Thanks a lot!!! It works as expected – Saurav May 27 at 5:56
  • 1
    @Saurav [ ! -s sample.txt ] && cp sample1.txt sample.txt – Kusalananda May 27 at 6:47
  • 1
    @Saurav I can recommend trying to solve issues on this site, or read and test other people's answers. I don't know any other on-line tutorials. I've learnt by solving real problems, and this site is full of these. – Kusalananda May 27 at 6:50
  • 1
    @Saurav Sure. It will look a bit wonky but it will work: sed -e '$!d' -e 's/.*/&r '"$fatstfile_path"$'\\\n&d/' – Kusalananda May 27 at 9:19
  • 1
    @Saurav See added bit at the end of the answer. – Kusalananda May 28 at 15:25
2

Try tac and awk combination:

tac sample.txt \
| awk -v replacement="$(tac sample1.txt)" '/hello/ && !n++ {gsub(/hello/,replacement,$0)}1' \
| tac

Note, this will also replace the hello part from nothello. If you want to replace only independent hello, you need to add word boundaries to the pattern: /\<hello\>/ instead of /hello/

2

With a single invocation + using the Posix-ly constructs of sed we can do as shown. The USP of this approach is that the sed code itself generates the file which it shall read later into the printing stream.

sed -n '
  /\n/{
    s/.$//w file.tmp
    b a
  }
  /hello/!{
    H;1h;$!d
    s/.*//;x
    //!{p;q;}
    G;D
  }
  x;1!p;$!d;:a
  r file2
  r file.tmp
' file1

Brief explanation of how it works:

  • Accumulate hello line and the subsequent non hello lines in hold space.
  • Now when we read the next line and it happens to be hello, then we know that the hello line in space cannot be the last, so we flush out the hold area.
  • This is the next cycle of events wherein we store the hello line at the top and subsequent non hellos underneath it.
  • The cycle (flush hold;store /hello/+!/hello/+!/hello/...) keeps going on until we hit the EOF.
  • Now at eof, one of the 3 possible scenarios can arise:
    • There never was a /hello/ line in the whole file, so the hold ballooned to contain the total lines and we flush it via the //!{p;q;} construct. At this stage, we already are at eof and we tested for the regex /hello/ , so we can reuse it as //.
    • Second scenario is hold had /hello/+atleast one non hello. What we need to do here is clip the top of the hold , the /hello/ line, and replace it with the contents of file2 since this is the LAST /hello/ line. But the fly in the ointment is we want the other portion (the non hello lines). If we just did r file2 this would place the contents at the end. So we have to come up with something to overcome it. What we do is , store these non hello lines in a file and then do a r tmpfile . Pbm solved.
    • Third possibility is the last line was a hello line. Here the tmpfile would not exist and we exploit the property of read command to not complain about non existent files.
3
  • That really could do with some explanation. It looks like random noise! :) – terdon May 26 at 11:57
  • Added a brief explanation of the working. – guest_7 May 26 at 14:59
  • Thanks! That's very helpful. – terdon May 26 at 15:26
0

We can use the sed stream editor utility to get the desired output.

sed -n '
  /hello/!{
    H;1h;$!d;g
    s///w file.tmp
    t a
    p;q
  }
  x;1!p;$!d
  :a
  r file2
' file1
sed -e '1d' file.tmp
  • accumulates the lines in hold until it sees the hello line
  • on a hello line, it prints what was saved in hold and starts the cycle again.
  • only at EOF or the last line we print the hold and pattern spaces.

Or, another way is the tac-perl-tac pipeline:

tac file1 |
perl -0777 -pe 's?hello\n?qx/tac file2/?e' - | 
tac

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.