What Is the Best Way to Perform a Search and Replace On only Specific Sections of a File?

Viewed 124

I have a markdown file with sections separated by headings. I want to perform a search and replace only on specific sections; however, each section has similar content, so a global search and replace would end up affecting all sections. Because of this, I would need to somehow limit the search and replace to only certain sections of the file.

For example, say I wanted to replace all instances of foo with bar under # Section 1, # Section 3, and # Section 4 leaving # Section 2 and # Section 5 unchanged, as shown below

Sample Input:

# Section 1

- foo
- foo
- Unimportant Item
- foo
- Unimportant Item

# Section 2

- foo
- Unimportant Item

# Section 3

- foo
- Unimportant Item

# Section 4

- foo
- Unimportant Item
- foo

# Section 5

- foo
- foo

Sample Output

# Section 1

- bar
- bar
- Unimportant Item
- bar
- Unimportant Item

# Section 2

- foo
- Unimportant Item

# Section 3

- bar
- Unimportant Item

# Section 4

- bar
- Unimportant Item
- bar

# Section 5

- foo
- foo

If I didn't have to worry about the individual sections, a global search and replace would be trivial by using

sed -i 's/foo/bar/g' <input_file>

but I'm not sure if sed is capable of checking context to allow what I am looking for.

6 Answers

Here's a sed version:

sed -E '/^#[^#]\s*Section\s+[134]\s*$/, // s/foo/bar/' input.md

You may use this awk:

awk 'p {sub(/foo$/, "bar")} /^#/ {p = / (Section [134])$/} 1' file
# Section 1

- bar
- bar
- Unimportant Item
- bar
- Unimportant Item

# Section 2

- foo
- Unimportant Item

# Section 3

- bar
- Unimportant Item

# Section 4

- bar
- Unimportant Item
- bar

# Section 5

- foo
- foo

To make it more readable:

awk 'p {                          # if p==1 and current line # == n
   sub(/foo$/, "bar")             # replace foo with bar
}
/^#/ {                            # if line starts with #
   p = / (Section [134])$/        # set p = 1/0 if it matches sections
} 1' file

For completion, this awk answer will do the substitutions in the whole section, including the header:

awk '/^#/ { in_section = /Section [1|3|4]/ } in_section { sub(/foo/, "bar") } 1' input.md

If you want to exclude the headers from the substitution:

awk ' /^#/ { in_section = /Section [1|3|4]/; header_line = NR }
      in_section && (NR > header_line) { sub(/foo/, "bar") } 1' input.md

Detail

awk '/^#/ {                              # if in section header
        in_section = /Section [1|3|4]/;    # determine if section of interest (1/0)
        header_line = NR;                # value of header line to exclude
    }
    in_section && (NR > header_line) {   # if in section of interest and after header line
        sub(/foo/, "bar");               # substitute text
    } 1' input.md                        # 1 is to print all lines

My usual advice whenever you're considering sed -i is to use its older brother ed instead, as unlike sed, it's intended from the get-go to edit files (It's also POSIX standard, unlike sed -i, and thus more portable.)

Something like

ed -s input.md <<EOF
/Section 1/;/Section/s/foo/bar/g
/Section 3/;/Section/sg
w
EOF

Translated: In the block starting with the first line containing Section 1 and ending with the next Section line, replace foo with bar. Then do the same substitution in the Section 3 block. Finally, write the changes back to disk.

You can always provide multiple commands to sed with the -e option so that substitution occurs even when the sections are one after another:

sed  -e '/# Section 1/,/#/ s/foo/bar/' -e '/# Section 2/,/#/ s/foo/bar/' input.md

Multiple commands can also be placed in a "sed script file":

# content of script.sed
/# Section 1/,/#/ s/foo/bar/
/# Section 2/,/#/ s/foo/bar/

And you executed like this:

sed  -f script.sed input.md

A solution with sed

The key is in the range. The first addressing pattern matches the header(s) where we want the substitution to begin, and the second, matches all headers except the ones in the first addressing pattern. Note that the substitute command is inclusive of the first and last lines in the range (i.e. the headers).

sed -E '/^# Section [134]/, /^# Section [^134]/ s/foo/bar/' input.md

This one excludes the headers from the substitution:

sed -E '/^# Section [134]/, /^# Section [^134]/ { /^#/!s/foo/bar/ }' input.md
Related