File Coverage

bin/jq-lite
Criterion Covered Total %
statement 366 501 73.0
branch 183 290 63.1
condition 75 142 52.8
subroutine 31 36 86.1
pod n/a
total 655 969 67.6


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2              
3 63     63   359031 use strict;
  63         138  
  63         2624  
4 63     63   304 use warnings;
  63         94  
  63         3775  
5 63     63   56950 use JSON::PP;
  63         1552452  
  63         6636  
6 63     63   48327 use IO::File;
  63         672081  
  63         8751  
7 63     63   37532 use FindBin;
  63         96957  
  63         3846  
8 63     63   46379 use Term::ANSIColor;
  63         851774  
  63         7142  
9 63     63   54916 use Getopt::Long qw(GetOptions :config no_ignore_case bundling no_pass_through);
  63         935621  
  63         364  
10 63     63   53136 use lib "$FindBin::Bin/../lib";
  63         48389  
  63         528  
11 63     63   48867 use JQ::Lite;
  63         310  
  63         2887  
12 63     63   416 use JQ::Lite::Util ();
  63         151  
  63         184561  
13              
14 63         3844837 my $decoder;
15             my $decoder_module;
16 63         174 my $decoder_debug = 0;
17 63         128 my $decoder_choice;
18 63         132 my $raw_input = 0;
19 63         166 my $raw_output = 0;
20 63         139 my $compact_output = 0;
21 63         188 my $ascii_output = 0;
22 63         134 my $color_output = 0;
23 63         130 my $null_input = 0;
24 63         127 my $slurp_input = 0;
25 63         123 my @query_files;
26 63         121 my $force_yaml = 0;
27 63         125 my $exit_status = 0;
28 63         355 my $yaml_module;
29             my $yaml_loader;
30 63         0 my %arg_vars;
31 63         0 my @slurpfiles;
32 63         147 my $slurpfile_probably_missing_file = 0;
33              
34             sub _usage_error {
35 16     16   567 my ($message) = @_;
36 16   50     64 $message ||= 'invalid usage';
37 16         122 $message =~ s/\s+\z//;
38 16         323 warn "[USAGE]$message\n";
39 16         2182 exit 5;
40             }
41              
42             sub _input_error {
43 2     2   378 my ($message) = @_;
44 2   50     8 $message ||= 'input error';
45 2         18 $message =~ s/\s+\z//;
46 2         77 warn "[INPUT]$message\n";
47 2         176 exit 4;
48             }
49              
50             sub _compile_error {
51 3     3   10 my ($message) = @_;
52 3   50     10 $message ||= 'compile error';
53 3         19 $message =~ s/\s+\z//;
54 3         120 warn "[COMPILE]$message\n";
55 3         270 exit 2;
56             }
57              
58             sub _runtime_error {
59 3     3   10 my ($message) = @_;
60 3   50     9 $message ||= 'runtime error';
61 3         16 $message =~ s/\s+\z//;
62 3         69 warn "[RUNTIME]$message\n";
63 3         288 exit 3;
64             }
65              
66             sub _validate_query_syntax {
67 46     46   159 my ($query) = @_;
68              
69 46 50 33     399 return if !defined $query || $query eq '';
70              
71 46         129 my @stack;
72             my $string;
73 46         102 my $escape = 0;
74              
75 46         275 for my $char (split //, $query) {
76 352 100       630 if (defined $string) {
77 8 100       15 if ($escape) {
78 2         4 $escape = 0;
79 2         3 next;
80             }
81              
82 6 100       14 if ($char eq '\\') {
83 2         5 $escape = 1;
84 2         4 next;
85             }
86              
87 4 100       11 if ($char eq $string) {
88 3         7 undef $string;
89             }
90 4         5 next;
91             }
92              
93 344 100 66     1090 if ($char eq "'" || $char eq '"') {
94 3         5 $string = $char;
95 3         5 next;
96             }
97              
98 341 100 100     1457 if ($char eq '(' || $char eq '[' || $char eq '{') {
      66        
99 19         69 push @stack, $char;
100 19         49 next;
101             }
102              
103 322 100 100     1423 if ($char eq ')' || $char eq ']' || $char eq '}') {
      66        
104 18         43 my $open = pop @stack;
105 18         98 my %pairs = (
106             ')' => '(',
107             ']' => '[',
108             '}' => '{',
109             );
110 18 50 33     184 if (!defined $open || $open ne $pairs{$char}) {
111 0         0 _compile_error('Invalid query syntax: unmatched brackets');
112             }
113             }
114             }
115              
116 46 100 66     324 if (defined $string || @stack) {
117 1         5 _compile_error('Invalid query syntax: unmatched brackets');
118             }
119              
120 45         348 my @pipeline_parts = JQ::Lite::Util::_split_top_level_pipes($query);
121 45 100 66     193 if (grep { !defined $_ || $_ !~ /\S/ } @pipeline_parts) {
  57         705  
122 1         5 _compile_error('Invalid query syntax: empty filter segment');
123             }
124              
125 44         301 for my $segment (@pipeline_parts) {
126 55         293 my @comma_parts = JQ::Lite::Util::_split_top_level_commas($segment);
127 55 100 66     153 if (grep { !defined $_ || $_ !~ /\S/ } @comma_parts) {
  56         669  
128 1         4 _compile_error('Invalid query syntax: empty filter segment');
129             }
130             }
131             }
132              
133             sub _is_truthy {
134 4     4   51 my ($value) = @_;
135              
136 4 100       25 return 0 unless defined $value;
137              
138 2 100       9 if (ref($value) eq 'JSON::PP::Boolean') {
139 1 50       33 return $value ? 1 : 0;
140             }
141              
142 1         4 return 1;
143             }
144              
145             # ---------- Help text ----------
146 63         189 my $USAGE = <<'USAGE';
147             jq-lite - minimal jq-style JSON filter (pure Perl)
148              
149             Usage:
150             jq-lite [options] '.query' [file.json]
151             jq-lite [options] -f query.jq [file.json]
152              
153             Options:
154             -R, --raw-input Read input as raw text instead of JSON (one filter run per line)
155             -r, --raw-output Print raw strings instead of JSON-encoded values
156             -c, --compact-output Print JSON results on a single line (no pretty-printing)
157             -a, --ascii-output Escape all non-ASCII characters in JSON output
158             --color Colorize JSON output (keys, strings, numbers, booleans)
159             --use Force JSON decoder module (e.g. JSON::PP, JSON::XS, Cpanel::JSON::XS)
160             --debug Show which JSON module is being used
161             -e, --exit-status Set exit code to 1 when the final result is false, null, or empty
162             --arg NAME VALUE Set jq-style variable $NAME to the string VALUE
163             --rawfile NAME FILE Set jq-style variable $NAME to the raw contents of FILE
164             --slurpfile NAME FILE Set jq-style variable $NAME to an array of JSON values from FILE
165             --argjson NAME JSON Set jq-style variable $NAME to a JSON-decoded value
166             --argfile NAME FILE Set jq-style variable $NAME to a JSON-decoded value from FILE
167             -f, --from-file FILE Read jq filter from FILE instead of the command line
168             (use '-' to read the filter from STDIN)
169             -n, --null-input Use null as input instead of reading JSON data
170             -s, --slurp Read entire input stream as an array of JSON values
171             --yaml Parse input as YAML (auto-detected for .yml/.yaml files)
172             -h, --help Show this help message
173             --help-functions Show list of all supported functions
174             -v, --version Show version information
175              
176             Examples:
177             cat users.json | jq-lite '.users[].name'
178             jq-lite '.users[] | select(.age > 25)' users.json
179             jq-lite -r '.users[] | .name' users.json
180             jq-lite '.meta has "version"' config.json
181             jq-lite --color '.items | sort | reverse | first' data.json
182              
183             Homepage:
184             https://metacpan.org/pod/JQ::Lite
185             USAGE
186              
187             # ---------- Option parsing (Getopt::Long) ----------
188 63         172 my ($want_help, $want_version, $help_functions) = (0, 0, 0);
189              
190 63         167 my @getopt_errors;
191             my $getopt_ok;
192             {
193 63         163 my $orig_warn = $SIG{__WARN__};
  63         202  
194             local $SIG{__WARN__} = sub {
195 10     10   2367 my ($msg) = @_;
196 10 100       75 if ($msg =~ /^\[(?:COMPILE|RUNTIME|INPUT|USAGE)\]/) {
197 9 50       34 if ($orig_warn) {
198 0         0 $orig_warn->($msg);
199             }
200             else {
201 9         200 CORE::warn($msg);
202             }
203 9         47 return;
204             }
205              
206 1         6 push @getopt_errors, $msg;
207 63         709 };
208              
209             $getopt_ok = GetOptions(
210             'raw-input|R' => \$raw_input,
211             'raw-output|r' => \$raw_output,
212             'compact-output|c'=> \$compact_output,
213             'ascii-output|a' => \$ascii_output,
214             'color' => \$color_output,
215             'use=s' => \$decoder_choice,
216             'debug' => \$decoder_debug,
217             'exit-status|e' => \$exit_status,
218             'null-input|n' => \$null_input,
219             'slurp|s' => \$slurp_input,
220             'yaml' => \$force_yaml,
221             'help|h' => \$want_help,
222             'help-functions' => \$help_functions,
223             'version|v' => \$want_version,
224             'from-file|f=s' => \@query_files,
225             'slurpfile=s' => sub {
226 5     5   13309 my ($opt_name, $var_name) = @_;
227 5 50 33     53 _usage_error('--slurpfile requires a variable name') if !defined $var_name || $var_name eq '';
228 5 50       38 if ($var_name !~ /^[A-Za-z_]\w*$/) {
229 0         0 _usage_error("Invalid variable name '$var_name' for --slurpfile");
230             }
231 5 50       18 _usage_error('--slurpfile requires a file path') if !@ARGV;
232 5         15 my $file_path = shift @ARGV;
233 5 50       40 $file_path = '' unless defined $file_path;
234              
235 5 100 66     29 $slurpfile_probably_missing_file = 1 if @ARGV == 0 && $file_path =~ /^\./;
236              
237 5         66 push @slurpfiles, $var_name, $file_path;
238             },
239             'arg=s' => sub {
240 3     3   7720 my ($opt_name, $var_name) = @_;
241 3 50 33     31 _usage_error('--arg requires a variable name') if !defined $var_name || $var_name eq '';
242 3 100       24 if ($var_name !~ /^[A-Za-z_]\w*$/) {
243 1         6 _usage_error("Invalid variable name '$var_name' for --arg");
244             }
245 2 100       12 _usage_error('--arg requires a value') if !@ARGV;
246 1         3 my $value = shift @ARGV;
247 1 50       5 $value = '' unless defined $value;
248 1         15 $arg_vars{$var_name} = "$value";
249             },
250             'argjson=s' => sub {
251 7     7   15055 my ($opt_name, $var_name) = @_;
252 7 50 33     64 _usage_error('--argjson requires a variable name') if !defined $var_name || $var_name eq '';
253 7 100       48 if ($var_name !~ /^[A-Za-z_]\w*$/) {
254 1         8 _usage_error("Invalid variable name '$var_name' for --argjson");
255             }
256 6 100       25 _usage_error('--argjson requires a value') if !@ARGV;
257 5         9 my $value_text = shift @ARGV;
258 5 50       17 $value_text = '' unless defined $value_text;
259              
260 5         10 my $decoded = eval { JQ::Lite::Util::_decode_json($value_text) };
  5         29  
261 5 100       1836 if (my $err = $@) {
262 2         19 $err =~ s/\s+\z//;
263 2         12 _usage_error("invalid JSON for --argjson $var_name: $err");
264             }
265              
266 3         40 $arg_vars{$var_name} = $decoded;
267             },
268             'argfile=s' => sub {
269 3     3   6269 my ($opt_name, $var_name) = @_;
270 3 50 33     27 _usage_error('--argfile requires a variable name') if !defined $var_name || $var_name eq '';
271 3 50       21 if ($var_name !~ /^[A-Za-z_]\w*$/) {
272 0         0 _usage_error("Invalid variable name '$var_name' for --argfile");
273             }
274 3 50       10 _usage_error('--argfile requires a file path') if !@ARGV;
275 3         8 my $file_path = shift @ARGV;
276 3 50       8 $file_path = '' unless defined $file_path;
277              
278 3 100       30 my $fh = IO::File->new($file_path, 'r')
279             or _usage_error("Cannot open file '$file_path' for --argfile $var_name: $!");
280 2         305 local $/;
281 2         89 my $json_text = <$fh>;
282 2 50       13 $json_text = '' unless defined $json_text;
283 2         21 $fh->close;
284              
285 2         37 my $decoded = eval { JQ::Lite::Util::_decode_json($json_text) };
  2         11  
286 2 100       936 if (my $err = $@) {
287 1         7 $err =~ s/\s+\z//;
288 1         4 _usage_error("invalid JSON in --argfile $var_name: $err");
289             }
290              
291 1         23 $arg_vars{$var_name} = $decoded;
292             },
293             'rawfile=s' => sub {
294 2     2   3871 my ($opt_name, $var_name) = @_;
295 2 50 33     20 _usage_error('--rawfile requires a variable name') if !defined $var_name || $var_name eq '';
296 2 50       11 if ($var_name !~ /^[A-Za-z_]\w*$/) {
297 0         0 _usage_error("Invalid variable name '$var_name' for --rawfile");
298             }
299 2 50       7 _usage_error('--rawfile requires a file path') if !@ARGV;
300 2         4 my $file_path = shift @ARGV;
301 2 50       23 $file_path = '' unless defined $file_path;
302              
303             open my $fh, '<', $file_path
304 2 100       206 or do {
305 1         19 _usage_error("Cannot open file '$file_path' for --rawfile $var_name: $!");
306             };
307 1         14 local $/;
308 1         41 my $content = <$fh>;
309 1         16 close $fh;
310 1 50       5 $content = '' unless defined $content;
311              
312 1         22 $arg_vars{$var_name} = $content;
313             },
314 63         2781 );
315             }
316              
317 54 100       107747 if (!$getopt_ok) {
318 1 50       5 my $message = @getopt_errors ? $getopt_errors[0] : 'invalid option(s)';
319 1         9 $message =~ s/\s+\z//;
320 1         5 _usage_error($message);
321             }
322              
323 53 50       219 if ($help_functions) {
324 0         0 print_supported_functions();
325 0         0 exit 0;
326             }
327              
328 53 50       209 if ($want_help) {
329 0         0 print $USAGE;
330 0         0 exit 0;
331             }
332              
333 53 50       188 if ($want_version) {
334 0         0 print "jq-lite $JQ::Lite::VERSION\n";
335 0         0 exit 0;
336             }
337              
338 53     1   966 $SIG{PIPE} = sub { exit 0 };
  1         1272  
339              
340             END {
341 63 50   63   455 return unless defined fileno(STDOUT);
342              
343 63 100       31 if (!close STDOUT) {
344             # On older perls a closed pipeline can report EINVAL instead of
345             # EPIPE. Both conditions indicate the writer has no consumer, so do
346             # not emit a warning in either case.
347 63 50 33 63   35992 return if $!{EPIPE} || $!{EINVAL};
  63         115140  
  63         735  
  1         16  
348 0         0 warn "Unable to flush stdout: $!";
349             }
350             }
351              
352             # ---------- Positional args: '.query' and [file.json] ----------
353             # Unknown options are already rejected above; only query and file remain.
354 53         212 my ($query, $filename);
355              
356 53   100     1679 my $non_help_option_used = $raw_input
357             || $raw_output
358             || $compact_output
359             || $ascii_output
360             || $color_output
361             || defined $decoder_choice
362             || $decoder_debug
363             || $exit_status
364             || $null_input
365             || $slurp_input
366             || $force_yaml
367             || @query_files
368             || keys(%arg_vars)
369             || @slurpfiles;
370              
371 53 100       233 if (@query_files) {
372 3 50       11 _usage_error('--from-file may only be specified once') if @query_files > 1;
373              
374 3         8 my $file = $query_files[0];
375 3 100 66     20 if (defined $file && $file eq '-') {
376 2 50 66     13 if (!$null_input && !@ARGV) {
377 1         5 _usage_error('Cannot use --from-file - when reading JSON from STDIN. Provide input file or use --null-input.');
378             }
379             }
380              
381 2 100 66     11 if (defined $file && $file eq '-') {
382 1         4 local $/;
383 1         30 $query = ;
384 1 50       5 $query = '' unless defined $query;
385             }
386             else {
387 1 50       12 my $fh = IO::File->new($file, 'r')
388             or _input_error("Cannot open query file '$file': $!");
389 1         172 local $/;
390 1   50     36 $query = <$fh> // '';
391 1         12 $fh->close;
392             }
393             }
394              
395 52 100       384 if (@ARGV == 0) {
    100          
    50          
396 4 100       14 if (!defined $query) {
397 3 100       11 if ($slurpfile_probably_missing_file) {
398 1         6 _usage_error('--slurpfile requires a file path');
399             }
400              
401 2 50       15 if ($raw_input) {
402 2 100       10 my $reason = $slurp_input
403             ? '--raw-input requires a query when used with --slurp.'
404             : '--raw-input requires a query when not using --slurp.';
405 2         11 _usage_error($reason);
406             }
407              
408 0 0       0 if ($null_input) {
409 0         0 $query = '.';
410             } else {
411 0 0       0 if ($non_help_option_used) {
412 0         0 _usage_error('filter expression is required');
413             }
414              
415             # If no args and no options, show help
416 0         0 print $USAGE;
417 0         0 exit 0;
418             }
419             }
420             }
421             elsif (@ARGV == 1) {
422             # Single arg: query or file
423 35 50 100     1837 if (!$null_input && -f $ARGV[0] && !defined $query) {
    100 66        
424 0         0 $filename = $ARGV[0];
425             # No query -> go to interactive mode (handled later)
426             }
427             elsif (!defined $query) {
428 34         112 $query = $ARGV[0];
429             }
430             else {
431             # Query already provided via -f; treat arg as filename if it exists
432 1         3 my $f = $ARGV[0];
433 1 50       3 if ($null_input) {
434 0         0 _usage_error('--null-input cannot be combined with file input');
435             }
436 1 50       31 if (-f $f) {
437 1         4 $filename = $f;
438             } else {
439 0         0 _input_error("Cannot open file '$f': $!");
440             }
441             }
442             }
443             elsif (@ARGV == 2) {
444             # Two args: query + file (in this order)
445 13 50       45 _usage_error('--null-input cannot be combined with file input') if $null_input;
446              
447 13 50       67 if (!defined $query) {
448 13         36 $query = $ARGV[0];
449             } else {
450 0         0 _usage_error('Cannot provide both --from-file and a query argument');
451             }
452              
453 13         28 my $f = $ARGV[1];
454 13 100       536 if (-f $f) {
455 12         41 $filename = $f;
456             } else {
457 1         20 _input_error("Cannot open file '$f': $!");
458             }
459             }
460             else {
461 0         0 _usage_error("Usage: jq-lite [options] '.query' [file.json]\n" .
462             " jq-lite [options] -f query.jq [file.json]");
463             }
464              
465             # ---------- JSON decoder selection ----------
466 48 50       145 if ($decoder_choice) {
467 0 0       0 if ($decoder_choice eq 'JSON::MaybeXS') {
    0          
    0          
    0          
468 0         0 require JSON::MaybeXS;
469 0         0 $decoder = \&JSON::MaybeXS::decode_json;
470 0         0 $decoder_module = 'JSON::MaybeXS';
471             }
472             elsif ($decoder_choice eq 'Cpanel::JSON::XS') {
473 0         0 require Cpanel::JSON::XS;
474 0         0 $decoder = \&Cpanel::JSON::XS::decode_json;
475 0         0 $decoder_module = 'Cpanel::JSON::XS';
476             }
477             elsif ($decoder_choice eq 'JSON::XS') {
478 0         0 require JSON::XS;
479 0         0 $decoder = \&JSON::XS::decode_json;
480 0         0 $decoder_module = 'JSON::XS';
481             }
482             elsif ($decoder_choice eq 'JSON::PP') {
483 0         0 require JSON::PP;
484 0         0 $decoder = \&JQ::Lite::Util::_decode_json;
485 0         0 $decoder_module = 'JSON::PP';
486             }
487             else {
488 0         0 _usage_error("Unknown JSON module: $decoder_choice");
489             }
490             }
491             else {
492 48 50       141 if (eval { require JSON::MaybeXS; 1 }) {
  48 0       32103  
  48 0       457772  
493 48         205 $decoder = \&JSON::MaybeXS::decode_json;
494 48         139 $decoder_module = 'JSON::MaybeXS';
495             }
496 0         0 elsif (eval { require Cpanel::JSON::XS; 1 }) {
  0         0  
497 0         0 $decoder = \&Cpanel::JSON::XS::decode_json;
498 0         0 $decoder_module = 'Cpanel::JSON::XS';
499             }
500 0         0 elsif (eval { require JSON::XS; 1 }) {
  0         0  
501 0         0 $decoder = \&JSON::XS::decode_json;
502 0         0 $decoder_module = 'JSON::XS';
503             }
504             else {
505 0         0 require JSON::PP;
506 0         0 $decoder = \&JQ::Lite::Util::_decode_json;
507 0         0 $decoder_module = 'JSON::PP';
508             }
509             }
510              
511 48 50       252 warn "[DEBUG] Using $decoder_module\n" if $decoder_debug;
512              
513             # ---------- --slurpfile handling ----------
514 48 100       232 if (@slurpfiles) {
515 4         20 for (my $i = 0; $i < @slurpfiles; $i += 2) {
516 4         23 my ($var_name, $file_path) = @slurpfiles[$i, $i + 1];
517              
518 4 50 33     41 _usage_error('--slurpfile requires a variable name') if !defined $var_name || $var_name eq '';
519 4 50       37 if ($var_name !~ /^[A-Za-z_]\w*$/) {
520 0         0 _usage_error("Invalid variable name '$var_name' for --slurpfile");
521             }
522              
523 4 50 33     27 _usage_error('--slurpfile requires a file path') if !defined $file_path || $file_path eq '';
524              
525 4 100       48 my $fh = IO::File->new($file_path, 'r')
526             or _usage_error("cannot read file: $file_path");
527 3         464 local $/;
528 3         76 my $json_text = <$fh>;
529 3 50       13 $json_text = '' unless defined $json_text;
530 3         26 $fh->close;
531              
532 3         75 my $decoded = eval { _decode_json_stream($json_text) };
  3         16  
533 3 100       337 if ($@) {
534 1         5 _usage_error("invalid JSON in slurpfile $var_name");
535             }
536              
537 2         19 $arg_vars{$var_name} = $decoded;
538             }
539             }
540              
541             # ---------- Validate query before reading input ----------
542 46 50       179 if (defined $query) {
543 46         275 _validate_query_syntax($query);
544             }
545              
546             # ---------- Read JSON input ----------
547 43         603 my $json_text;
548 43         404 my $use_yaml = 0;
549 43 100       319 if ($null_input) {
    100          
550 8 100       28 $json_text = $slurp_input ? '[]' : 'null';
551             }
552             elsif (defined $filename) {
553 13 50       734 open my $fh, '<', $filename or _input_error("Cannot open file '$filename': $!");
554 13         77 local $/;
555 13         469 $json_text = <$fh>;
556 13         196 close $fh;
557 13 100 66     242 $use_yaml = 1 if !$force_yaml && $filename =~ /\.ya?ml\z/i;
558             }
559             else {
560             # When no file is given, if STDIN is a TTY, error out to avoid blocking
561 22 50       232 if (-t STDIN) {
562 0         0 _input_error('No input provided. Pass a file or pipe JSON via STDIN.');
563             }
564 22         136 local $/;
565 22         833 $json_text = ;
566             }
567              
568 43 100       197 $use_yaml = 0 if $raw_input;
569 43 100 66     240 $use_yaml = 1 if $force_yaml && !$null_input;
570              
571 43 100       211 if ($use_yaml) {
572 2         10 $json_text = convert_yaml_to_json($json_text);
573 2 50 33     598 warn "[DEBUG] Parsing YAML with $yaml_module\n" if $decoder_debug && $yaml_module;
574             }
575              
576             # ---------- JQ::Lite core ----------
577 43         647 my $jq = JQ::Lite->new(raw => $raw_output, vars => \%arg_vars);
578              
579 43 100       181 if ($raw_input) {
580 4         8 $use_yaml = 0; # Raw input is plain text; ignore YAML handling
581             }
582              
583 43         95 my @raw_lines;
584 43 100       182 if ($raw_input) {
585 4   50     14 my $text = $json_text // '';
586 4         38 my $encoder = JSON::PP->new->utf8->allow_nonref;
587              
588 4 100       342 if ($slurp_input) {
589 2         9 $json_text = $encoder->encode($text);
590             }
591             else {
592 2         6442 @raw_lines = split /\r?\n/, $text, -1;
593 2 50 33     57 if (@raw_lines && $raw_lines[-1] eq '' && $text =~ /\r?\n\z/) {
      33        
594 2         36 pop @raw_lines;
595             }
596             }
597             }
598              
599 43 100 100     820 if (!$raw_input && $slurp_input && !$null_input) {
      100        
600 2         5 my $decoded = eval { _decode_json_stream($json_text) };
  2         10  
601 2 0 33     26 if (!$decoded && $@) {
602 0         0 _input_error("Failed to decode JSON stream for --slurp: $@");
603             }
604              
605 2         12 $json_text = JSON::PP->new->encode($decoded);
606             }
607              
608 43 100       734 if (!$raw_input) {
609 39 100       107 eval { JQ::Lite::Util::_decode_json($json_text); 1 }
  39         353  
  38         18650  
610             or _input_error("Failed to parse JSON input: $@");
611             }
612              
613             sub convert_yaml_to_json {
614 2     2   4 my ($text) = @_;
615 2 50       5 $text = '' unless defined $text;
616              
617 2 50       14 ($yaml_loader, $yaml_module) = _build_yaml_loader() unless $yaml_loader;
618              
619 2         4 my @docs = eval { $yaml_loader->($text) };
  2         6  
620 2 50       7 if (my $err = $@) {
621 0         0 $err =~ s/\s+\z//;
622 0         0 _input_error("Failed to parse YAML input: $err");
623             }
624              
625 2         3 my $data;
626 2 50       13 if (!@docs) {
    50          
627 0         0 $data = undef;
628             }
629             elsif (@docs == 1) {
630 2         4 $data = $docs[0];
631             }
632             else {
633 0         0 $data = \@docs;
634             }
635              
636 2         27 return JSON::PP->new->allow_nonref->encode($data);
637             }
638              
639             sub _build_yaml_loader {
640 2 50   2   4 if (eval { require YAML::XS; 1 }) {
  2 50       497  
  0         0  
641 0     0   0 return (sub { YAML::XS::Load($_[0]) }, 'YAML::XS');
  0         0  
642             }
643 2         113 elsif (eval { require YAML::PP; 1 }) {
  0         0  
644 0         0 require YAML::PP;
645 0         0 my $yp = YAML::PP->new(boolean => 'JSON::PP');
646 0     0   0 return (sub { $yp->load_string($_[0]) }, 'YAML::PP');
  0         0  
647             }
648             else {
649 2         927 require CPAN::Meta::YAML;
650             return (
651             sub {
652 2   50 2   63 my $string = shift // '';
653 2         22 my $docs = CPAN::Meta::YAML->read_string($string);
654 2 50       1046 if (!defined $docs) {
655 0         0 my $err;
656             {
657 63     63   138516 no warnings 'once';
  63         139  
  63         5999479  
  0         0  
658 0         0 $err = $CPAN::Meta::YAML::errstr;
659             }
660 0   0     0 $err ||= 'Unknown YAML parsing error';
661 0         0 die $err;
662             }
663 2         3 return @{$docs};
  2         18  
664             },
665 2         10865 'CPAN::Meta::YAML'
666             );
667             }
668             }
669              
670             sub _decode_json_stream {
671 5     5   15 my ($text) = @_;
672 5 50       18 $text = '' unless defined $text;
673              
674 5         12 my $length = length $text;
675 5         23 my $offset = 0;
676 5         15 my @values;
677 5         51 my $decoder = JSON::PP->new->utf8->allow_nonref;
678              
679 5         403 while (1) {
680 11   100     87 while ($offset < $length && substr($text, $offset, 1) =~ /\s/) {
681 6         23 $offset++;
682             }
683 11 100       28 last if $offset >= $length;
684              
685 7         12 my ($value, $consumed);
686 7         39 ($value, $consumed) = $decoder->decode_prefix(substr($text, $offset));
687              
688 6         1770 push @values, $value;
689 6         15 $offset += $consumed;
690             }
691              
692 4         57 return \@values;
693             }
694              
695             # ---------- Colorization ----------
696             sub colorize_json {
697 0     0   0 my $json = shift;
698              
699 0         0 $json =~ s/"([^"]+)"(?=\s*:)/color("cyan")."\"$1\"".color("reset")/ge;
  0         0  
700 0         0 $json =~ s/(:\s*)"([^"]*)"/$1.color("green")."\"$2\"".color("reset")/ge;
  0         0  
701 0         0 $json =~ s/(:\s*)(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/$1.color("yellow")."$2".color("reset")/ge;
  0         0  
702 0         0 $json =~ s/(:\s*)(true|false|null)/$1.color("magenta")."$2".color("reset")/ge;
  0         0  
703              
704 0         0 return $json;
705             }
706              
707             # ---------- Output ----------
708             sub print_results {
709 39     39   2092 my @results = @_;
710 39         453 my $pp = JSON::PP->new->utf8->allow_nonref->canonical;
711 39 100       13388 $pp->pretty unless $compact_output;
712 39         2226 $pp->ascii($ascii_output);
713              
714 39         586 for my $r (@results) {
715 2089 100 33     5898 if (!defined $r) {
    50          
716 2         47 print "null\n";
717             }
718             elsif ($raw_output && !ref($r)) {
719 0         0 print "$r\n";
720             }
721             else {
722 2087         4270 my $json = $pp->encode($r);
723 2087 50       135260 $json = colorize_json($json) if $color_output;
724 2087         4024 print $json;
725 2087 100       7326 print "\n" unless $json =~ /\n\z/;
726             }
727             }
728             }
729              
730             # ---------- Interactive mode ----------
731 42 50 66     266 if ($raw_input && !defined $query) {
732 0 0       0 my $reason = $slurp_input
733             ? '--raw-input requires a query when used with --slurp.'
734             : '--raw-input requires a query when not using --slurp.';
735 0         0 _usage_error($reason);
736             }
737              
738 42 50       219 if (!defined $query) {
739 0         0 system("stty -icanon -echo");
740              
741             $SIG{INT} = sub {
742 0     0   0 system("stty sane");
743 0         0 print "\n[EXIT]\n";
744 0         0 exit 0;
745 0         0 };
746              
747 0         0 my $input = '';
748 0         0 my @last_results;
749              
750 0         0 my $ok = eval {
751 0         0 @last_results = $jq->run_query($json_text, '.');
752 0         0 1;
753             };
754 0 0 0     0 if (!$ok || !@last_results) {
755 0         0 my $data = eval { JQ::Lite::Util::_decode_json($json_text) };
  0         0  
756 0 0       0 if ($data) {
757 0         0 @last_results = ($data);
758             }
759             }
760              
761 0         0 system("clear");
762 0 0       0 if (@last_results) {
763 0         0 print_results(@last_results);
764             } else {
765 0         0 print "[INFO] Failed to load initial JSON data.\n";
766             }
767              
768 0         0 print "\nType query (ESC to quit):\n";
769 0         0 print "> $input\n";
770              
771 0         0 while (1) {
772 0         0 my $char;
773 0         0 sysread(STDIN, $char, 1);
774              
775 0         0 my $ord = ord($char);
776 0 0       0 last if $ord == 27; # ESC
777              
778 0 0 0     0 if ($ord == 127 || $char eq "\b") {
779 0 0       0 chop $input if length($input);
780             } else {
781 0         0 $input .= $char;
782             }
783              
784 0         0 system("clear");
785              
786 0         0 my @results;
787 0         0 my $ok = eval {
788 0         0 @results = $jq->run_query($json_text, $input);
789 0         0 1;
790             };
791              
792 0 0 0     0 if ($ok && @results) {
793 0         0 @last_results = @results;
794             }
795              
796 0 0       0 if (!$ok) {
    0          
797 0         0 print "[INFO] Invalid or partial query. Showing last valid results.\n";
798             } elsif (!@results) {
799 0         0 print "[INFO] Query returned no results. Showing last valid results.\n";
800             }
801              
802 0 0       0 if (@last_results) {
803             eval {
804 0         0 print_results(@last_results);
805 0         0 1;
806 0 0       0 } or do {
807 0   0     0 my $e = $@ || 'Unknown error';
808 0         0 print "[RUNTIME]Failed to print: $e\n";
809             };
810             } else {
811 0         0 print "[INFO] No previous valid results.\n";
812             }
813              
814 0         0 print "\n> $input\n";
815             }
816              
817 0         0 system("stty sane");
818 0         0 print "\nGoodbye.\n";
819 0         0 exit 0;
820             }
821              
822             # ---------- One-shot mode ----------
823 42         321 my @results;
824              
825 42 100 100     327 if ($raw_input && !$slurp_input) {
826 2         12 my $encoder = JSON::PP->new->utf8->allow_nonref;
827 2         197 for my $line (@raw_lines) {
828 5002         15322 my $encoded_line = $encoder->encode($line);
829 5002         341794 my @line_results = eval { $jq->run_query($encoded_line, $query) };
  5002         16325  
830 5002 50       12550 if ($@) {
831 0   0     0 my $err = $@ || 'Unknown error';
832 0         0 $err =~ s/\s+\z//;
833 0         0 _runtime_error($err);
834             }
835 5002         15738 push @results, @line_results;
836             }
837             }
838             else {
839 40         113 @results = eval { $jq->run_query($json_text, $query) };
  40         337  
840 40 100       312 if ($@) {
841 3   50     10 my $err = $@ || 'Unknown error';
842 3         27 $err =~ s/\s+\z//;
843 3         26 _runtime_error($err);
844             }
845             }
846              
847 39         170 my $status = 0;
848 39 100       170 if ($exit_status) {
849 4 100       15 my $last = @results ? $results[-1] : undef;
850 4 100       15 $status = _is_truthy($last) ? 0 : 1;
851             }
852              
853             sub print_supported_functions {
854 0     0     print <<'EOF';
855              
856             Supported Functions:
857             length - Count array elements, hash keys, or characters in scalars
858             keys - Extract sorted keys from a hash or indexes from an array
859             keys_unsorted - Extract object keys without sorting (jq-compatible)
860             values - Extract values from a hash (v0.34)
861             leaf_paths() - Emit only terminal paths to leaf values
862             sort - Sort array items
863             sort_desc - Sort array items in descending order
864             sort_by(KEY) - Sort array of objects by key
865             pluck(KEY) - Collect a key's value from each object in an array
866             pick(KEYS...) - Build new objects containing only the supplied keys (arrays handled element-wise)
867             merge_objects() - Merge arrays of objects into a single hash (last-write-wins)
868             unique - Remove duplicate values
869             unique_by(KEY) - Remove duplicates by projecting each item on KEY
870             reverse - Reverse an array
871             first / last - Get first / last element of an array
872             limit(N) - Limit array to first N elements
873             drop(N) - Skip the first N elements of an array
874             rest - Drop the first element of an array
875             tail(N) - Return the final N elements of an array
876             chunks(N) - Split array into subarrays each containing up to N items
877             range(START; END[, STEP])
878             - Emit numbers from START (default 0) up to but not including END using STEP (default 1)
879             enumerate() - Pair each array element with its zero-based index
880             transpose() - Rotate arrays-of-arrays from rows into columns
881             scalars - Pass through only scalar inputs (string/number/bool/null)
882             objects - Pass through only object inputs
883             count - Count total number of matching items
884             map(EXPR) - Map/filter array items with a subquery
885             map_values(FILTER)
886             - Apply FILTER to each value in an object (dropping keys when FILTER yields no result)
887             if COND then A [elif COND then B ...] [else Z] end
888             - jq-style conditional branching across optional elif/else chains
889             foreach(EXPR as $var (init; update [; extract]))
890             - jq-compatible streaming reducer with lexical bindings and optional emitters
891             walk(FILTER) - Recursively apply FILTER to every value in arrays and objects
892             recurse([FILTER])
893             - Emit the current value and depth-first descendants using optional FILTER for children
894             add / sum - Sum all numeric values in an array
895             sum_by(KEY) - Sum numeric values projected from each array item
896             avg_by(KEY) - Average numeric values projected from each array item
897             median_by(KEY) - Return the median of numeric values projected from each array item
898             min_by(PATH) - Return the element with the smallest projected value
899             max_by(PATH) - Return the element with the largest projected value
900             product - Multiply all numeric values in an array
901             min / max - Return minimum / maximum numeric value in an array
902             avg - Return the average of numeric values in an array
903             median - Return the median of numeric values in an array
904             mode - Return the most frequent value in an array (ties pick earliest occurrence)
905             percentile(P) - Return the requested percentile (0-100 or 0-1) of numeric array values
906             variance - Return the variance of numeric values in an array
907             stddev - Return the standard deviation of numeric values in an array
908             abs - Convert numbers (and array elements) to their absolute value
909             ceil() - Round numbers up to the nearest integer
910             floor() - Round numbers down to the nearest integer
911             round() - Round numbers to the nearest integer (half-up semantics)
912             clamp(MIN, MAX) - Clamp numeric values within an inclusive range
913             tostring() - Convert values into their JSON string representation
914             @json - Format the input as JSON text (jq-style formatter)
915             @csv - Format arrays/scalars as a single CSV row
916             @tsv - Format arrays/scalars as a single TSV row
917             @base64 - Encode input as a Base64 string
918             @base64d - Decode Base64-encoded text into strings/arrays
919             @uri - Percent-encode text (URL-safe)
920             tojson() - Encode values as JSON text regardless of type
921             fromjson() - Decode JSON text into native values (arrays handled element-wise)
922             tonumber() / to_number()
923             - Coerce numeric-looking strings/booleans into numbers
924             nth(N) - Get the Nth element of an array (zero-based index)
925             index(VALUE) - Return the zero-based index of VALUE within arrays or strings
926             rindex(VALUE) - Return the zero-based index of the last VALUE within arrays or strings
927             indices(VALUE) - Return every index where VALUE occurs within arrays or strings
928             group_by(KEY) - Group array items by field
929             group_count(KEY) - Count grouped items by field
930             join(SEPARATOR) - Join array elements with a string
931             split(SEPARATOR) - Split string values (and arrays of strings) by a literal separator
932             substr(START[, LENGTH])
933             - Extract substring using zero-based indices (arrays handled element-wise)
934             slice(START[, LENGTH])
935             - Return a subarray using zero-based indices (negative starts count from the end)
936             replace(OLD; NEW)
937             - Replace literal substrings (arrays handled element-wise)
938             to_entries - Convert objects/arrays into [{"key","value"}, ...] pairs
939             from_entries - Convert entry arrays back into an object
940             with_entries(FILTER)
941             - Transform entries using FILTER and rebuild an object
942             delpaths(PATHS) - Delete keys at the supplied PATHS array (jq-compatible)
943             has - Check if objects contain a key or arrays expose an index
944             contains - Check if strings include a fragment, arrays contain an element, or hashes have a key
945             contains_subset(VALUE)
946             - jq-style subset containment for arrays (order-insensitive)
947             inside(CONTAINER) - Check if the input value is contained within CONTAINER
948             any([FILTER]) - Return true if any input (optionally filtered) is truthy
949             all([FILTER]) - Return true if every input (optionally filtered) is truthy
950             not - Logical negation following jq truthiness rules
951             test("pattern"[, "flags"]) - Check strings against regexes (flags: i, m, s, x)
952             explode() - Convert strings to arrays of Unicode code points
953             implode() - Turn arrays of code points back into strings
954             flatten - Explicitly flatten arrays (same as .[])
955             flatten_all() - Recursively flatten nested arrays into a single array
956             flatten_depth(N) - Flatten nested arrays up to N levels deep
957             arrays - Pass through inputs that are arrays, discarding others
958             del(KEY) - Remove a key from objects in the result
959             compact - Remove undefined values from arrays
960             path - Return available keys for objects or indexes for arrays
961             paths - Emit every path to nested values as an array of keys/indices
962             getpath(PATH) - Retrieve the value(s) at the supplied path array or expression
963             setpath(PATH; VALUE)
964             - Set or create value(s) at a path using literal or filter input
965             is_empty - Check if an array or object is empty
966             upper() - Convert scalars and array elements to uppercase
967             lower() - Convert scalars and array elements to lowercase
968             ascii_upcase() - ASCII-only uppercase conversion (leaves non-ASCII untouched)
969             ascii_downcase() - ASCII-only lowercase conversion (leaves non-ASCII untouched)
970             titlecase() - Convert scalars and array elements to title case
971             trim() - Strip leading/trailing whitespace from strings (recurses into arrays)
972             ltrimstr(PFX) - Remove PFX when it appears at the start of strings (arrays handled element-wise)
973             rtrimstr(SFX) - Remove SFX when it appears at the end of strings (arrays handled element-wise)
974             startswith(PFX) - Check if a string (or array of strings) begins with PFX
975             endswith(SFX) - Check if a string (or array of strings) ends with SFX
976             empty - Discard all output (for side-effect use)
977             type() - Return the type of value ("string", "number", "boolean", "array", "object", "null")
978             lhs // rhs - jq-style alternative operator yielding rhs when lhs is null/missing
979             default(VALUE) - Substitute VALUE when result is undefined
980              
981             EOF
982             }
983              
984 39         546 print_results(@results);
985              
986 38         4054 exit $status;
987              
988             __END__