10

I have Perl code:

my $s =  "The+quick+brown+fox+jumps+over+the+lazy+dog+that+is+my+dog";

I want to replace every + with space and dog with cat.

I have this regular expression:

$s =~ s/\+(.*)dog/ ${1}cat/g;

But, it only matches the first occurrence of + and last dog.

Mark Canlas
  • 9,385
  • 5
  • 41
  • 63
user332951
  • 359
  • 1
  • 3
  • 18
  • 5
    Wouldn't it simplify things to use two separate regular expression substitutions for this? – WhirlWind May 05 '10 at 00:42
  • Would you please ask the real question? – codeholic May 05 '10 at 05:25
  • 1
    If you are looking for performance, you should have asked in your question, but the way to get performance might be to do this entirely without regular expressions. Did you try tr, for example? – WhirlWind May 05 '10 at 20:09

7 Answers7

12

You can use the 'e' modifier to execute code in the second part of an s/// expression.

$s =~ s/(\+)|(dog)/$1 ? ' ' : 'cat'/eg;

If $1 is true, that means the \+ matched, so it substitutes a space; otherwise it substitutes "cat".

Brock
  • 330
  • 1
  • 6
  • 1
    this solution work great but when i ran through profile it seem that two line work faster than one line because it has to call CORE:substcont rather just CORE:subst. But anyway thank you very much I already have two lines solution in place. What happen is i have a file with string of 100k of lines I will need to normalize it before insert into DB. I am trying to speed things up. I was under impression if I ran through regexp match one time will be faster if I have to do it two times – user332951 May 05 '10 at 19:22
  • 2
    Chad: To keep it from running loose around the neighborhood? :) Right, no need to capture "dog". `$s =~ s/(\+)|dog/$1 ? ' ' : 'cat'/eg;` – Brock May 05 '10 at 22:49
  • @duenguyen: One regex is faster than two regexes **unless** the single regex is substantially more complex than either of the two regexes, as it is in this case. Using the `/e` modifier is one of the biggest regex performance killers in Perl because it allows Perl code to be run within the regex, which adds enormous complexity to it. – Dave Sherohman May 06 '10 at 11:00
  • Does using `e` modifier have security implications? – Leo Galleguillos Jun 03 '17 at 21:09
11

Perl 5.14 and newer has the ability to chain substitutions with a non-destructive assignment so you can kill 3 birds with one stone: do your two global substitutions plus assign the result to a new variable without modifying your original variable.

my $s =  "The+quick+brown+fox+jumps+over+the+lazy+dog+that+is+my+dog";
my $result = $s =~ s/+/ /gr 
                =~ s/dog/cat/gr; 

Will replace all your + with space and replace every dog with cat, assigning the result into a new variable. In a one-liner.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
boson
  • 111
  • 1
  • 4
  • 1
    need to escape the +: ($s =~ s/\+/ /gr =~ s/dog/cat/gr) – Frank Oct 07 '19 at 15:33
  • nice, this was exactly what i was looking for, I always forget these little things, and since 5.14 is pretty old now I don't think we have to worry so much about it unless we are using it on some old standalone device in which case, we would have to revert to the old way :) – osirisgothra Dec 10 '22 at 19:57
10

Two regular expressions might make your life a lot easier:

$s =~ s/\+/ /g;
$s =~ s/dog/cat/g;

The following matches "+," followed by a bunch of stuff, followed by "dog." Also, "+" is technically a metacharacter.

/+(.*)dog/
WhirlWind
  • 13,974
  • 3
  • 42
  • 42
  • I already have two lines solution in place. What happen is i have a file with string of 100k of lines I will need to normalize it before insert into DB. I am trying to speed things up. I was under impression if I ran through regexp match one time will be faster if I have to do it two times – user332951 May 05 '10 at 18:07
  • That seems to be the wrong impression given one of the newer answers below. – Mark Canlas May 05 '10 at 18:37
7

A hash may do what you want:

#!/usr/bin/perl

use strict;
use warnings;

my $s =  "The+quick+brown+fox+jumps+over+the+lazy+dog+that+is+my+dog";

my %replace = (
    "+" => " ",
    dog => "cat",
);

$s =~ s/([+]|dog)/$replace{$1}/g;

print "$s\n";

In the comments I see that you are concerned with performance, the two regex solution is more performant. This is because any solution that works for one regex will need to use captures (which slow down the regex).

Here are the results of a benchmark:

eval: The quick brown fox jumps over the lazy cat that is my cat
hash: The quick brown fox jumps over the lazy cat that is my cat
two: The quick brown fox jumps over the lazy cat that is my cat
         Rate hash eval  two
hash  33184/s   -- -29% -80%
eval  46419/s  40%   -- -72%
two  165414/s 398% 256%   --

I used the following benchmark:

#!/usr/bin/perl

use strict;
use warnings;

use Benchmark;

my $s =  "The+quick+brown+fox+jumps+over+the+lazy+dog+that+is+my+dog";

my %replace = (
    "+" => " ",
    dog => "cat",
);

my %subs = (
    hash => sub {
        (my $t = $s) =~ s/([+]|dog)/$replace{$1}/g;
        return $t;
    },
    two => sub {
        (my $t = $s) =~ s/[+]/ /g;
        $t =~ s/dog/cat/g;
        return $t;
    },
    eval => sub {
        (my $t = $s) =~ s/(\+)|(dog)/$1 ? ' ' : 'cat'/eg;
        return $t;
    },
);

for my $k (sort keys %subs) {
    print "$k: ", $subs{$k}(), "\n";
}

Benchmark::cmpthese -1, \%subs;
Chas. Owens
  • 64,182
  • 22
  • 135
  • 226
4

Simple answer - use 2 lines!:

$s =~ s/+/ /g;
$s =~ s/dog/cat/g;

It could probably be done in one line with 'non-greedy' matching, but this should do the trick

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
laher
  • 8,860
  • 3
  • 29
  • 39
3

If speed is important, you should probably stick with two lines. But when I need to do multiple substitions at once I usually care more about convenience, so I use a hash like suggested by Chas. Owens. Two advantages over the two-liner being that it's easy to modify, and it behaves like expected (e.g. when substituting "cat" for "dog" and "dog" for "cat" at the same time).

However, I am much to lazy to write the regex by hand and prefer to assemble it with join, and use map to escape stuff:

#!/usr/bin/perl

use strict;
use warnings;

my $s = "The+quick+brown+fox+jumps+over+the+lazy+dog+that+is+my+dog";

my %replace = (
    "+" => " ",
    dog => "cat",
);

my $regex = join "|", 
    #use quotemeta to escape special characters
    map  { quotemeta } 
    #reverse sort the keys because "ab" =~ /(a|ab)/ returns "a"
    sort { $b cmp $a } keys %replace;

#compiling the regex before using it prevents
#you from having to recompile it each time
$regex = qr/$regex/;

$s =~ s/($regex)/$replace{$1}/g;

print "$s\n";
Chas. Owens
  • 64,182
  • 22
  • 135
  • 226
Pontus
  • 1,639
  • 2
  • 11
  • 6
  • Me myself prefer code re-ability and convenience anytime but for this special case that I am doing I have to read string that anywhere from 100k to 1Mils of line every 3 mins. Therefore I have to worry about speed or things will get clog up in the queue – user332951 May 05 '10 at 21:02
  • 1
    Chas: Thanks for the corrections. I have only been using stackoverflow for a few days, and I am already learning useful things. I should probably go looking for places where I may have used this kind of code... Just one question, why not s/$regex/$replace{$&}/g ? – Pontus May 08 '10 at 15:06
  • duenguyen: sounds like a fun! – Pontus May 08 '10 at 15:07
  • 1
    from the perldoc (http://perldoc.perl.org/perlvar.html#%24%26): "The use of this variable anywhere in a program imposes a considerable performance penalty on all regular expression matches." Basically you should always use captures, not `$\``, `$&`, or `$'`. – Chas. Owens May 10 '10 at 15:09
3

I know this is an old thread, but here's a one-liner for Perls earlier than v5.14:

my $s = 'The+quick+brown+fox+jumps+over+the+lazy+dog+that+is+my+dog';
$s = do {local $_ = $s; s/\+/ /g; s/dog/cat/g; $_};
textral
  • 1,029
  • 8
  • 13