Dmitry Ivanov
Dmitry Ivanov

Reputation: 381

Multiline pattern after pattern match using sed

I have a config file that consists of multiple sections and lots of them contain a property with the same name, let it be host. I need to replace the host property of one particular section only. Here's the file:

section1 {
  ...
  setting1 = "true"
  ...
  host = "localhost"
  ...
}
section2 {
  ...
  host = "whatever"
  ...
}

I want to replace the host value of section2 with something else. Note that there may be any number of lines in between, marked as ...

Upvotes: 1

Views: 930

Answers (3)

karakfa
karakfa

Reputation: 67467

awk to the rescue!

$ awk 'BEGIN{RS=ORS="\n}"}
  /section2/{sub("host =[^\n]*", "host = \"newvalue\"")}NF' file 

section1 {
  ...
  setting1 = "true"
  ...
  host = "localhost"
  ...
}
section2 {
  ...
  host = "newvalue"
  ...
}

define the record structure, find the corresponding record and substitute.

Upvotes: 1

mklement0
mklement0

Reputation: 437082

Joao Morais's answer is probably the best choice.

To complement it with an awk solution (a corrected, more robust version of karakfa's answer):

awk '
  BEGIN{ RS=ORS="}\n" }
  /^section2/ { $0 =gensub(/(\n *host = )[^\n]*/, "\\1\"new value\"", 1) }
  1' file
  • With mawk (less robust; also works with GNU awk):
awk '
  BEGIN{ RS=ORS="}\n" }
  /^section2/ { sub(/ host = [^\n]*/, " host = \"new value\"") }
  1' file

Note: Due to use of a multi-character RS value, neither solution works with BSD/OSX awk; making it work there would require more effort.

Explanation:

  • RS=ORS="}\n" tells awk to split the input into records by }\n instances (via special variable RS, the input record separator), and to use the same separator for output (via ORS, the output record separator).

  • /^section2/ matches literal section at the beginning of each record.

    • Mawk solution: { sub(/ host = [^\n]*/, " host = \"new value\"") } replaces the value for key host with the new value.

      • Note the need to match only to the next newline ([^\n]*), because just .* would match to the end of the record, across line breaks.
      • It would be preferable to match the host key more robustly, such as with
        sub("/(\n *host = )...), but you'd need capture-group references to retain the same amount of preceding whitespace in the replacement value, which sub() doesn't support.
        GNU awk, however, does offer capture-group references via its nonstandard gensub() function; see below.
    • GNU Awk solution: Use of the nonstandard gensub() function enables the use of capture groups (\\1 refers to the 1st captured group, ...), which makes the replacement both more robust and more convenient.

      • gensub(), unlike sub() doesn't modify the input string and instead returns the modified copy - hence the need to assign it to $0.
      • 1 as the 3rd argument tells gensub() to only replace 1 occurrence.
  • 1 is a common shorthand for simply printing the resulting record.

Upvotes: 0

Joao Morais
Joao Morais

Reputation: 1925

sed -i.bak '/^section2 {/,/^}/s/host .*/host = "newvalue"/' file

This will search between section2 { and the next }, changing all host = occurences. GNU sed syntax, you should use eg sed -i '' ... on OSX.

Upvotes: 3

Related Questions