title: Regex for matching indented continuation lines
body: <p>Need match <code>key = value</code> pairs in arbitrary text using the following rules.</p>
  
  <ul>
  <li>the leading line has a structure: 
  
  <ul>
  <li>start with indentation - "two spaces or tab" at leas once, e.g.: <code>(  |\t)+</code></li>
  <li>the <code>+</code> character and one space</li>
  <li>words <code>VAR</code> or <code>CONST</code></li>
  <li>and the <code>key</code> and <code>value</code> using the <code>=</code> character</li>
  </ul></li>
  </ul>
  
  <p>Examples:</p>
  
  <pre><code>  + VAR somename = somevalue (indented with two spaces)
          + VAR name3 = indented by one \t
  </code></pre>
  
  <p>The following regex matches such lines:</p>
  
  <pre><code>/^(  |\t)+\+\s+(VAR|CONST)\s+(\w+)\s*=\s*(.*)$/
  </code></pre>
  
  <p>Now the problem: The syntax allows <strong>continuation lines</strong>, e.g. when the above line is followed by the line which starts at least one indentation sequence <code>(  |\t)</code> (aka TWO spaces or one tab) is considered as an continuation line and its whole content (with leading spaces too) should be the <code>value</code> for the key in previous line.</p>
  
  <p>Example:</p>
  
  <pre><code>  + VAR multi = 3 line value where the continuation lines
    are indented (starts with two spaces or one tab)
    and NOT followed by the '+'
  </code></pre>
  
  <p>e.g., the regex for the <em>continuation line</em> is</p>
  
  <pre><code>/^(  |\t)+([^\+](.*))$/
  </code></pre>
  
  <p>The solution is easy with <em>line based</em> approach, e.g. when I split the whole text into lines and processes it line-by-line.</p>
  
  <p>But, I looking for an (complex) regex (mainly for learning and benchmarking purposes) which could match the key=value pairs in one line or multiline form. Tried this:</p>
  
  <pre><code>while( $text =~ m/^(  |\t)+\+\s+(VAR|CONST)\s+(\w+)\s*=\s*((.*)$(?=(  |\t)+[^\+](.*)$)*)/gm ) {
      ...
  }
  </code></pre>
  
  <p>but I got:</p>
  
  <pre><code>(?=(  |\t)+[^\+](.*)$)* matches null string many times in regex; marked by &lt;-- HERE in m/^(  |\t)+\+\s+(VAR|CONST)\s+(\w+)\s*=\s*((.*)$(?=(  |\t)+[^\+](.*)$)* &lt;-- HERE )/ at so line 36.
  </code></pre>
  
  <p><strong>Side question:</strong> how to use the multi-line extended regexes, like:</p>
  
  <pre><code>/
     ^(  |\t)+      # &lt;- space ... :(
     \+\s+
     (VAR|CONST)
     \s+
     (\w+)
     \s*=\s*
     (.*)$
  /x
  </code></pre>
  
  <p>when the regex must contain exactly the SPACE character (e.g. can't use the universal <code>\s</code>)?</p>
  
  <p>If someone want help, here is a code which produces the wanted output (using the line-based approach) and also the non-working <code>regex-based</code> solution.</p>
  
  <pre><code>#!/usr/bin/env perl
  use 5.014;
  use warnings;
  use Data::Dumper;
  
  my $txt = do { local $/; &lt;DATA&gt; };
  
  my @matches1 = parse_by_lines($txt // '');
  mydump('BY LINES', @matches1);
  
  my @matches2 = parse_by_one_regex($txt // '');
  mydump('REGEX', @matches2);
  
  sub parse_by_lines { #produces the wanted output
      my ($text) = @_;
      my @match;
      my $havekey;
      for my $line (split "\n", $text) {
          if( $line =~ m/^(  |\t)+\+\s+(VAR|CONST)\s+(\w+)\s*=\s*(.*)$/ ) {
              push @match, { indent =&gt; $1, type =&gt; $2, key =&gt; $3, val =&gt; $4 };
              $havekey++;
          }
          elsif( $havekey &amp;&amp; $line =~ m/^(  |\t)+([^\+](.*))$/ ) {    #continuation line
              $match[-1]-&gt;{val} .= "\n$line"; #prserve the \n in the val
          }
          else {
              $havekey = 0;
          }
      }
      return @match;
  }
  
  
  sub parse_by_one_regex { #not working
      my ($text) = @_;
      my @match;
      while( $text =~ m/^(  |\t)+\+\s+(VAR|CONST)\s+(\w+)\s*=\s*((.*)$(?=(  |\t)+[^\+](.*)$)*)/gm ) {
          push @match, { indent =&gt; $1, type =&gt; $2, key =&gt; $3, val =&gt; $4 };
      }
      return @match;
  }
  
  sub mydump {
      my($label, @match) = @_;
      say "#### $label ####";
      for my $m ( @match ) {
          printf "%-6s: [%s]\n", $_, $m-&gt;{$_} for (qw(indent type key val));
          print "\n";
      }
  }
  
  __DATA__
  some arbitrary text lines
  or empty lines
  
      could be indented
    and could contain any character
  
    + VAR name1 = var indented by two spaces and the first nonspace character is '+'
  line of arbitrary text
      + VAR name2 = var indented by 2x2 spaces
  
      + VAR name3 = var indented by one \t
    + VAR name4 = the next line with "name5" is not valid. missing the = character, should not be matched
    + VAR name5
    + CONST name6 = the type could be VAR or CONST
  
    + VAR multi1 = multiline value where the continuation lines
    are indented (starts with two spaces or one tab) and NOT followed by the '+'
  
    + VAR multi1 = multiline value
      indented
  
    + VAR multi1 = multiline value
       indented ok too
  
  
    + VAR single = this is single line
    + because this line even if it is indented, the first nonspace character is '+'
  
    + VAR multi2 = multiline
    could be
       indented
          any way
    and any number of times
    until the first non-indented line
  
  the following should NOT match
  
  + VAR some = sould not be matched, because the line isn't indented
   + VAR some = sould not be matched, because the line isn't indented at least with TWO spaces or one tab
    + SOME name = value not matched because the SOME isn't VAR or CONST
  </code></pre>
  
  <hr>
  
  <p><strong>EDIT:</strong> using the accepted answer, and adding the wanted capture groups, got the following:</p>
  
  <pre><code>    while( $text =~ /
              (?m)            # multiline match
              ^               # at the start of the line
              ([ ]{2}|\t)+    # two spaces or tab - at least once
              \+              # the '+' character
              \s*             # followed by any number of spaces (e.g. "+VAR" or "+    VAR" are valid)
              (VAR|CONST)     # the VAR or CONST
              \s+             # followed at least one space (e.g. the "VAR_" should not matched)
              (\w+)           # the keyword
              \s*=\s*         # the '=' surrounded (and consumed) by any number of spaces
              (               # capture the whole value (as it is)
                      .*                      # any string up to end of line
                      (?:                     # followed by (non-capturing group)
                              \R              # one line-break
                              ^               # at the start of the line
                              (?&gt;[ ]{2,}|\t+) # atomic group - at least two spaces or at least one tab
                              [^+]            # followed by any character but '+'
                              .*              # any string up the end of line
                      )*              # any number of times (e.g. optionally)
              )
      /xg) {
              push @match, { indent =&gt; $1, type =&gt; $2, key =&gt; $3, val =&gt; $4 };
      }
  </code></pre>
  
  <hr>
  
  <p><strong>EDIT2</strong> And yes, the regex based solution is 34% faster (at least on my HW).</p>