File Coverage

blib/lib/List/Regexp.pm
Criterion Covered Total %
statement 171 183 93.4
branch 67 86 77.9
condition 8 9 88.8
subroutine 15 15 100.0
pod 0 6 0.0
total 261 299 87.2


line stmt bran cond sub pod time code
1             # -*- perl -*-
2             # Copyright (C) 2015-2021 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 7     7   13023 use strict;
  7         40  
  7         163  
20 7     7   29 use Carp;
  7         11  
  7         608  
21 7     7   3790 use Data::Dumper;
  7         42453  
  7         460  
22 7     7   49 use warnings;
  7         11  
  7         1429  
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.04";
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 54     54 0 65 my $aref = shift;
44              
45 54         63 my ($n, $l);
46              
47 54         73 my $c = $aref->[0][0];
48 54   100     70 for ($n = 0; $n+1 <= $#{$aref} and $aref->[$n+1][0] eq $c; $n++) {};
  74         226  
49            
50             OUTER:
51 54         76 for ($l = 0; $l < $#{$aref->[0]}; $l++) {
  80         132  
52 32         43 $c = $aref->[0][$l+1];
53 32         59 for (my $i = 1; $i <= $n; $i++) {
54 19 100 66     25 last OUTER if ($l+1 > $#{$aref->[$i]} or $aref->[$i][$l+1] ne $c);
  19         96  
55             }
56             }
57 54         93 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 7     7   44 use constant T_ALT => 0x0;
  7         13  
  7         351  
65              
66             # A prefixed list of alternatives. Element 1 is the prefix string and
67             # element 2 is a reference to the list.
68 7     7   35 use constant T_PFX => 0x1;
  7         12  
  7         259  
69             # A suffixed list of alternatives. Element 1 is the suffix string and
70             # element 2 is a reference to the list.
71 7     7   33 use constant T_SFX => 0x2;
  7         12  
  7         266  
72              
73             # This mask is used to get the node type:
74 7     7   35 use constant T_MASK => 0xf;
  7         11  
  7         308  
75              
76             # If the type is ORed with T_OPT, the element is optional.
77 7     7   36 use constant T_OPT => 0x10;
  7         13  
  7         10141  
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 17     17 0 44 my @t = @_;
87 17         23 my @output;
88 17 50       51 return [] if $#t == -1;
89 17         39 while (1) {
90 31         67 my @res = find_prefix \@t;
91 31 100       62 if (!$res[0]) {
    50          
92 23         36 my @rv = map { [ reverse @{$_} ] } @t;
  55         58  
  55         143  
93 23         59 @res = find_prefix \@rv;
94 23 100       42 if ($res[0]) {
95 2         5 my @x = reverse @{$rv[0]}[0..$res[1]];
  2         6  
96 2         4 my $sfxlen = $#x;
97 2         6 my $sfx = join('', @x);
98 2         5 my $type = T_SFX;
99 2         6 my $prefixes = parse(map { my @r = @{$_};
  5         6  
  5         17  
100 5 100       15 if ($sfxlen == $#r) {
101 2         4 $type |= T_OPT;
102 2         16 ();
103             } else {
104 3         29 [ @r[0..$#r-$sfxlen-1] ];
105             }
106             } @t[0..$res[0]]);
107 2         16 push @output, [ $type, $sfx, $prefixes ];
108             } else {
109 21         43 push @output, map { join('', @{$_}) } @t[0..$res[0]];
  21         32  
  21         76  
110             }
111             } elsif ($res[0] == 0) {
112 0         0 push @output, join('', @{$t[0]});
  0         0  
113             } else {
114 8         14 my @x = @{$t[0]}[0..$res[1]];
  8         20  
115 8         10 my $pfxlen = $#x;
116 8         17 my $pfx = join('', @x);
117 8         10 my $type = T_PFX;
118 8         16 my $suffixes = parse(map { my @r = @{$_};
  25         28  
  25         54  
119 25 100       40 if ($pfxlen == $#r) {
120 3         4 $type |= T_OPT;
121 3         3 ();
122             } else {
123 22         122 [ @r[$pfxlen+1..$#r] ];
124             }
125             } @t[0..$res[0]]);
126 8         23 push @output, [ $type, $pfx, $suffixes ];
127             }
128 31 100       68 last if $res[0] == $#t;
129 14         34 @t = @t[($res[0]+1)..$#t];
130             }
131 17         35 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 16     16 0 29 my ($conf,$s) = @_;
171 16         156 $s =~ s/($conf->{rxchars})/\\$1/g;
172 16         43 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 17     17 0 25 my $conf = shift;
184 17         20 my $opt = shift;
185 17         73 my @alternations;
186             my @cclass;
187 17         0 my $s;
188 17         0 my $set;
189            
190 17         32 foreach my $elt (@_) {
191 31 100       89 if (ref($elt) eq 'ARRAY') {
    100          
192 10         32 push @alternations, generic_regexp($conf, $elt);
193             } elsif (length($elt) == 1) {
194 15         29 push @cclass, $elt;
195             } else {
196 6         11 push @alternations, escape_re_chars($conf, $elt);
197             }
198             }
199              
200 17 100       54 if ($#cclass == 0) {
    100          
201 6         14 push @alternations, $cclass[0];
202             } elsif ($#cclass >= 0) {
203 2         4 $s = '[';
204             @cclass = sort {
205 2 100       21 if ($a eq '[') {
  15 100       59  
    100          
    50          
    100          
    100          
206 2 100       4 if ($b eq ']') {
207 1         2 return 1;
208             } else {
209 1         6 return -1;
210             }
211             } elsif ($b eq '[') {
212 2 50       4 if ($b eq ']') {
213 0         0 return -1;
214             } else {
215 2         2 return 1;
216             }
217             } elsif ($a eq ']') {
218 2         2 return -1;
219             } elsif ($b eq ']') {
220 0         0 return 1;
221             } elsif ($a eq '-') {
222 3         5 return 1;
223             } elsif ($b eq '-') {
224 1         1 return -1;
225             } else {
226 5         9 $a cmp $b;
227             }
228             } @cclass;
229              
230 2         4 my $start = shift @cclass;
231 2         4 my $end;
232 2         5 while (my $c = shift @cclass) {
233 7 100       19 if (defined($end)) {
    100          
234 2 100       5 if (ord($c) - ord($end) == 1) {
235 1         2 $end = $c;
236             } else {
237 1 50       3 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         2 $end = undef;
244             }
245             } elsif (ord($c) - ord($start) == 1) {
246 1         2 $end = $c;
247             } else {
248 4         6 $s .= $start;
249 4 50       7 $s .= $end if defined $end;
250 4         5 $start = $c;
251 4         9 $end = undef;
252             }
253             }
254              
255 2 50       4 if (defined($start)) {
256 2         4 $s .= $start;
257 2 50       4 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         9 $set = 1;
268             }
269              
270 17 100 100     91 if ($#alternations > 0) {
    100          
271             $s = $conf->{group}[0]
272             . join($conf->{branch},@alternations)
273 6         32 . $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 6         20 $s = $conf->{group}[0] . $alternations[0] . $conf->{group}[1];
278             } else {
279 5         10 $s = $alternations[0];
280             }
281            
282 17 100       35 $s .= '?' if $opt;
283              
284 17         56 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 17     17 0 40 my ($conf, $treeref) = @_;
298 17         22 my @tree = @{$treeref};
  17         49  
299 17         30 my $delim;
300             my $str;
301            
302 17         21 my $mode = shift @tree;
303 17         48 my $type = $mode & T_MASK;
304 17 100       57 if ($type == T_ALT) {
    100          
    50          
305 7         26 $str = nodelist_to_regexp($conf, $mode & T_OPT, @tree);
306             } elsif ($type == T_PFX) {
307             $str = escape_re_chars($conf, shift(@tree))
308 8         28 . nodelist_to_regexp($conf, $mode & T_OPT, @{$tree[0]});
  8         59  
309             } elsif ($type == T_SFX) {
310 2         6 my $sfx = shift(@tree);
311 2         4 $str = nodelist_to_regexp($conf, $mode & T_OPT, @{$tree[0]})
  2         15  
312             . escape_re_chars($conf, $sfx);
313             } else {
314 0         0 croak "unrecognized element type";
315             }
316 17         43 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 7     7 0 2880 my $trans;
408             my $opts;
409 7         0 my $conf;
410            
411 7 50       103 $opts = shift if (ref($_[0]) eq 'HASH');
412              
413 7 50       49 if (exists($opts->{type})) {
414             croak "unsupported type: $opts->{type}"
415 7 50       29 unless exists $transtab{$opts->{type}};
416 7         21 $trans = $transtab{$opts->{type}};
417             } else {
418 0         0 $trans = $transtab{'pcre'};
419             }
420              
421 7         19 my %h = map { $_, 1 } @_; # Make sure there are no duplicates
  26         60  
422 7         43 my @t = map { my @x = split //, $_; \@x } sort keys %h;
  26         58  
  26         45  
423 7         38 my $tree = parse(@t);
424 7         9 unshift @{$tree}, T_ALT;
  7         17  
425 7 50       23 print Data::Dumper->Dump([$tree], [qw(tree)]) if ($opts->{debug});
426              
427 7         22 my $s = generic_regexp($trans, $tree);
428 7 50       21 if (exists($opts->{match})) {
429 7 100       32 if ($opts->{match} eq 'word') {
    50          
    50          
430 6         23 $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 7 50       25 if $opts->{group};
439 7         51 return $s;
440             }
441              
442             1;
443              
444              
445