Chen Levy
Chen Levy

Reputation: 16408

excluding first and last lines from sed /START/,/END/

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 refining the "s:::" clause can give a 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

Upvotes: 52

Views: 23541

Answers (6)

Ville Laurikari
Ville Laurikari

Reputation: 29288

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, in "standard" sed 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?

GNU sed only

As mentioned in the comments, this more concise version will work for GNU sed, as long as you are only targeting OSes where the GNU version is certain to be present:

sed '/=sec1=/,/=sec2=/ { //! s/^/#/ }'

Using the sed on BSD derivatives (including macOS), you'll get this error message, though:

sed: 1: "/=sec1=/,/=sec2=/ { //! ...": bad flag in substitute command: '}'

On your system, GNU sed may be available as gsed. This is the case if, for example, you use MacPorts on macOS.

Upvotes: 45

Matt Whitlock
Matt Whitlock

Reputation: 897

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).

Upvotes: 1

Maxim_united
Maxim_united

Reputation: 2003

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

Upvotes: 2

MikhailVS
MikhailVS

Reputation: 473

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

Upvotes: 7

Paul Whittaker
Paul Whittaker

Reputation: 3947

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

Upvotes: 13

ghostdog74
ghostdog74

Reputation: 343057

you could also use awk

awk '/sec1/{f=1;print;next}f && !/sec2/{ $0="#"$0}/sec2/{f=0}1' file

Upvotes: 2

Related Questions