File Coverage

blib/lib/List/Regexp.pm
Criterion Covered Total %
statement 168 183 91.8
branch 67 86 77.9
condition 8 9 88.8
subroutine 15 15 100.0
pod 0 6 0.0
total 258 299 86.2


line stmt bran cond sub pod time code
1             # -*- perl -*-
2             # Copyright (C) 2015-2016 Sergey Poznyakoff
3             #
4             # This program is free software; you can redistribute it and/or modify
5             # it under the terms of the GNU General Public License as published by
6             # the Free Software Foundation; either version 3, or (at your option)
7             # any later version.
8             #
9             # This program is distributed in the hope that it will be useful,
10             # but WITHOUT ANY WARRANTY; without even the implied warranty of
11             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12             # GNU General Public License for more details.
13             #
14             # You should have received a copy of the GNU General Public License
15             # along with this program. If not, see .
16              
17             package List::Regexp;
18              
19 6     6   9525 use strict;
  6         9  
  6         133  
20 6     6   18 use Carp;
  6         6  
  6         353  
21 6     6   3202 use Data::Dumper;
  6         41913  
  6         318  
22 6     6   33 use warnings;
  6         7  
  6         1210  
23              
24             require Exporter;
25             our @ISA = qw(Exporter);
26              
27             our %EXPORT_TAGS = ( 'all' => [ qw(regexp_opt) ] );
28             our @EXPORT_OK = ( qw(regexp_opt) );
29             our @EXPORT = qw(regexp_opt);
30             our $VERSION = "1.03";
31              
32             # Synopsis:
33             # my @res = find_prefix(AREF)
34             # Arguments:
35             # AREF is a reference to a sorted ARRAY of char array references.
36             # Description:
37             # Find N first elements of ARRAY sharing the shortest common prefix (of
38             # length L).
39             # Return value:
40             # (N, L)
41              
42             sub find_prefix {
43 47     47 0 42 my $aref = shift;
44              
45 47         29 my ($n, $l);
46              
47 47         45 my $c = $aref->[0][0];
48 47   100     46 for ($n = 0; $n+1 <= $#{$aref} and $aref->[$n+1][0] eq $c; $n++) {};
  64         187  
49            
50             OUTER:
51 47         41 for ($l = 0; $l < $#{$aref->[0]}; $l++) {
  73         133  
52 31         52 $c = $aref->[0][$l+1];
53 31         47 for (my $i = 1; $i <= $n; $i++) {
54 18 100 66     13 last OUTER if ($l+1 > $#{$aref->[$i]} or $aref->[$i][$l+1] ne $c);
  18         78  
55             }
56             }
57 47         104 return ($n,$l);
58             }
59              
60             # Each node of the parse tree is a list. Its 0th element keeps the type of
61             # the node. Its lowest byte is one of the following:
62              
63             # Rest of the node is a list of alternatives.
64 6     6   29 use constant T_ALT => 0x0;
  6         7  
  6         305  
65              
66             # A prefixed list of alternatives. Element 1 is the prefix string and
67             # element 2 is a reference to the list.
68 6     6   22 use constant T_PFX => 0x1;
  6         15  
  6         237  
69             # A suffixed list of alternatives. Element 1 is the suffix string and
70             # element 2 is a reference to the list.
71 6     6   21 use constant T_SFX => 0x2;
  6         6  
  6         220  
72              
73             # This mask is used to get the node type:
74 6     6   20 use constant T_MASK => 0xf;
  6         7  
  6         214  
75              
76             # If the type is ORed with T_OPT, the element is optional.
77 6     6   20 use constant T_OPT => 0x10;
  6         6  
  6         7403  
78              
79             # Synopsis:
80             # my @list = parse(ARRAY)
81             # Arguments:
82             # ARRAY is a sorted array of char array references.
83             # Description:
84             # Recursively parse the array of arguments and return a parse tree.
85             sub parse {
86 14     14 0 19 my @t = @_;
87 14         13 my @output;
88 14 50       33 return [] if $#t == -1;
89 14         16 while (1) {
90 27         40 my @res = find_prefix \@t;
91 27 100       47 if (!$res[0]) {
    50          
92 20         24 my @rv = map { [ reverse @{$_} ] } @t;
  50         34  
  50         76  
93 20         52 @res = find_prefix \@rv;
94 20 100       31 if ($res[0]) {
95 1         3 my @x = reverse @{$rv[0]}[0..$res[1]];
  1         3  
96 1         1 my $sfxlen = $#x;
97 1         2 my $sfx = join('', @x);
98 1         4 my $type = T_SFX;
99 1         2 my $prefixes = parse(map { my @r = @{$_};
  3         2  
  3         5  
100 3 100       5 if ($sfxlen == $#r) {
101 1         1 $type |= T_OPT;
102 1         8 ();
103             } else {
104 2         5 [ @r[0..$#r-$sfxlen-1] ];
105             }
106             } @t[0..$res[0]]);
107 1         3 push @output, [ $type, $sfx, $prefixes ];
108             } else {
109 19         30 push @output, map { join('', @{$_}) } @t[0..$res[0]];
  19         13  
  19         51  
110             }
111             } elsif ($res[0] == 0) {
112 0         0 push @output, join('', @{$t[0]});
  0         0  
113             } else {
114 7         8 my @x = @{$t[0]}[0..$res[1]];
  7         11  
115 7         5 my $pfxlen = $#x;
116 7         11 my $pfx = join('', @x);
117 7         6 my $type = T_PFX;
118 7         8 my $suffixes = parse(map { my @r = @{$_};
  22         27  
  22         28  
119 22 100       27 if ($pfxlen == $#r) {
120 3         3 $type |= T_OPT;
121 3         2 ();
122             } else {
123 19         73 [ @r[$pfxlen+1..$#r] ];
124             }
125             } @t[0..$res[0]]);
126 7         11 push @output, [ $type, $pfx, $suffixes ];
127             }
128 27 100       47 last if $res[0] == $#t;
129 13         26 @t = @t[($res[0]+1)..$#t];
130             }
131 14         22 return \@output;
132             }
133            
134             # ###################################
135             # Convert parse tree to a regexp
136             #
137             # The functions below take hash and string reference as their first two
138             # arguments.
139             #
140             # The first argument is a reference to a configuration hash, which contains
141             # the following keys:
142             #
143             # rxchars: A Perl regular expression matching special characters, which should
144             # be escaped with a backslash on output:
145             # posix [][\\<>.(){}?*+^\$]
146             # pcre [][\\.(){}?*^+\$]
147             #
148             # group: A reference to a list of two elements containig markers for
149             # parenthesized groups. Non-capturing groups are used, if possible.
150             # posix [ '(', ')' ]
151             # pcre [ '(?:', ')' ]
152             #
153             # branch: A delimiter used to separate branches ('|' for both posix and
154             # pcre)
155             #
156             # The second argument is a reference to a string where the generated
157             # expression will be stored.
158             #
159             # ###################################
160              
161             # Synopsis:
162             # my $s = escape_re_chars(CONF, STRING)
163             # Arguments:
164             # See above.
165             # Description:
166             # Escape special characters in the STRING
167             # Return value:
168             # Escaped string, safe to use in regular expressions.
169             sub escape_re_chars {
170 14     14 0 17 my ($conf,$s) = @_;
171 14         77 $s =~ s/($conf->{rxchars})/\\$1/g;
172 14         24 return $s;
173             }
174              
175             # Synopsis:
176             # nodelist_to_regexp(CONF, LIST...)
177             # Arguments:
178             # CONF and STRING as described above.
179             # LIST is a subtree.
180             # Description:
181             # Convert subtree into regular expression.
182             sub nodelist_to_regexp {
183 14     14 0 11 my $conf = shift;
184 14         12 my $opt = shift;
185 14         11 my @alternations;
186             my @cclass;
187 0         0 my $s;
188 0         0 my $set;
189            
190 14         22 foreach my $elt (@_) {
191 27 100       56 if (ref($elt) eq 'ARRAY') {
    100          
192 8         19 push @alternations, generic_regexp($conf, $elt);
193             } elsif (length($elt) == 1) {
194 13         18 push @cclass, $elt;
195             } else {
196 6         8 push @alternations, escape_re_chars($conf, $elt);
197             }
198             }
199              
200 14 100       39 if ($#cclass == 0) {
    100          
201 4         9 push @alternations, $cclass[0];
202             } elsif ($#cclass >= 0) {
203 2         3 $s = '[';
204             @cclass = sort {
205 2 100       7 if ($a eq '[') {
  15 100       42  
    100          
    50          
    100          
    100          
206 2 100       5 if ($b eq ']') {
207 1         1 return 1;
208             } else {
209 1         1 return -1;
210             }
211             } elsif ($b eq '[') {
212 2 50       3 if ($b eq ']') {
213 0         0 return -1;
214             } else {
215 2         3 return 1;
216             }
217             } elsif ($a eq ']') {
218 2         1 return -1;
219             } elsif ($b eq ']') {
220 0         0 return 1;
221             } elsif ($a eq '-') {
222 3         2 return 1;
223             } elsif ($b eq '-') {
224 1         4 return -1;
225             } else {
226 5         5 $a cmp $b;
227             }
228             } @cclass;
229              
230 2         3 my $start = shift @cclass;
231 2         2 my $end;
232 2         6 while (my $c = shift @cclass) {
233 7 100       18 if (defined($end)) {
    100          
234 2 100       3 if (ord($c) - ord($end) == 1) {
235 1         3 $end = $c;
236             } else {
237 1 50       2 if (ord($end) - ord($start) > 1) {
238 1         2 $s .= "$start-$end";
239             } else {
240 0         0 $s .= "$start$end";
241             }
242 1         1 $start = $c;
243 1         5 $end = undef;
244             }
245             } elsif (ord($c) - ord($start) == 1) {
246 1         2 $end = $c;
247             } else {
248 4         4 $s .= $start;
249 4 50       8 $s .= $end if defined $end;
250 4         4 $start = $c;
251 4         9 $end = undef;
252             }
253             }
254              
255 2 50       5 if (defined($start)) {
256 2         1 $s .= $start;
257 2 50       38 if (defined($end)) {
258 0 0       0 if (ord($end) - ord($start) > 1) {
259 0         0 $s .= "-$end";
260             } else {
261 0         0 $s .= $end;
262             }
263             }
264             }
265 2         4 $s .= ']';
266 2         3 push @alternations, $s;
267 2         8 $set = 1;
268             }
269              
270 14 100 100     57 if ($#alternations > 0) {
    100          
271             $s = $conf->{group}[0]
272             . join($conf->{branch},@alternations)
273 5         12 . $conf->{group}[1];
274             } elsif (!$set and length($alternations[0]) > 1) {
275             # Add grouping if the resulting text is not a character set
276             # and is longer than one character
277 5         13 $s = $conf->{group}[0] . $alternations[0] . $conf->{group}[1];
278             } else {
279 4         6 $s = $alternations[0];
280             }
281            
282 14 100       29 $s .= '?' if $opt;
283              
284 14         29 return $s;
285             }
286              
287             # Synopsis:
288             # generic_regexp(CONF, TREE...)
289             # Arguments:
290             # CONF and STRING as described above.
291             # TREE is a list of tree nodes.
292             # Description:
293             # Recursively convert tree into a regular expression.
294             # Return value:
295             # Regular expression string.
296             sub generic_regexp {
297 14     14 0 16 my ($conf, $treeref) = @_;
298 14         12 my @tree = @{$treeref};
  14         27  
299 14         10 my $delim;
300             my $str;
301            
302 14         14 my $mode = shift @tree;
303 14         14 my $type = $mode & T_MASK;
304 14 100       32 if ($type == T_ALT) {
    100          
    50          
305 6         18 $str = nodelist_to_regexp($conf, $mode & T_OPT, @tree);
306             } elsif ($type == T_PFX) {
307             $str = escape_re_chars($conf, shift(@tree))
308 7         8 . nodelist_to_regexp($conf, $mode & T_OPT, @{$tree[0]});
  7         24  
309             } elsif ($type == T_SFX) {
310 1         2 my $sfx = shift(@tree);
311 1         1 $str = nodelist_to_regexp($conf, $mode & T_OPT, @{$tree[0]})
  1         4  
312             . escape_re_chars($conf, $sfx);
313             } else {
314 0         0 croak "unrecognized element type";
315             }
316 14         23 return $str;
317             }
318              
319             # ########################################################
320             # Generate POSIX and Perl-compatible regular expressions.
321             # ########################################################
322              
323             my %transtab = (
324             pcre => {
325             rxchars => '[][\\|.(){}?*+^\$]',
326             group => [ '(?:', ')' ],
327             branch => '|',
328             word => [ '\\b', '\\b' ]
329             },
330             posix => {
331             rxchars => '[][\\<>.|(){}?*+^\$]',
332             group => [ '(', ')' ],
333             branch => '|',
334             word => [ '\\<', '\\>' ]
335             },
336             emacs => {
337             rxchars => '[][.?*+^\$]',
338             group => [ '\\\\(?:', '\\\\)' ],
339             branch => '\\\\|',
340             word => [ '\\\\<', '\\\\>' ]
341             }
342             );
343              
344             =pod
345              
346             =head1 NAME
347              
348             regexp_opt - Convert list of strings to a regular expression
349              
350             =head1 SYNOPSIS
351              
352             use List::Regexp qw(:all);
353              
354             my $s = regexp_opt(@strings);
355              
356             my $s = regexp_opt(\%opts, @strings);
357              
358             =head1 DESCRIPTION
359              
360             Returns a regular expression that will match any string from the input
361             list B<@strings>. First argument can be a reference to a hash, which
362             controls how the regexp is built.
363              
364             Valid keys are:
365              
366             =over 4
367              
368             =item B => B|B|B
369              
370             Controls the flavor of the generated expression: Perl-compatible (the
371             default), POSIX extended, or Emacs.
372              
373             =item B => B|B|B
374            
375             If B, the expression will match any word from B<@strings> appearing
376             as a part of another word.
377              
378             If B, the expression will match a word from B<@strings> appearing
379             on a line alone.
380            
381             If B, the expression will match single words only.
382            
383             =item B => B<0>|B<1>
384              
385             If B<1>, enable debugging output.
386              
387             =item B => B<0>|B<1>
388              
389             If B<1>, enclose entire regexp in a group.
390            
391             =back
392              
393             =head1 LICENSE
394              
395             GPLv3+: GNU GPL version 3 or later, see
396            
397            
398             This is free software: you are free to change and redistribute it.
399             There is NO WARRANTY, to the extent permitted by law.
400            
401             =head1 AUTHORS
402              
403             Sergey Poznyakoff
404            
405             =cut
406             sub regexp_opt {
407 6     6 0 2093 my $trans;
408             my $opts;
409 0         0 my $conf;
410            
411 6 50       31 $opts = shift if (ref($_[0]) eq 'HASH');
412              
413 6 50       19 if (exists($opts->{type})) {
414             croak "unsupported type: $opts->{type}"
415 6 50       22 unless exists $transtab{$opts->{type}};
416 6         15 $trans = $transtab{$opts->{type}};
417             } else {
418 0         0 $trans = $transtab{'pcre'};
419             }
420              
421 6         13 my %h = map { $_, 1 } @_; # Make sure there are no duplicates
  23         43  
422 6         30 my @t = map { my @x = split //, $_; \@x } sort keys %h;
  23         40  
  23         35  
423 6         20 my $tree = parse(@t);
424 6         7 unshift @{$tree}, T_ALT;
  6         10  
425 6 50       18 print Data::Dumper->Dump([$tree], [qw(tree)]) if ($opts->{debug});
426              
427 6         16 my $s = generic_regexp($trans, $tree);
428 6 50       17 if (exists($opts->{match})) {
429 6 100       22 if ($opts->{match} eq 'word') {
    50          
    50          
430 5         16 $s = $trans->{word}[0] . $s . $trans->{word}[1];
431             } elsif ($opts->{match} eq 'exact') {
432 0         0 $s = "^$s\$";
433             } elsif ($opts->{match} ne 'default') {
434 0         0 croak "invalid match value: $opts->{match}";
435             }
436             }
437             $s = $trans->{group}[0] . $s . $trans->{group}[1]
438 6 50       39 if $opts->{group};
439 6         32 return $s;
440             }
441              
442             1;
443              
444              
445