File Coverage

blib/lib/ETL/Yertl/Command/yq/Regex.pm
Criterion Covered Total %
statement 134 149 89.9
branch 103 118 87.2
condition 32 45 71.1
subroutine 10 10 100.0
pod 0 3 0.0
total 279 325 85.8


line stmt bran cond sub pod time code
1             package ETL::Yertl::Command::yq::Regex;
2             our $VERSION = '0.036';
3             # ABSTRACT: A regex-based parser for programs
4              
5 8     8   2805 use ETL::Yertl;
  8         15  
  8         36  
6 8     8   239 use boolean qw( :all );
  8         14  
  8         36  
7 8     8   3066 use Regexp::Common;
  8         13740  
  8         32  
8 8     8   954415 use Time::Local qw( timegm );
  8         12579  
  8         524  
9 8     8   79 use ETL::Yertl::Util qw( firstidx );
  8         32  
  8         12178  
10              
11             sub empty() {
12 5     5 0 55 bless {}, 'empty';
13             }
14              
15             sub is_empty($) {
16 158     158 0 433 return ref $_[0] eq 'empty';
17             }
18              
19             *diag = *yertl::diag;
20              
21             my $QUOTE_STRING = $RE{delimited}{-delim=>q{'"}};
22             my $EVAL_NUMS = qr{(?:0b$RE{num}{bin}|0$RE{num}{oct}|0x$RE{num}{hex})};
23              
24             # Match a document path
25             our $GRAMMAR = qr{
26             (?(DEFINE)
27             (?
28             (?:\$?[.](?:\w+|\[\d*\]))+ # hash/array lookup
29             |
30             \$?[.] # entire document
31             |
32             $QUOTE_STRING
33             |
34             $RE{num}{real}|$EVAL_NUMS
35             |
36             \w+ # Constant/bareword
37             )
38             (?eq|ne|==?|!=|>=?|<=?)
39             (?empty|select|grep|group_by|keys|length|sort|each|parse_time)
40             (?
41             \{\s*(?&FILTER)\s*:\s*(?0)\s*(?:,(?-1))*\} # Hash constructor
42             |
43             \[\s*(?0)\s*(?:,(?-1))*\] # Array constructor
44             |
45             (?&FUNC_NAME)(?:\(\s*(?&EXPR)\s*(?:,\s*(?&EXPR)\s*)*\))? # Function with optional argument(s)
46             |
47             (?:(?&FILTER)|(?&FUNC_NAME)(?:\(\s*(?&EXPR)\s*\))?)\s+(?&OP)\s+(?&EXPR) # Binop with filter
48             |
49             (?&FILTER)
50             )
51             )
52             }x;
53              
54             my $FILTER = qr{(?&FILTER)$GRAMMAR};
55             my $OP = qr{(?&OP)$GRAMMAR};
56             my $FUNC_NAME = qr{(?&FUNC_NAME)$GRAMMAR};
57             my $EXPR = qr{(?&EXPR)$GRAMMAR};
58             my $PIPE = qr{[|]};
59              
60             my @DAYS = qw< sun sunday mon monday tue tuesday wed wednesday thu thursday fri friday sat saturday sun sunday>;
61             my $DAYS = qr{@{[ join '|', @DAYS ]}}i;
62             my @MONTHS = qw< jan feb mar apr may jun jul aug sep oct nov dec >;
63             my $MONTHS = qr{@{[ join '|', @MONTHS ]}}i;
64              
65             my %PARSE_TIME = (
66             iso => qr{(?\d{4})-?(?\d{2})-?(?\d{2})(?:[ T]?(?\d{2}):?(?\d{2})(?::?(?\d{2})))?},
67             apache => qr{(?\d{2})/(?$MONTHS)/(?\d{4}):(?\d{2}):(?\d{2}):(?\d{2})},
68             );
69             $PARSE_TIME{auto} = qr{$PARSE_TIME{iso}|$PARSE_TIME{apache}};
70              
71             # Filter MUST NOT mutate $doc!
72             sub filter {
73 384     384 0 313958 my ( $class, $filter, $doc, $scope, $orig_doc ) = @_;
74 384   66     1069 $orig_doc ||= $doc;
75              
76             # Pipes: LEFT | RIGHT pipes the output of LEFT to the input of RIGHT
77 384 100       15242 if ( $filter =~ $PIPE ) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
78 11         433 my @exprs = split /\s*$PIPE\s*/, $filter;
79 11         34 my @in = ( $doc );
80 11         26 for my $expr ( @exprs ) {
81 22         32 my @out = ();
82 22         36 for my $doc ( @in ) {
83 27         118 push @out, $class->filter( $expr, $doc, $scope, $orig_doc );
84             }
85 22         145 @in = @out;
86             }
87 11         37 return @in;
88             }
89              
90             # Hash constructor
91             elsif ( $filter =~ /^{/ ) {
92 6         12 my %out;
93 6         40 my ( $inner ) = $filter =~ /^\{\s*([^\}]+?)\s*\}$/;
94 6         22 for my $pair ( split /\s*,\s*/, $inner ) {
95 8         97 my ( $key_filter, $value_expr ) = split /\s*:\s*/, $pair;
96 8         32 my $key = $class->filter( $key_filter, $doc, $scope, $orig_doc );
97 8         28 $out{ $key } = $class->filter( $value_expr, $doc, $scope, $orig_doc );
98             }
99 6         20 return \%out;
100             }
101              
102             # Array constructor
103             elsif ( $filter =~ /^\[/ ) {
104 1         2 my @out;
105 1         8 my ( $inner ) = $filter =~ /^\[\s*([^\]]+?)\s*\]$/;
106 1         8 for my $value_expr ( split /\s*,\s*/, $inner ) {
107 5         12 push @out, $class->filter( $value_expr, $doc, $scope, $orig_doc );
108             }
109 1         5 return \@out;
110             }
111              
112             # Function calls
113             elsif ( my ( $func, @args ) = $filter =~ /^((?&FUNC_NAME))(?:\(\s*((?&EXPR))\s*(?:,\s*((?&EXPR))\s*)*\))?$GRAMMAR$/ ) {
114 62   100     530 diag( 1, "F: $func, ARGS: " . ( join( ', ', grep defined, @args ) || '' ) );
115 62 100 100     475 if ( $func eq 'empty' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
116 1 50       4 if ( @args ) {
117 1         31 warn "empty does not take arguments\n";
118             }
119 1         24 return empty;
120             }
121             elsif ( $func eq 'select' || $func eq 'grep' ) {
122 8 50       22 if ( !@args ) {
123 0         0 warn "'$func' takes an expression argument";
124 0         0 return empty;
125             }
126 8 100       53 return $class->filter( $args[0], $doc, $scope, $orig_doc ) ? $doc : empty;
127             }
128             elsif ( $func eq 'group_by' ) {
129 6         19 my $grouping = $class->filter( $args[0], $doc, $scope, $orig_doc );
130 6         27 push @{ $scope->{ group_by }{ $grouping } }, $doc;
  6         20  
131 6         20 return;
132             }
133             elsif ( $func eq 'sort' ) {
134 3   50     8 $args[0] ||= '.';
135 3         10 my $value = $class->filter( $args[0], $doc, $scope, $orig_doc );
136 3         5 push @{ $scope->{sort} }, [ "$value", $doc ];
  3         11  
137 3         10 return;
138             }
139             elsif ( $func eq 'keys' ) {
140 5   100     19 $args[0] ||= '.';
141 5         21 my $value = $class->filter( $args[0], $doc, $scope, $orig_doc );
142 5 100       23 if ( ref $value eq 'HASH' ) {
    50          
143 4         30 return [ keys %$value ];
144             }
145             elsif ( ref $value eq 'ARRAY' ) {
146 1         3 return [ 0..$#{ $value } ];
  1         4  
147             }
148             else {
149 0         0 warn "keys() requires a hash or array";
150 0         0 return empty;
151             }
152             }
153             elsif ( $func eq 'each' ) {
154 7   100     25 $args[0] ||= '.';
155 7         27 my $value = $class->filter( $args[0], $doc, $scope, $orig_doc );
156 7 100       24 if ( ref $value eq 'HASH' ) {
    50          
157 6         60 return map +{ key => $_, value => $value->{ $_ } }, keys %$value;
158             }
159             elsif ( ref $value eq 'ARRAY' ) {
160 1         11 return map +{ key => $_, value => $value->[ $_ ] }, 0..$#$value;
161             }
162             else {
163 0         0 warn "each() requires a hash or array";
164 0         0 return empty;
165             }
166             }
167             elsif ( $func eq 'length' ) {
168 13   100     31 $args[0] ||= '.';
169 13         57 my $value = $class->filter( $args[0], $doc, $scope, $orig_doc );
170 13 100       46 if ( ref $value eq 'HASH' ) {
    100          
    50          
171 3         15 return scalar keys %$value;
172             }
173             elsif ( ref $value eq 'ARRAY' ) {
174 7         17 return scalar @$value;
175             }
176             elsif ( !ref $value ) {
177 3         12 return length $value;
178             }
179             else {
180 0         0 warn "length() requires a hash, array, string, or number";
181 0         0 return empty;
182             }
183             }
184             elsif ( $func eq 'parse_time' ) {
185 19         47 my ( $expr, $format ) = @args;
186 19   100     64 $format ||= 'auto';
187             die sprintf "Invalid format '%s' in parse_time()\n", $format
188 19 50       58 if !$PARSE_TIME{ $format};
189 19         61 my $value = $class->filter( $expr, $doc, $scope, $orig_doc );
190 19         88 diag( 1, "FMT: $PARSE_TIME{ $format }, VAL: $value" );
191 19 50       199 if ( $value =~ $PARSE_TIME{ $format } ) {
192 7     7   2066 my @tlargs = @{+}{qw< s n h d m y >};
  7         2466  
  7         9832  
  19         275  
193 19 100 66     128 if ( !$+{m} && ( my $mname = $+{mn} ) ) {
194 7     33   41 $tlargs[4] = firstidx { /$mname/i } @MONTHS;
  33         230  
195             }
196             else {
197 12         31 $tlargs[4] -= 1;
198             }
199 19         86 return timegm( @tlargs );
200             }
201 0         0 warn sprintf "time '%s' does not match format '%s'\n", $value, $format;
202 0         0 return empty;
203             }
204             }
205              
206             # Hash and array keys to traverse the data structure
207             elsif ( $filter =~ /^((?&FILTER))$GRAMMAR$/ ) {
208             # Extract quoted strings
209 213 100       2088 if ( $filter =~ /^(['"])(.+)(\1)$/ ) {
    100          
    100          
210 9         67 return $2;
211             }
212             # Eval numbers to allow bin, hex, and oct
213             elsif ( $filter =~ /^$EVAL_NUMS$/ ) {
214             ## no critic ( ProhibitStringyEval )
215 3         204 return eval $filter;
216             }
217             # Constants/barewords do not begin with .
218             elsif ( $filter !~ /^[\$.]/ ) {
219             # If it's not a reserved word, it's a string
220             # XXX: This is a very poor decision...
221 43         134 return $filter;
222             }
223              
224 158 100       378 if ( is_empty $doc ) {
225 2         7 return empty;
226             }
227              
228 156         455 my @keys = split /[.]/, $filter;
229 156 100 66     400 my $subdoc = $keys[0] && $keys[0] eq '$' ? $orig_doc : $doc;
230 156         421 for my $key ( @keys[1..$#keys] ) {
231 154 100       621 if ( $key =~ /^\[\]$/ ) {
    100          
    50          
232 1         2 return @{ $subdoc };
  1         6  
233             }
234             elsif ( $key =~ /^\[(\d+)\]$/ ) {
235 4         14 $subdoc = $subdoc->[ $1 ];
236             }
237             elsif ( $key =~ /^\w+$/ ) {
238 149         347 $subdoc = $subdoc->{ $key };
239             }
240             else {
241 0         0 die "Invalid filter key '$key'";
242             }
243             }
244 155         552 return $subdoc;
245             }
246              
247             # Binary operators (binops)
248             elsif ( $filter =~ /^((?&FILTER)|(?&FUNC_NAME)(?:\(\s*(?&EXPR)\s*\))?)\s+((?&OP))\s+((?&EXPR))$GRAMMAR$/ ) {
249 87         368 my ( $lhs_filter, $cond, $rhs_filter ) = ( $1, $2, $3 );
250 87 100       184 if ( $cond eq '=' ) {
251             # Get the referent from the left-hand side
252 29         87 my @keys = split /[.]/, $lhs_filter;
253 29 50 33     94 my $subdoc = $keys[0] && $keys[0] eq '$' ? \$orig_doc : \$doc;
254 29         84 for my $key ( @keys[1..$#keys] ) {
255 29 50       138 if ( $key =~ /^\[(\d+)\]$/ ) {
    50          
256 0         0 $subdoc = \( $$subdoc->[ $1 ] );
257             }
258             elsif ( $key =~ /^\w+$/ ) {
259 29         84 $subdoc = \( $$subdoc->{ $key } );
260             }
261             else {
262 0         0 die "Invalid filter key '$key'";
263             }
264             }
265              
266 29         118 my $rhs_value = $class->filter( $rhs_filter, $doc, $scope, $orig_doc );
267 29   50     791 diag( 1, join " ", "BINOP:", $lhs_filter, $cond, $rhs_value // '' );
268 29         53 $$subdoc = $rhs_value;
269 29         129 return $doc; # Assignment does not change current document
270             }
271             else {
272 58         192 my $lhs_value = $class->filter( $lhs_filter, $doc, $scope, $orig_doc );
273 58         133 my $rhs_value = $class->filter( $rhs_filter, $doc, $scope, $orig_doc );
274 58   50     367 diag( 1, join " ", "BINOP:", $lhs_value // '', $cond, $rhs_value // '' );
      50        
275             # These operators suppress undef warnings, treating undef as just
276             # another value. Undef will never be treated as '' or 0 here.
277 58 100       241 if ( $cond eq 'eq' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
278 9 100 66     75 return defined $lhs_value == defined $rhs_value
279             && $lhs_value eq $rhs_value ? true : false;
280             }
281             elsif ( $cond eq 'ne' ) {
282 4 100 66     21 return defined $lhs_value != defined $rhs_value
283             || $lhs_value ne $rhs_value ? true : false;
284             }
285             elsif ( $cond eq '==' ) {
286 14 100 66     86 return defined $lhs_value == defined $rhs_value
287             && $lhs_value == $rhs_value ? true : false;
288             }
289             elsif ( $cond eq '!=' ) {
290 4 100 66     22 return defined $lhs_value != defined $rhs_value
291             || $lhs_value != $rhs_value ? true : false;
292             }
293             # These operators allow undef warnings, since equating undef to 0 or ''
294             # can be a cause of problems.
295             elsif ( $cond eq '>' ) {
296 6 100       20 return $lhs_value > $rhs_value ? true : false;
297             }
298             elsif ( $cond eq '>=' ) {
299 9 100       35 return $lhs_value >= $rhs_value ? true : false;
300             }
301             elsif ( $cond eq '<' ) {
302 6 100       23 return $lhs_value < $rhs_value ? true : false;
303             }
304             elsif ( $cond eq '<=' ) {
305 6 100       22 return $lhs_value <= $rhs_value ? true : false;
306             }
307             }
308             }
309              
310             # Conditional (if/then/else)
311             # NOTE: If we're capturing using $EXPR, then we _must_ use named captures,
312             # because $EXPR has captures in itself
313             elsif ( $filter =~ /^if\s+(?$EXPR)\s+then\s+(?$FILTER)(?:\s+else\s+(?$FILTER))?$/ ) {
314 3         34 my ( $expr, $true_filter, $false_filter ) = @+{qw( expr true false )};
315 3         22 my $expr_value = $class->filter( $expr, $doc, $scope, $orig_doc );
316 3 100       44 if ( $expr_value ) {
317 2         16 return $class->filter( $true_filter, $doc, $scope, $orig_doc );
318             }
319             else {
320 1 50       10 return $false_filter ? $class->filter( $false_filter, $doc, $scope, $orig_doc ) : ();
321             }
322             }
323              
324             # , does multiple filters, yielding multiple documents
325             # This must be the least-specific rule because of all the other
326             # possible uses of the comma
327             # XXX: In the future, this should be used to parse function
328             # arguments to allow for recursion
329             elsif ( $filter =~ /,/ ) {
330 1         9 my @filters = split /\s*,\s*/, $filter;
331 1         3 return map { $class->filter( $_, $doc, $scope, $orig_doc ) } @filters;
  3         21  
332             }
333              
334             else {
335 0           die "Could not parse filter '$filter'\n";
336             }
337 0           return;
338             }
339              
340             1;
341              
342             __END__