2

Essentially, I'm trying to get the content between two string, but that use the same character repeated N time, like so:

===
This is a test
===

====
Another test
====

==
Last test
==

The above is just an example of course. Here what I tried and their result:

sed -u '/==/!d;s//&\n/;s/.*\n//;:a;/==/bb;$!{n;ba};:b;s//\n&/;P;D' testfile

Give

=
This is a test

=




Another test






Last test


If I were to use only one of the above in the testfile:


Last test


This would give the wanted result (albeit with too many added newlines but that's fine for this example).

The above works only if there a single instance of those repeated characters or if it is a pair of unique strings enclosing the content...

How can I get the content between two strings using the same number of repeated characters? I prefer to use either grep, sed or awk for this.

2
  • Can your input have different number of = in between two equal number of = characters? In other words, would sed -nE '/^=+$/,//{//!p}' testfile fail for your usecase?
    – Sundeep
    May 8 at 15:09
  • no, most "content" between the example two string wouldn't have any of the character used as "enclosing". :) @Sundeep May 8 at 16:46
8

We use the awk utility to employ flip flop logic to toggle from on state to off as shown:

$ awk -v str="==" '
      $0""==str{f=!f;next};f
' testfile
Last test

Using Posixly sed constructs we can implement the flip flop logic:

sed -ne '
  /^==$/{
    x;        # access state info from hold
    s/^$/0/;  # initialize state
    y/01/10/; # toggle state
    x;        # save state in hold
    d;        # next
  }
  G;/\n1/P
' testfile

Employing the GNU version of the stream editor sed in its extended mode -E

$ sed -Ee '
    $!N; /^(\S)\1+\n/!D
    :loop
        $d;N
        s/^(.*)\n(.*)\n\1$/\2/;t
    bloop
' testfile
This is a test
Another test
Last test

Notes:-

  • Keep a two-line pattern space by means of the N command.
  • keep rejecting lines until we meet the golden line (=> one in which the first portion of the pattern space comprises only a single type of nonwhitespace char)
  • once such a line is found, then we loop till we meet its exact replica atleast two lines down the road. And we"re done finding the first set.
  • This process repeats till we hit the eof.

Using the flip-flop operator ... in perl we can do like as shown:

perl -lne 'print if 
 /^(\S)\1+$(?{$a=$_})/ ... $_ eq $a and $_ ne $a;
' testfile

For fixed predetermined strings to search for is even simpler , for then we don't have to craft a regex rather a string equality test would suffice:

$ perl -nlse 'print if
    $_ eq $a ... $_ eq $a and $_ ne $a;
' -- -a=== testfile

$ sed -Ee '
    /^==$/!d
    $!N
    :a
        $d;N
        s/^(.*)\n(.*)\n\1$/\2/;t
    ba
' testfile
3
  • Essentially, I would prefer if this only output one result depending on the number of repeated char used. eg: if you were to use the pattern for getting the string "Another test", then by using "====" it would only output every string being enclosed in that...etc May 8 at 15:30
  • 1
    Why do you need the “” after $0 in the awk example?
    – Jonah
    May 8 at 23:36
  • 2
    That is to guard against the possibility of a false match when str is 0 and $0 is an empty line. We coerce a string comparison at all times by doing this.
    – guest_7
    May 8 at 23:51
4

I'd use perl:

$ perl -0777 -ne 'print $3 while /^((\S)\2+\n)(.*?)^\1/smg' < your-file
This is a test
Another test
Last test

Or pcregrep:

$ pcregrep -Mo3 '(?s)^((\S)\2+\n)(.*?)\n?^\1' < your-file
This is a test
Another test
Last test

If it's only about returning what's between fixed delimiters:

$ pcregrep -Mo1 '(?s)^==\n(.*?)\n?^==$' < your-file
Last test
1
  • I would prefer if it only outputs one result depending on the number of repeated char used. eg: if you use "==" as input, then it would only output the "Last test" string, without outputting anything else...etc May 8 at 15:29
4

tl;dr

$ sed '/^==*$/,//{//!p};d' testfile
This is a test
Another test
Last test

On first look, a simple range could print all pairs (no loops needed):

$ sed -n '/^=/,//p' testfile
===
This is a test
===
====
Another test
====
==
Last test
==

That prints every line between a line that starts with = and the next repeated regex (\\).

That could be improved to a line that only contains =: \^==*$\.

And to remove all the markers:

$ sed -n '/^==*$/,//H;${x;s/\n==*//g;s/^\n//;p}' testfile
This is a test
Another test
Last test

Or, in a shorter form:

$ sed -n '/^==*$/,//{//d;p}' testfile
This is a test
Another test
Last test

To match an exact number of = change the regex to:

$ sed -n '/^==$/,//{//d;p}' testfile
Last test

And, to avoid the -n option:

$ sed '/^==$/,//{//!p};d' testfile
Last test

In awk it could be done like:

$ awk 'p==0 && /^==*$/ {p=1;next}
       p==1 && /^==*$/ {p=0}
       p          
      ' testfile

This is a test
Another test
Last test

Or, in a bit less explicit form:

awk ' /^==*$/ { p = !p ; next}
      p
    ' testfile
3

Seems like your question wasn't clear and you got various types of solution. To get only content surrounded by == with GNU sed (syntax/feature might vary with other implementations):

$ sed -n '/^==$/,//{//!p}' testfile
Last test

sed has a way to select a range of lines by specifying two addresses separated by a comma. To avoid repeating the regexp you can use // here. Since you do not want the marker lines themselves in the output, you can negate those matches. {} is used to group commands to be executed only for the matching lines.

1
  • 1
    +1 star answer.
    – guest_7
    May 9 at 4:42
2

command:

awk '{a[++i]=$0}/==/{for(x=NR-1;x<NR;x++)print a[x]}' filename|sed '/^$/d'

output

This is a test
Another test
Last test
1
  • Yes, but i need only one output (see the comment on the other answer). And only if I use a specific pattern like say "==" instead of the other one in the example, then it would only output "Last test" May 8 at 16:43
2

In case the input file is guaranteed to have empty lines between the enclosed content it doesn't include lines having only --, which is the case with testfile, there is grep-only solution:

$grep -A 1 '^==$' testfile | grep -v '^==$' | grep -v '^--$' | grep .
Last test
  • grep -A 1 '^==$' will print one line after matching the string enclosing the content
  • grep -v '^==$' will remove the lines in the result which contain the string enclosing the content
  • grep -v '^--$' will remove the group separator which would appear in the result when using the -A option
  • grep . will filter out only the lines containing at least one character which will remove the empty lines

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.