Find and replace text in a file after match of pattern only for first occurrence using sed

Let's say I have the following input file:

Server 'Test AB'
    option type 'ss'
    option port '1234'
    option timeout '60'

Server 'Test CD'
    option type 'ss'
    option port '1234'
    option timeout '60'

Server 'Test EF'
    option type 'ss'
    option port '1234'
    option timeout '60'

Server 'Test GH'
    option type 'ss'
    option port '1234'
    option timeout '60'

And now I want to use sed to exchange "option port '1234'" with "option port '9876'", but I only want to do this for Server 'Test EF'. All the other ports should stay unchanged.

Output should look like:

Server 'Test AB'
    option type 'ss'
    option port '1234'
    option timeout '60'

Server 'Test CD'
    option type 'ss'
    option port '1234'
    option timeout '60'

Server 'Test EF'
    option type 'ss'
    option port '9876'
    option timeout '60'

Server 'Test GH'
    option type 'ss'
    option port '1234'
    option timeout '60'

I already tried many sed commands, I found out that I can search between lines with

sed '11,15s/port '1234'/port '9876'/g' file

But this doesn't help me, because Server 'Test EF' won't be always in the same lines. I would need to limit the area instead of 11,15 with for example "'Test EF',option timeout" but no idea how I can realize it.


Solution 1:

Contrary to what the previous answer says, this is right up sed’s alley.  And, contrary to another suggestion (now deleted), it is easily done in a single sed process.

sed "/Server 'Test EF'/,/Server/ s/option port '1234'/option port '9876'/" file

will do what you want.

Some say sed code is usually quite cryptic, unless you're a sed expert.  While it may be hard to write, I believe that it’s fairly easy to read and understand:

  • Starting at a line that contains Server 'Test EF',
  • and continuing through the next line that contains Server,
  • search each line for option port '1234'
  • and, if you find it, substitute option port '9876'.

You were actually fairly close.  Quoting was a big problem with your attempt: you can’t use single-quote characters (') in a string that’s also delimited with single-quotes (i.e., that begins and ends with single-quotes).  One way to handle this (which my answer, above, uses) is to enclose the string in double-quote characters ("); as in

  … "…                         … s/option port '1234'/option port '9876'/" …
    ↑                                                                    ↑

That works fine if the string doesn’t contain any " characters — or $, \ or `.  If you’re using bash (as you indicate with your tags), you can use $'……\'……'; e.g.,

echo $'the cat\'s pajamas'
the cat's pajamas

so you could do

  … $'…                     … s/option port \'1234\'/option port \'9876\'/' …
                                            ⇑⇑    ⇑⇑            ⇑⇑    ⇑⇑

And here’s a much-less-cryptic way of doing it in awk:

awk $'
    /Server/             { matched=0 }
    /Server \'Test EF\'/ { matched=1 }
    matched              { gsub("option port \'1234\'", "option port \'9876\'") }
                         { print }
  '
  • Define a variable called matched that is 1 (only) when we’re in a Server 'Test EF' stanza.
  • Set it to 1 when we match Server 'Test EF', …
  • … and set it to 0 when we see any other line containing Server.
  • When matched is non-zero (i.e., when we’re in a Server 'Test EF' stanza), search each line for option port '1234' and substitute option port '9876'.