Find multi-line text & replace it, using regex, in shell script

Viewed 1000

I am trying to find a pattern of two consecutive lines, where the first line is a fixed string and the second has a part substring I like to replace.

This is to be done in sh or bash on macOS.

If I had a regex tool at hand that would operate on the entire text, this would be easy for me. However, all I find is bash's simple text replacement - which doesn't work with regex, and sed, which is line oriented.

I suspect that I can use sed in a way where it first finds a matching first line, and only then looks to replace the following line if its pattern also matches, but I cannot figure this out.

Or are there other tools present on macOS that would let me do a regex-based search-and-replace over an entire file or a string? Maybe with Python (v2.7 and v3 is installed)?

Here's a sample text and how I like it modified:

  keyA
  value:474
  keyB
  value:474    <-- only this shall be replaced (follows "keyB")
  keyC
  value:474
  keyB
  value:474

Now, I want to find all occurances where the first line is "keyB" and the following one is "value:474", and then replace that second line with another value, e.g. "value:888".

As a regex that ignores line separators, I'd write this:

  • Search: (\bkeyB\n\s*value):474
  • Replace: $1:888

So, basically, I find the pattern before the 474, and then replace it with the same pattern plus the new number 888, thereby preserving the original indentation (which is variable).

3 Answers

You can use

sed -e '/keyB$/{n' -e 's/\(.*\):[0-9]*/\1:888/' -e '}' file
# Or, to replace the contents of the file inline in FreeBSD sed:
sed -i '' -e '/keyB$/{n' -e 's/\(.*\):[0-9]*/\1:888/' -e '}' file

Details:

  • /keyB$/ - finds all lines that end with keyB
  • n - empties the current pattern space and reads the next line into it
  • s/\(.*\):[0-9]*/\1:888/ - find any text up to the last : + zero or more digits capturing that text into Group 1, and replaces with the contents of the group and :888.

The {...} create a block that is executed only once the /keyB$/ condition is met.

See an online sed demo.

Use a perl one-liner with -0777 to scan over multiple lines:

$ # inline edit:
$ perl -0777 -i -pe 's/\bkeyB\s*value):\d*/$1:888/' file.txt
$ # to stdout:
$ cat file.txt | perl -0777 -pe 's/\bkeyB\s*value):\d*/$1:888/'

In plain bash:

#!/bin/bash

keypattern='^[[:blank:]]*keyB$'
valpattern='(.*):'
replacement=888

while read -r; do
    printf '%s\n' "$REPLY"
    if [[ $REPLY =~ $keypattern ]]; then
        read -r
        if [[ $REPLY =~ $valpattern ]]; then
            printf '%s%s\n' "${BASH_REMATCH[0]}" "$replacement"
        else
            printf '%s\n' "$REPLY"
        fi
    fi
done < file
Related