51

Consider the input:

=sec1=
some-line
some-other-line

foo
bar=baz

=sec2=
c=baz

If I wish to process only =sec1= I can for example comment out the section by:

sed -e '/=sec1=/,/=[a-z]*=/s:^:#:' < input

... well, almost.

This will comment the lines including "=sec1=" and "=sec2=" lines, and the result will be something like:

#=sec1=
#some-line
#some-other-line
#
#foo
#bar=baz
#
#=sec2=
c=baz

My question is: What is the easiest way to exclude the start and end lines from a /START/,/END/ range in sed?

I know that for many cases refinement of the "s:::" claws can give solution in this specific case, but I am after the generic solution here.

In "Sed - An Introduction and Tutorial" Bruce Barnett writes: "I will show you later how to restrict a command up to, but not including the line containing the specified pattern.", but I was not able to find where he actually show this.

In the "USEFUL ONE-LINE SCRIPTS FOR SED" Compiled by Eric Pement, I could find only the inclusive example:

# print section of file between two regular expressions (inclusive)
sed -n '/Iowa/,/Montana/p'             # case sensitive
Chen Levy
  • 15,438
  • 17
  • 74
  • 92

6 Answers6

43

This should do the trick:

sed -e '/=sec1=/,/=sec2=/ { /=sec1=/b; /=sec2=/b; s/^/#/ }' < input

This matches between sec1 and sec2 inclusively and then just skips the first and last line with the b command. This leaves the desired lines between sec1 and sec2 (exclusive), and the s command adds the comment sign.

Unfortunately, you do need to repeat the regexps for matching the delimiters. As far as I know there's no better way to do this. At least you can keep the regexps clean, even though they're used twice.

This is adapted from the SED FAQ: How do I address all the lines between RE1 and RE2, excluding the lines themselves?

Ville Laurikari
  • 28,380
  • 7
  • 60
  • 55
  • For folks on macOS with BSD sed you need to use actual newlines instead of semicolons. For more details check out [this answer](http://stackoverflow.com/a/15470635/2909897) – mbigras May 09 '17 at 22:39
  • 11
    not sure about other versions, but with GNU sed, this could be easily done using `'/=sec1=/,/=sec2=/ { //! s/^/#/ }'` ... from [manual](https://www.gnu.org/software/sed/manual/sed.html#Regexp-Addresses) `empty regular expression ‘//’ repeats the last regular expression match` – Sundeep Jun 20 '17 at 09:37
  • @Sundeep Given that this is the most concise expression, why not post it as answer? – yugr Oct 11 '19 at 10:15
14

If you're not interested in lines outside of the range, but just want the non-inclusive variant of the Iowa/Montana example from the question (which is what brought me here), you can write the "except for the first and last matching lines" clause easily enough with a second sed:

sed -n '/PATTERN1/,/PATTERN2/p' < input | sed '1d;$d'

Personally, I find this slightly clearer (albeit slower on large files) than the equivalent

sed -n '1,/PATTERN1/d;/PATTERN2/q;p' < input

MikhailVS
  • 453
  • 5
  • 4
Paul Whittaker
  • 3,817
  • 3
  • 25
  • 20
  • nice. also, the second version doesn't blow up php because of that pesky $. – orolo Oct 25 '12 at 16:02
  • 6
    I have a hunch, they're not equivalent, should there be more than one of those ranges in the stream/file. – Dan Mar 08 '18 at 14:22
7

Another way would be

sed '/begin/,/end/ {
       /begin/n
       /end/ !p
     }'

/begin/n -> skip over the line that has the "begin" pattern
/end/ !p -> print all lines that don't have the "end" pattern

Taken from Bruce Barnett's sed tutorial http://www.grymoire.com/Unix/Sed.html#toc-uh-35a

MikhailVS
  • 453
  • 5
  • 4
2

I've used:

sed '/begin/,/end/{/begin\|end/!p}'

This will search all the lines between the patterns, then print everything not containing the patterns

Maxim_united
  • 1,911
  • 1
  • 14
  • 23
1

you could also use awk

awk '/sec1/{f=1;print;next}f && !/sec2/{ $0="#"$0}/sec2/{f=0}1' file
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
1

You don't have to repeat any regular expression(s) to make this work.

$ sed -e '/^=sec1=$/{:0;n;/^=\w*=$/!{s/^/#/;b0}}' <<EOF
=sec1=
some-line
some-other-line

foo
bar=baz

=sec2=
c=baz
EOF

The output will be:

=sec1=
#some-line
#some-other-line
#
#foo
#bar=baz
#
=sec2=
c=baz

Taking apart the sed script, we have:

  1. /^=sec1=$/ a regular expression address matching the opening section marker
  2. {:0;n;/^=\w*=$/!{s/^/#/;b0}} a command block, as follows:
    1. :0 a label to return to later
    2. n print the current line, and read the next line
    3. /^=\w*=$/! a regular expression address matching any line that isn't a section marker
    4. {s/^/#/;b0} a command block, as follows:
      1. s/^/#/ prepend a # to the line
      2. b0 branch to label 0

The inner loop between :0 and b0 continues looping until it encounters any line that is a section marker (or the end of the file).

Matt Whitlock
  • 756
  • 7
  • 10