Why does this Regex match only show the first word in the capture variable, not the whole line?

Viewed 113

I'm fairly new to Perl and Regexes, so please be patient with me if I misuse terminology.

I'm attempting to read a text file containing a movie script, and use a Regex to display all lines spoken by a particular character. Here's the excerpt I'm using:

BRIAN: Hello, mother.
MANDY: Don't you 'hello mother' me. What are all those people doing out ther    e?!
BRIAN: Oh. Well-- well, I, uh--
MANDY: Come on! What have you been up to, my lad?!
BRIAN: Well, uh, I think they must have popped by for something.
MANDY: 'Popped by'?! 'Swarmed by', more like! There's a multitude out there!
BRIAN: Mm, they-- they started following me yesterday.
MANDY: Well, they can stop following you right now. Now, stop following my son! You ought to be ashamed of yourselves.
FOLLOWERS: The Messiah! The Messiah! Show us the Messiah!
MANDY: The who?
FOLLOWERS: The Messiah!
MANDY: Huh, there's no Messiah in here. There's a mess, all right, but no Me    ssiah. Now, go away!
FOLLOWERS: The Messiah! The Messiah!
MANDY: Ooooh.
FOLLOWERS: Show us the Messiah! The Messiah! The Messiah! Show us the Messiah!
MANDY: Now, you listen here! He's not the Messiah. He's a very naughty boy! Now, go away!

And here's the code:

  1 use strict;
  2 use warnings;
  3 
  4 my $filename = "movie_script.txt";
  5 my $charname = $ARGV[0];
  6 
  7 if (-e $filename) {
  8     print "File exists.\n";
  9 } else {
 10     print "Alas, file does not exist.\n";
 11     exit 1;
 12 }
 13 
 14 open(my $fh, '<', $filename);
 15 
 16 my $match = "^($charname):.*/i";
 17 
 18 while (my $line = <$fh>) {
 19     if ( $line =~ m/^($charname):.*/i ) {
 20         $line =~ s/($charname): //i;
 21         print $line;                                                
 22     }
 23 }   
 24 print "\n";
 25 close $fh;

The code works fine, and when I run the program, passing "Brian" as a command-line argument, it only shows me Brian's lines, ditto if I enter "Mandy" or "Followers" (all case-insensitive.)

I'm trying to understand how capture variables work so I can more sensitively manipulate the text file. When I change line 21 to print $1 instead of print $line, I would have expected the result to be the same, because the regex I've supplied should match any instance of "BRIAN", followed by a colon, then any number of characters until the end of the line.

When I do this, however, it just returns:

BRIANBRIANBRIANBRIAN

... Instead of the four lines belonging to Brian. So I tried swapping lines 22 and 21, putting the print $1 statement before the regex substitution, but that returns the same result.

Why is the capture variable only showing the first word "BRIAN" and not the whole line? I'm sure it's a very simple error, but I'm struggling to understand what I'm doing wrong.

3 Answers

$1 is your first capture group: the part which matched the first pair of parentheses in your regex.

If you had a regex with 2 sets of parentheses, $2 would be what matched the second part.

Here is an alternative to that part of your script:

my $match = qr/^($charname):\s*(.*)/i;

while (my $line = <$fh>) {
    if ( $line =~ m/$match/ ) {
        print "Character : $1\n",
              "text      : $2\n";                                                
    }
}   

And just for fun, here would be a shortened version of your full script, with comments on the regex parts:

#!/usr/bin/env perl

use strict;
use warnings;

my $filename = "/tmp/y";
my $charname = $ARGV[0];

open(my $fh, '<', $filename)
  or die "Cannot find $filename\n";

my $match = qr/^\s* ($charname) \s*:\s* (.*)/ix;
#               |   |              |     |   | \ extended regex which allows spaces for readability
#               |   |              |     |   \ case insensitive
#               |   |              |     \ capture the rest of the line into $2
#               |   |              \ colon, optionally with spaces before and/or after
#               |   \ capture the name into $1
#               \ also accept spaces before the name


while ( <$fh> ) {   # use the default $_ variable instead of unneeded $line
    print "$2\n" if ( /$match/ );
}

print "\n";
close $fh;

Let's look at your code:

while (my $line = <$fh>) {
    if ( $line =~ m/^($charname):.*/i ) {
        $line =~ s/($charname): //i;
        print $line;                                                
    }
} 

In your first line:

while (my $line = <$fh>) {

You read a line from $fh into $line. That's fine. Then we look for your character name:

if ( $line =~ m/^($charname):.*/i ) {

You look for the character name at the start of the string (that's what ^ does), followed by a colon and then any other characters. That .* is pointless as it doesn't change what the regex matches at all.

But the parentheses that you put around $charname do something interesting. They capture the bit of the string that matches that part of the regex and store it in $1. Now, that's a bit wasteful, to be honest. As $charname is a fixed string, you already know what's going to end up in $1. It'll be "BRIAN" or whichever character you're looking for.

$line =~ s/($charname): //i;
print $line;

You then edit $line to remove the character name and the colon (and a space) from the start of the line. So you just get the line that is spoken. And you print that.

So far so good. Your code is a bit wasteful in places, but it does what you think.

Then you change the line:

print $line;

To:

print $1;

And you get confused :-)

But as we've already seen, the capturing parentheses will store "BRIAN" in $1. So if you print $1, you'll see "BRIAN".

You ask,

Why is the capture variable only showing the first word "BRIAN" and not the whole line?

And the answer is, because that's what you've asked it to do. $1 will contain what is inside the capturing parentheses. Which is $charname. Which is "BRIAN". The rest of the regex match is outside of the parentheses, so it doesn't end up in $1.

Does that make sense?

Please investigate the following perl script how desired output can be achieved.

NOTE:

  • Input test data stored in __DATA__ block
  • For reading from a file replace <DATA> with <> and run as movie_script.pl BRIAN movie_script.txt.
use strict;
use warnings;
use feature 'say';

my $charname = shift or die 'Specify character';

say $charname;
/^$charname: (.*)\Z/ && say $1 for <DATA>;

__DATA__
BRIAN: Hello, mother.
MANDY: Don't you 'hello mother' me. What are all those people doing out ther    e?!
BRIAN: Oh. Well-- well, I, uh--
MANDY: Come on! What have you been up to, my lad?!
BRIAN: Well, uh, I think they must have popped by for something.
MANDY: 'Popped by'?! 'Swarmed by', more like! There's a multitude out there!
BRIAN: Mm, they-- they started following me yesterday.
MANDY: Well, they can stop following you right now. Now, stop following my son! You ought to be ashamed of yourselves.
FOLLOWERS: The Messiah! The Messiah! Show us the Messiah!
MANDY: The who?
FOLLOWERS: The Messiah!
MANDY: Huh, there's no Messiah in here. There's a mess, all right, but no Me    ssiah. Now, go away!
FOLLOWERS: The Messiah! The Messiah!
MANDY: Ooooh.
FOLLOWERS: Show us the Messiah! The Messiah! The Messiah! Show us the Messiah!
MANDY: Now, you listen here! He's not the Messiah. He's a very naughty boy! Now, go away!

Output sample movie_script.pl BRIAN

BRIAN
Hello, mother.
Oh. Well-- well, I, uh--
Well, uh, I think they must have popped by for something.
Mm, they-- they started following me yesterday.

Output sample movie_script.pl FOLLOWERS

FOLLOWERS
The Messiah! The Messiah! Show us the Messiah!
The Messiah!
The Messiah! The Messiah!
Show us the Messiah! The Messiah! The Messiah! Show us the Messiah!
Related