How to edit the entire file after match a grep pattern?

This gets a lot easier if you go through the file backwards. Fortunately, you can do so easily with tac (which happens to be cat backwards). We can then use a relatively simple awk script to look for your host, and change only its ip:

$ tac input | awk -v OFS="=" -v myip="changed_address" -v myhost="d" -F"=" '$1 == "host" { if( $2 == myhost ) { sw = "on" } else { sw="off" } } sw == "on" && $1 == "ip" { $2=myip } { print $0 }' | tac
ip=x.x.x.a
mask=255.0.0.0
host=a
ip=x.x.x.b
mask=255.0.0.0
host=b
ip=x.x.x.c
mask=255.0.0.0
host=c
ip=changed_address
blahblah
mask=255.0.0.0
host=d

I shall explain in detail how the awk works:

First, we declare a few variables: one each for the host and new value for the ip:

-v myip="changed_address" -v myhost="d"

Further, we declare the field separator for the input and output:

-v OFS="=" -F"="

Now, the actual awk script itself:

$1 == "host" {             // If we see the "host" line..
  if( $2 == myhost ) {     // And it matches the one we're looking for..
    sw = "on"              // Set a flag to swap the next IP
  } else { 
    sw="off"               // Otherwise, unset the flag
  }
} 

sw == "on" && $1 == "ip" { // If the flag is set and this is an IP line.. 
    $2=myip                // Swap in the new IP 
}

{
   print $0                // Finally, print out the processed line
}

Once that's all done, we just use tac again to re-reverse it, making it forwards again.


I know you are expecting something very fast, and simple like a one-line sed command or a smart awk code, but if you don't care...

#!/bin/bash
#Note: Adjusted to run with a posix shell (tested in dash)
filename='file'
newip='127.0.0.1'
hostchar='d'

tac "$filename" | while IFS= read -r line ; do 
     case $line in 
          host=${hostchar})  
              flag=on 
          ;;
          host=*)
              flag=off
          ;;
     esac
     if [ "$flag" = "on" ]; then
     case $line in 
          ip=*) 
              echo ip=$newip
              continue 
          ;; 
          #you can replace more variables at once by adding it here
          #in the same standard. 
          #for ex: mask=*) echo mask=$newmask; continue ;; etc...
     esac  
     fi
     echo $line 
done | tac 

Results:

ip=x.x.x.a
mask=255.0.0.0
host=a
ip=x.x.x.b
mask=255.0.0.0
host=b
ip=x.x.x.c
mask=255.0.0.0
host=c
ip=127.0.0.1
blahblah
mask=255.0.0.0
host=d

This will change the IP associated with host c to 1.2.3.4:

$ sed 's/^ip/\nip/' file | perl -00pe 'if(/\nhost=c\n/){s/ip=\S+/ip=1.2.3.4/} s/\n\n/\n/' 
ip=x.x.x.a
mask=255.0.0.0
host=a
ip=x.x.x.b
mask=255.0.0.0
host=b
ip=1.2.3.4
mask=255.0.0.0
host=c
ip=x.x.x.x
blahblah
mask=255.0.0.0
host=d

Explanation:

  • sed 's/^ip/\nip/' file : add an extra newline (\n) to each line beginning with ip. I think this might not work with all implementations of sed, so if yours doesn't support this, replace the sed command with perl -pe 's/^ip/\nip/'. We need this in order to use Perl's "paragraph mode" (seen below).

  • perl -00pe : the -00 makes perl run in "paragraph mode" where a "line" is defined by two consecutive newlines. This enables us to treat each host's block as a single "line". The -pe means "print each line after applying the script given by -e to it".

  • if(/\nhost=c\n/){s/ip=\S+/ip=1.2.3.4/} : if this "line" (section) matches a newline followed by the string host=c and then another newline, then replace ip= and 1 or more non-whitespace characters (\S+) following it with ip=1.2.3.4.

  • s/\n\n/\n/ replace each pair of newlines with a single newline to get the original file's format back.

If you want this to change the file in place, you can use:

tmp=$(mktemp); sed 's/^ip/\nip/' file > $tmp; 
perl -00pe 'if(/\nhost=c\n/){s/ip=\S+/ip=1.2.3.4/} s/\n\n/\n/' $tmp > file