File Coverage

blib/lib/App/Greple/jq.pm
Criterion Covered Total %
statement 62 65 95.3
branch 14 20 70.0
condition 1 3 33.3
subroutine 13 15 86.6
pod 0 7 0.0
total 90 110 81.8


line stmt bran cond sub pod time code
1             =encoding utf-8
2              
3             =head1 NAME
4              
5             greple -Mjq - greple module to search JSON data with jq
6              
7             =head1 SYNOPSIS
8              
9             greple -Mjq --glob JSON-DATA --IN label pattern
10              
11             =head1 VERSION
12              
13             Version 0.06
14              
15             =head1 DESCRIPTION
16              
17             This is an experimental module for L to search JSON
18             formatted text using L as a backend.
19              
20             Search top level json object which includes both C and
21             C somewhere in its text representation.
22              
23             greple -Mjq 'Marvin Zaphod'
24              
25             You can search object C<.commit.author.name> includes C like this:
26              
27             greple -Mjq --IN .commit.author.name Marvin
28              
29             Search first C field including C under C<.commit>:
30              
31             greple -Mjq --IN .commit..name Marvin
32              
33             Search any C field including C:
34              
35             greple -Mjq --IN author.name Marvin
36              
37             Search C is C and C is C or C:
38              
39             greple -Mjq --IN name Marvin --IN type 'Robot|Android'
40              
41             Please be aware that this is just a text matching tool for indented
42             result of L command. So, for example, C<.commit.author>
43             includes everything under it and it matches C field name.
44             Use L filter for more complex and precise operation.
45              
46             =head1 CAUTION
47              
48             L commands read entire input before processing. So it
49             should not be used for gigantic data or infinite stream.
50              
51             =head1 INSTALL
52              
53             =head2 CPANMINUS
54              
55             $ cpanm App::Greple::jq
56              
57             =head1 OPTIONS
58              
59             =over 7
60              
61             =item B<--IN> I
62              
63             Search I included in I
64              
65             Character C<%> can be used as a wildcard in I
66             C<%name> matches labels end with C, and C matches labels
67             start with C.
68              
69             If the label is simple string like C, it matches any level of
70             JSON data.
71              
72             If the label string contains period (C<.>), it is considered as a
73             nested labels. Name C<.name> matches only C label at the top
74             level. Name C matches only C entry of some
75             C hash.
76              
77             If labels are separated by two or more dots (C<..>), they don't have
78             to have direct relationship.
79              
80             =item B<--NOT> I
81              
82             Specify negative condition.
83              
84             =item B<--MUST> I
85              
86             Specify required condition. If there is one or more required
87             condition, all other positive rules move to optional. They are not
88             required but highlighted if exist.
89              
90             =back
91              
92             =head1 LABEL SYNTAX
93              
94             =over 15
95              
96             =item B<.file>
97              
98             C at the top level.
99              
100             =item B<.file.path>
101              
102             C under C<.file>.
103              
104             =item B<.file..path>
105              
106             C in descendants of C<.file>.
107              
108             =item B
109              
110             C at any level.
111              
112             =item B
113              
114             C at any level.
115              
116             =item B
117              
118             Some C in descendants of some C.
119              
120             =item B<%path>
121              
122             Any labels end with C.
123              
124             =item B
125              
126             Any labels start with C.
127              
128             =item B<%path%>
129              
130             Any labels include C.
131              
132             =back
133              
134             =head1 EXAMPLES
135              
136             Search from any C labels.
137              
138             greple -Mjq --IN name _mina
139              
140             Search from C<.process.name> label.
141              
142             greple -Mjq --IN .process.name _mina
143              
144             Object C<.process.name> contains C<_mina> and C<.event> contains
145             C.
146              
147             greple -Mjq --IN .process.name _mina --IN .event EXEC
148              
149             Object C is 803 and C<.event> contains C or C.
150              
151             greple -Mjq --IN ppid 803 --IN event 'FORK|EXEC'
152              
153             Object C is C<_mina> and C<.event> contains C.
154              
155             greple -Mjq --IN name _mina --IN event 'CREATE'
156              
157             Object C contains C<1132> and C<.event> contains C
158             with C highlighted.
159              
160             greple -Mjq --IN ancestors 1132 --IN event EXEC --IN arguments .
161              
162             Object C<*pid> label contains 803.
163              
164             greple -Mjq --IN %pid 803
165              
166             Object any contains C<_mina> under C<.file> and C<.event>
167             contains C.
168              
169             greple -Mjq --IN .file..path _mina --IN .event WRITE
170              
171             =head1 TIPS
172              
173             =over 2
174              
175             =item *
176              
177             Use C<--all> option to show entire data.
178              
179             =item *
180              
181             Use C<--nocolor> option or set C to disable colored
182             output.
183              
184             =item *
185              
186             Use C<-o> option to show only matched part.
187              
188             =item *
189              
190             Use C<--blockend=> option to cancel showing block separator.
191              
192             =item *
193              
194             Since this module implements original search function, L
195             B<-i> does not take effect. Set modifier in regex like C<(?i)pattern>
196             if you want case-insensitive match.
197              
198             =item *
199              
200             Use C<-Mjq::set=debug> to see actual regex.
201              
202             =item *
203              
204             Use C<-Mjq::set=noif> if you don't have to use L as an input
205             filter. Data have to be well-formatted in that case.
206              
207             =item *
208              
209             Use C<--color=always> and set C if you want to
210             see the output using L. Put next line in your F<~/.greplerc>
211             to enable colored output always.
212              
213             option default --color=always
214              
215             =back
216              
217             =head1 SEE ALSO
218              
219             L, L
220              
221             L
222              
223             =head1 AUTHOR
224              
225             Kazumasa Utashiro
226              
227             =head1 LICENSE
228              
229             Copyright ©︎ 2022-2024 Kazumasa Utashiro
230              
231             This library is free software; you can redistribute it and/or modify
232             it under the same terms as Perl itself.
233              
234             =cut
235              
236             package App::Greple::jq;
237              
238 24     24   382881 use 5.014;
  24         104  
239 24     24   191 use strict;
  24         48  
  24         807  
240 24     24   539 use warnings;
  24         1369  
  24         2051  
241 24     24   177 use Carp;
  24         77  
  24         2772  
242              
243             our $VERSION = "0.06";
244              
245 24     24   175 use Exporter 'import';
  24         45  
  24         2073  
246             our @EXPORT = qw(&jq_filter);
247              
248 24     24   755 use App::Greple::Common;
  24         486  
  24         1785  
249 24     24   857 use App::Greple::Regions qw(match_regions merge_regions);
  24         14861  
  24         2242  
250 24     24   165 use Data::Dumper;
  24         44  
  24         26439  
251              
252             my %config;
253 23     23 0 69441 sub set { %config = @_ }
254 0     0 0 0 sub debug { set debug => 1 }
255 0     0 0 0 sub noif { set noif => 1 }
256              
257             my $indent = ' ';
258             my $indent_re = qr/$indent/;
259              
260             sub finalize {
261 23     23 0 471 my($mod, $argv) = @_;
262 23 50       152 if ($config{noif}) {
263 23         139 my @default = $mod->default;
264 23         1382 $mod->setopt(default => grep { $_ ne '--jq-filter' } @default);
  46         194  
265             }
266             }
267              
268             sub re {
269 70     70 0 136 my $pattern = shift;
270 70         128 my $re = eval { qr/$pattern/ };
  70         1254  
271 70 50       219 if ($@) {
272 0         0 die sprintf("$pattern: pattern error - %s\n",
273             $@ =~ /(.*?(?=;|$))/);
274             }
275 70         219 return $re;
276             }
277              
278             sub prefix_regex {
279 23     23 0 91 my $path = shift;
280 23         53 my @prefix_re;
281 23         50 my $level = '';
282 23         227 while ($path =~ s/^([^.\n]*?)(\.+)//) {
283 24         133 my($label, $dot) = ($1, $2);
284 24         70 $label =~ s/%/.*/g;
285 24         103 my $label_re = re($label);
286 24         238 my $start_with = '';
287 24         57 my $prefix_re = do {
288 24 100       104 if ($label eq '') {
289 3 50       109 length($dot) > 1 ? '' : qr{ ^ (?= $indent_re \S) }xm;
290             } else {
291 21 100       100 if (length($dot) == 1) {
292             ## using same capture group name is not a good idea
293             ## so make sure to put just for the last one
294 17 100       56 $level = '?' if $path eq '';
295 17         71 $start_with = qr/(?=\S)/;
296             }
297             qr{
298             ^ (${level} $indent_re*) "$label_re": .* \n
299             (?:
300             ## single line key-value pair
301             \g{-1} $indent_re $start_with .++ \n
302             |
303             ## indented array/hash
304             \g{-1} $indent_re \S .* [\[\{] \n
305             (?: \g{-1} $indent_re \s .*+ \n) *+
306             \g{-1} $indent_re [\]\}] ,? \n
307             ) *?
308 21         1819 }xm;
309             }
310             };
311 24 50       285 push @prefix_re, $prefix_re if $prefix_re;
312             }
313 23 100       89 if ($level eq '') {
314             ## refering named capture group causes error if it is not used
315             ## so put dummy expression just to fail
316 10         41 push @prefix_re, qr/(?(?!))?/;
317             }
318             @prefix_re
319 23         114 }
320              
321             sub IN {
322 23     23 0 3939 my %opt = @_;
323 23 50       348 my $target = delete $opt{&FILELABEL} or die;
324 23         137 my($label, $pattern) = @opt{qw(label pattern)};
325 23   33     451 my @prefix_re = $label =~ s/^((?:.*\.)?)// && prefix_regex($1);
326 23         99 $label =~ s/%/.*/g;
327 23         107 my($label_re, $pattern_re) = map re($_), $label, $pattern;
328 23         2929 my $re = qr{
329             @prefix_re \K
330             ^
331             (?() (?= \g{level} $indent_re \S ) ) # required level
332             (? [ ]*) "$label_re": [ ]*+ # find given label
333             (?: . | \n\g{in} \s++ ) * # and look for ...
334             $pattern_re # pattern
335             (?: . | \n\g{in} (?: \s++ | [\]\}] ) ) * # and take the rest
336             }xm;
337 23 50       190 warn "$re\n" if $config{debug};
338 23         188 match_regions pattern => $re;
339             }
340              
341             1;
342              
343             __DATA__