A relatively short solution using negative lookahead assertions in a perl regular expression:
perl -p0e 's/(ip=)[^\n]*((?:(?!ip=).)*?host=c)/\1new_ip_for_host_c\2/gs' hosts.txt
First, a print-loop is used using the
-p option. This iterates through lines of a file provided as argument to
perl and prints them. If the line is changed, the changed variant is printed, as with
The file contents are not actually treated as separate lines, the
-p option merely serves to make
perl read in the file at once and use the contents as the default string to match in, without using a variable. This is achieved via the
-0 option, which makes
perl treat multiline input as a single line, substituting newline characters by null bytes.
In the pattern, parts excluding the value to be changed are saved.
The value is recognized as anything until the next newline, which I assume is understood as the next null-byte in this situation. Someone who knows this could perhaps explain.
Then, using a negative lookahead assertion, all characters are matched that are not preceded by an
ip= part, until the
host=d part is encountered. This is necessary, otherwise perl would prefer the left-most match over the shortest, and would match from the very first
ip= to the required
The characters not preceded by
ip= are matched in a non-greedy manner, otherwise again, the match would encompass everything from the first
host=c, because by default, the longest match is preferred.
In other terms, this ensures that text is matched in sections terminated by
host=c, and started by the
ip= segment that is closest above
host=c, not furthest.
The character restricted by the negative lookahead expression inside the
(?:) construct. It denotes a non-capturing group, meaning that it won't count toward the total number of backreferences in the substitution string.
Grouping is used so the
* sign can properly quantify the whole negative lookahead - dot character combo as a single atom.
In the example, backreferences range from
\2, whilst without the non-capturing group, they would be
\3 denoting the nested group.
\2 in the substitution string are the matched parts of the section that enclose exactly the ip address for the host found in the matched section.
The ip address will thus be substituted by
I am not sure if the data format was the OP's choice, just putting this out in general: it is better to store such simple data in such a manner that records are separated by newlines, i.e. there are no multiline records.
Refer to the format of /etc/passwd .
Most standard Unix filters are line-oriented, and data manipulation is much easier if you follow "da Unix wae".
If your data is more complex (nested data items, etc.), you are better off using a format such as JSON and manipulating it with a proper parser.