rjq 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +38 -0
- data/CHANGELOG.md +7 -0
- data/COMPATIBILITY.md +48 -0
- data/CONTRIBUTING.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/SECURITY.md +10 -0
- data/bin/rjq +9 -0
- data/lib/rjq/ast.rb +1475 -0
- data/lib/rjq/builtins/array.rb +3 -0
- data/lib/rjq/builtins/core.rb +3 -0
- data/lib/rjq/builtins/date.rb +3 -0
- data/lib/rjq/builtins/format.rb +3 -0
- data/lib/rjq/builtins/io.rb +3 -0
- data/lib/rjq/builtins/math.rb +3 -0
- data/lib/rjq/builtins/regex.rb +3 -0
- data/lib/rjq/builtins/sql.rb +3 -0
- data/lib/rjq/builtins/stream.rb +3 -0
- data/lib/rjq/builtins/string.rb +3 -0
- data/lib/rjq/builtins.rb +2103 -0
- data/lib/rjq/cli.rb +459 -0
- data/lib/rjq/color.rb +36 -0
- data/lib/rjq/compiler.rb +392 -0
- data/lib/rjq/errors.rb +76 -0
- data/lib/rjq/json/dumper.rb +191 -0
- data/lib/rjq/json/input_buffer.rb +99 -0
- data/lib/rjq/json/parser.rb +405 -0
- data/lib/rjq/json/stream_parser.rb +526 -0
- data/lib/rjq/json.rb +4 -0
- data/lib/rjq/lexer.rb +344 -0
- data/lib/rjq/math_functions.rb +168 -0
- data/lib/rjq/module_loader.rb +178 -0
- data/lib/rjq/modules.rb +88 -0
- data/lib/rjq/number.rb +189 -0
- data/lib/rjq/opcodes.rb +130 -0
- data/lib/rjq/parser.rb +755 -0
- data/lib/rjq/path.rb +250 -0
- data/lib/rjq/runtime.rb +377 -0
- data/lib/rjq/semantic_analyzer.rb +209 -0
- data/lib/rjq/value.rb +287 -0
- data/lib/rjq/version.rb +5 -0
- data/lib/rjq/vm.rb +1359 -0
- data/lib/rjq.rb +41 -0
- metadata +106 -0
data/lib/rjq/cli.rb
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../rjq'
|
|
4
|
+
|
|
5
|
+
module Rjq
|
|
6
|
+
class CLI
|
|
7
|
+
HELP = <<~HELP.freeze
|
|
8
|
+
rjq - commandline JSON processor [version #{VERSION}]
|
|
9
|
+
|
|
10
|
+
Usage: rjq [options] <jq filter> [file...]
|
|
11
|
+
rjq [options] --args <jq filter> [strings...]
|
|
12
|
+
rjq [options] --jsonargs <jq filter> [JSON_TEXTS...]
|
|
13
|
+
|
|
14
|
+
rjq is a tool for processing JSON inputs, applying the given filter to
|
|
15
|
+
its JSON text inputs and producing the filter's results as JSON on
|
|
16
|
+
standard output.
|
|
17
|
+
|
|
18
|
+
Command options:
|
|
19
|
+
-n, --null-input use `null` as the single input value;
|
|
20
|
+
-R, --raw-input read each line as string instead of JSON;
|
|
21
|
+
-s, --slurp read all inputs into an array and use it as
|
|
22
|
+
the single input value;
|
|
23
|
+
-c, --compact-output compact instead of pretty-printed output;
|
|
24
|
+
-r, --raw-output output strings without escapes and quotes;
|
|
25
|
+
--raw-output0 implies -r and output NUL after each output;
|
|
26
|
+
-j, --join-output implies -r and output without newline after
|
|
27
|
+
each output;
|
|
28
|
+
-a, --ascii-output output strings by only ASCII characters
|
|
29
|
+
using escape sequences;
|
|
30
|
+
-S, --sort-keys sort keys of each object on output;
|
|
31
|
+
-C, --color-output colorize JSON output;
|
|
32
|
+
-M, --monochrome-output disable colored output;
|
|
33
|
+
--tab use tabs for indentation;
|
|
34
|
+
--indent n use n spaces for indentation (max 7 spaces);
|
|
35
|
+
--unbuffered flush output stream after each output;
|
|
36
|
+
--stream parse the input value in streaming fashion;
|
|
37
|
+
--stream-errors implies --stream and report parse error as
|
|
38
|
+
an array;
|
|
39
|
+
--seq parse input/output as application/json-seq;
|
|
40
|
+
--max-filter-depth n reject filters nested deeper than n;
|
|
41
|
+
--max-call-depth n bound non-tail user-function calls;
|
|
42
|
+
--max-instructions n bound executed bytecode instructions;
|
|
43
|
+
--max-replay-cache n bound values cached for filter replay;
|
|
44
|
+
-f, --from-file file load filter from the file;
|
|
45
|
+
-L directory search modules from the directory;
|
|
46
|
+
--arg name value set $name to the string value;
|
|
47
|
+
--argjson name value set $name to the JSON value;
|
|
48
|
+
--slurpfile name file set $name to an array of JSON values read
|
|
49
|
+
from the file;
|
|
50
|
+
--rawfile name file set $name to string contents of file;
|
|
51
|
+
--args consume remaining arguments as positional
|
|
52
|
+
string values;
|
|
53
|
+
--jsonargs consume remaining arguments as positional
|
|
54
|
+
JSON values;
|
|
55
|
+
-e, --exit-status set exit status code based on the output;
|
|
56
|
+
-V, --version show the version;
|
|
57
|
+
--build-configuration show rjq's build configuration;
|
|
58
|
+
-h, --help show the help;
|
|
59
|
+
-- terminates argument processing;
|
|
60
|
+
|
|
61
|
+
Named arguments are also available as $ARGS.named[], while
|
|
62
|
+
positional arguments are available as $ARGS.positional[].
|
|
63
|
+
HELP
|
|
64
|
+
OPTION_HELP = <<~HELP
|
|
65
|
+
Use rjq --help for help with command-line options,
|
|
66
|
+
or see the jq manpage, or online docs at https://jqlang.github.io/jq
|
|
67
|
+
HELP
|
|
68
|
+
|
|
69
|
+
class OptionError < StandardError; end
|
|
70
|
+
class EarlyExit < StandardError
|
|
71
|
+
attr_reader :status
|
|
72
|
+
|
|
73
|
+
def initialize(status)
|
|
74
|
+
@status = status
|
|
75
|
+
super()
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def initialize(argv, stdin:, stdout:, stderr:)
|
|
80
|
+
@argv = argv.dup
|
|
81
|
+
@stdin = stdin
|
|
82
|
+
@stdout = stdout
|
|
83
|
+
@stderr = stderr
|
|
84
|
+
@opts = Runtime::DEFAULT_OPTIONS.merge(variables: { 'ARGS.named' => {}, 'ARGS.positional' => [] })
|
|
85
|
+
@filter = nil
|
|
86
|
+
@files = []
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run
|
|
90
|
+
parse!
|
|
91
|
+
return run_tests if @opts[:run_tests]
|
|
92
|
+
|
|
93
|
+
@opts[:stderr] = @stderr
|
|
94
|
+
@opts[:color] = @stdout.tty? && !ENV.key?('NO_COLOR') if @opts[:color].nil?
|
|
95
|
+
runtime_failed = false
|
|
96
|
+
@opts[:runtime_error_handler] = lambda do |error, _record|
|
|
97
|
+
runtime_failed = true
|
|
98
|
+
@stderr.puts("rjq: runtime error: #{error.message}")
|
|
99
|
+
end
|
|
100
|
+
last = nil
|
|
101
|
+
count = 0
|
|
102
|
+
runtime = Runtime.new(@filter || '.', @opts)
|
|
103
|
+
runtime.run_io_streams(input_streams).each do |value|
|
|
104
|
+
last = value
|
|
105
|
+
count += 1
|
|
106
|
+
write_value(runtime, value)
|
|
107
|
+
end
|
|
108
|
+
return 5 if runtime_failed
|
|
109
|
+
return exit_status(last, count) if @opts[:exit_status]
|
|
110
|
+
|
|
111
|
+
0
|
|
112
|
+
rescue HaltError => e
|
|
113
|
+
@stderr.puts(e.message) if e.value
|
|
114
|
+
e.status
|
|
115
|
+
rescue EarlyExit => e
|
|
116
|
+
e.status
|
|
117
|
+
rescue OptionError => e
|
|
118
|
+
@stderr.puts(e.message)
|
|
119
|
+
@stderr.print(OPTION_HELP)
|
|
120
|
+
2
|
|
121
|
+
rescue JSONParseError => e
|
|
122
|
+
@stderr.puts("rjq: JSON parse error: #{e.message}")
|
|
123
|
+
5
|
|
124
|
+
rescue ParseError, CompileError => e
|
|
125
|
+
@stderr.puts("rjq: compile error: #{e.message}")
|
|
126
|
+
3
|
|
127
|
+
rescue Rjq::RuntimeError => e
|
|
128
|
+
@stderr.puts("rjq: runtime error: #{e.message}")
|
|
129
|
+
5
|
|
130
|
+
rescue SystemStackError
|
|
131
|
+
@stderr.puts('rjq: runtime error: recursion limit exceeded')
|
|
132
|
+
5
|
|
133
|
+
rescue Errno::EPIPE
|
|
134
|
+
0
|
|
135
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR => e
|
|
136
|
+
@stderr.puts("rjq: #{e.message}")
|
|
137
|
+
2
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def parse!
|
|
143
|
+
until @argv.empty?
|
|
144
|
+
arg = @argv.shift
|
|
145
|
+
case arg
|
|
146
|
+
when '--'
|
|
147
|
+
if @filter.nil? && !@opts[:filter_file] && !@argv.empty?
|
|
148
|
+
@filter = @argv.shift
|
|
149
|
+
end
|
|
150
|
+
@files.concat(@argv)
|
|
151
|
+
@argv.clear
|
|
152
|
+
when /\A--/
|
|
153
|
+
parse_long(arg)
|
|
154
|
+
when /\A-(?:\d|\.)/
|
|
155
|
+
if @filter.nil? && !@opts[:filter_file]
|
|
156
|
+
@filter = arg
|
|
157
|
+
else
|
|
158
|
+
@files << arg
|
|
159
|
+
end
|
|
160
|
+
when /\A-[^-]/
|
|
161
|
+
parse_short(arg)
|
|
162
|
+
else
|
|
163
|
+
if @filter.nil? && !@opts[:filter_file]
|
|
164
|
+
@filter = arg
|
|
165
|
+
else
|
|
166
|
+
@files << arg
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return unless @opts[:filter_file]
|
|
172
|
+
|
|
173
|
+
@files.unshift(@filter) if @filter
|
|
174
|
+
filter_file = @opts.delete(:filter_file)
|
|
175
|
+
@filter = File.read(filter_file)
|
|
176
|
+
@opts[:source_path] = File.realpath(filter_file)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def parse_long(arg)
|
|
180
|
+
case arg
|
|
181
|
+
when '--compact-output' then @opts[:compact] = true
|
|
182
|
+
when '--raw-output' then @opts[:raw_output] = true
|
|
183
|
+
when '--raw-output0' then @opts[:raw_output] = @opts[:raw_output0] = true
|
|
184
|
+
when '--join-output' then @opts[:raw_output] = @opts[:join_output] = true
|
|
185
|
+
when '--null-input' then @opts[:null_input] = true
|
|
186
|
+
when '--raw-input' then @opts[:raw_input] = true
|
|
187
|
+
when '--slurp' then @opts[:slurp] = true
|
|
188
|
+
when '--ascii-output' then @opts[:ascii] = true
|
|
189
|
+
when '--sort-keys' then @opts[:sort_keys] = true
|
|
190
|
+
when '--tab' then @opts[:tab] = true
|
|
191
|
+
when '--seq' then @opts[:seq] = true
|
|
192
|
+
when '--stream' then @opts[:stream] = true
|
|
193
|
+
when '--stream-errors' then @opts[:stream] = @opts[:stream_errors] = true
|
|
194
|
+
when '--exit-status' then @opts[:exit_status] = true
|
|
195
|
+
when '--unbuffered' then @opts[:unbuffered] = true
|
|
196
|
+
when '--color-output' then @opts[:color] = true
|
|
197
|
+
when '--monochrome-output' then @opts[:color] = false
|
|
198
|
+
when '--allow-comments' then @opts[:allow_comments] = true
|
|
199
|
+
when '--max-filter-depth'
|
|
200
|
+
@opts[:max_filter_depth] = validate_limit(next_arg('--max-filter-depth'), '--max-filter-depth', minimum: 1)
|
|
201
|
+
when '--max-call-depth'
|
|
202
|
+
@opts[:max_call_depth] = validate_limit(next_arg('--max-call-depth'), '--max-call-depth', minimum: 1)
|
|
203
|
+
when '--max-instructions'
|
|
204
|
+
@opts[:max_instructions] = validate_limit(next_arg('--max-instructions'), '--max-instructions', minimum: 0)
|
|
205
|
+
when '--max-replay-cache'
|
|
206
|
+
@opts[:max_replay_cache] = validate_limit(next_arg('--max-replay-cache'), '--max-replay-cache', minimum: 0)
|
|
207
|
+
when '--indent'
|
|
208
|
+
@opts[:indent] = validate_indent(next_arg('--indent', '--indent takes one parameter'))
|
|
209
|
+
@opts[:tab] = false
|
|
210
|
+
when '--arg' then bind_string(*next_args(2, '--arg takes two parameters (e.g. --arg varname value)'))
|
|
211
|
+
when '--argjson' then bind_json(*next_args(2, '--argjson takes two parameters (e.g. --argjson varname text)'))
|
|
212
|
+
when '--slurpfile' then bind_json_array(*next_args(2,
|
|
213
|
+
'--slurpfile takes two parameters (e.g. --slurpfile varname filename)'))
|
|
214
|
+
when '--rawfile' then bind_raw_file(*next_args(2,
|
|
215
|
+
'--rawfile takes two parameters (e.g. --rawfile varname filename)'))
|
|
216
|
+
when '--args' then consume_positional(json: false)
|
|
217
|
+
when '--jsonargs' then consume_positional(json: true)
|
|
218
|
+
when '--from-file' then @opts[:filter_file] = next_arg('--from-file')
|
|
219
|
+
when '--run-tests' then parse_run_tests
|
|
220
|
+
when '--build-configuration' then print_build_configuration_and_exit
|
|
221
|
+
when '--version' then print_version_and_exit
|
|
222
|
+
when '--help' then print_help_and_exit
|
|
223
|
+
else
|
|
224
|
+
raise OptionError, "rjq: Unknown option #{arg}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def parse_short(arg)
|
|
229
|
+
chars = arg.delete_prefix('-').chars
|
|
230
|
+
until chars.empty?
|
|
231
|
+
char = chars.shift
|
|
232
|
+
case char
|
|
233
|
+
when 'c' then @opts[:compact] = true
|
|
234
|
+
when 'r' then @opts[:raw_output] = true
|
|
235
|
+
when 'j' then @opts[:raw_output] = @opts[:join_output] = true
|
|
236
|
+
when 'n' then @opts[:null_input] = true
|
|
237
|
+
when 'R' then @opts[:raw_input] = true
|
|
238
|
+
when 's' then @opts[:slurp] = true
|
|
239
|
+
when 'a' then @opts[:ascii] = true
|
|
240
|
+
when 'S' then @opts[:sort_keys] = true
|
|
241
|
+
when 'e' then @opts[:exit_status] = true
|
|
242
|
+
when 'f'
|
|
243
|
+
@opts[:filter_file] = chars.empty? ? next_arg('-f') : chars.join
|
|
244
|
+
chars.clear
|
|
245
|
+
when 'L'
|
|
246
|
+
(@opts[:library_path] ||= []) << (chars.empty? ? next_arg('-L') : chars.join)
|
|
247
|
+
chars.clear
|
|
248
|
+
when 'V' then print_version_and_exit
|
|
249
|
+
when 'h' then print_help_and_exit
|
|
250
|
+
when 'C', 'M'
|
|
251
|
+
@opts[:color] = char == 'C'
|
|
252
|
+
else
|
|
253
|
+
raise OptionError, "rjq: Unknown option -#{char}"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def parse_run_tests
|
|
259
|
+
@opts[:run_tests] = true
|
|
260
|
+
@opts[:run_tests_file] = @argv.shift unless @argv.empty?
|
|
261
|
+
@argv.clear
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def bind_string(name, value)
|
|
265
|
+
@opts[:variables][name] = value
|
|
266
|
+
@opts[:variables]['ARGS.named'][name] = value
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def bind_json(name, value)
|
|
270
|
+
parsed = JSON::Parser.parse_one(value)
|
|
271
|
+
@opts[:variables][name] = parsed
|
|
272
|
+
@opts[:variables]['ARGS.named'][name] = parsed
|
|
273
|
+
rescue JSONParseError => e
|
|
274
|
+
raise OptionError, "rjq: invalid JSON text passed to --argjson: #{e.message}"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def bind_json_array(name, path)
|
|
278
|
+
parsed = JSON::Parser.parse(File.read(path)).to_a
|
|
279
|
+
@opts[:variables][name] = parsed
|
|
280
|
+
@opts[:variables]['ARGS.named'][name] = parsed
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def bind_raw_file(name, path)
|
|
284
|
+
bind_string(name, File.read(path))
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def consume_positional(json:)
|
|
288
|
+
@filter ||= next_arg(json ? '--jsonargs filter' : '--args filter')
|
|
289
|
+
values = @argv.map { |arg| json ? JSON::Parser.parse_one(arg) : arg }
|
|
290
|
+
@opts[:variables]['ARGS.positional'] = values
|
|
291
|
+
@argv.clear
|
|
292
|
+
rescue JSONParseError => e
|
|
293
|
+
raise OptionError, "rjq: invalid JSON text passed to --jsonargs: #{e.message}"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def next_arg(option, message = "#{option} takes one parameter")
|
|
297
|
+
raise OptionError, "rjq: #{message}" if @argv.empty?
|
|
298
|
+
|
|
299
|
+
@argv.shift
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def next_args(count, message)
|
|
303
|
+
raise OptionError, "rjq: #{message}" if @argv.length < count
|
|
304
|
+
|
|
305
|
+
@argv.shift(count)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def validate_indent(value)
|
|
309
|
+
indent = Integer(value)
|
|
310
|
+
raise OptionError, 'rjq: --indent must be between 0 and 7' unless indent.between?(0, 7)
|
|
311
|
+
|
|
312
|
+
indent
|
|
313
|
+
rescue ArgumentError
|
|
314
|
+
raise OptionError, 'rjq: --indent must be an integer'
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def validate_limit(value, option, minimum:)
|
|
318
|
+
limit = Integer(value, 10)
|
|
319
|
+
raise OptionError, "rjq: #{option} must be at least #{minimum}" if limit < minimum
|
|
320
|
+
|
|
321
|
+
limit
|
|
322
|
+
rescue ArgumentError
|
|
323
|
+
raise OptionError, "rjq: #{option} must be an integer"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def input_streams
|
|
327
|
+
Enumerator.new do |yielder|
|
|
328
|
+
if @files.empty?
|
|
329
|
+
yielder << [@stdin, nil, false]
|
|
330
|
+
next
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
@files.each do |file|
|
|
334
|
+
if file == '-'
|
|
335
|
+
yielder << [@stdin, nil, false]
|
|
336
|
+
else
|
|
337
|
+
yielder << [File.open(file, 'rb'), file, true]
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def write_value(runtime, value)
|
|
344
|
+
if @opts[:raw_output0] && value.is_a?(String) && value.include?("\0")
|
|
345
|
+
raise Rjq::RuntimeError, 'Cannot dump a string containing NUL with --raw-output0 option'
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
@stdout.print("\x1e") if @opts[:seq]
|
|
349
|
+
runtime.write_output(value, @stdout)
|
|
350
|
+
@stdout.print(output_separator)
|
|
351
|
+
@stdout.flush if @opts[:unbuffered] && @stdout.respond_to?(:flush)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def output_separator
|
|
355
|
+
return "\0" if @opts[:raw_output0]
|
|
356
|
+
return '' if @opts[:join_output]
|
|
357
|
+
|
|
358
|
+
"\n"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def exit_status(last, count)
|
|
362
|
+
return 4 if count.zero?
|
|
363
|
+
return 1 if last.nil? || last == false
|
|
364
|
+
|
|
365
|
+
0
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def run_tests
|
|
369
|
+
source = @opts[:run_tests_file] ? File.read(@opts[:run_tests_file]) : @stdin.read
|
|
370
|
+
results = run_test_groups(test_groups(source))
|
|
371
|
+
@stdout.puts("#{results.fetch(:passed)} of #{results.fetch(:checked)} tests passed " \
|
|
372
|
+
"(#{results.fetch(:malformed)} malformed, #{results.fetch(:skipped)} skipped)")
|
|
373
|
+
results.fetch(:failed).zero? && results.fetch(:malformed).zero? ? 0 : 1
|
|
374
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
375
|
+
@stderr.puts(e.message.include?('No such file') ? 'fopen: No such file or directory' : e.message)
|
|
376
|
+
1
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def test_groups(source)
|
|
380
|
+
groups = []
|
|
381
|
+
current = []
|
|
382
|
+
start_line = nil
|
|
383
|
+
fail_mode = false
|
|
384
|
+
source.each_line(chomp: true).with_index(1) do |line, line_number|
|
|
385
|
+
if line.empty?
|
|
386
|
+
groups << [fail_mode, current, start_line] unless current.empty?
|
|
387
|
+
current = []
|
|
388
|
+
start_line = nil
|
|
389
|
+
fail_mode = false
|
|
390
|
+
next
|
|
391
|
+
end
|
|
392
|
+
next if line.start_with?('#')
|
|
393
|
+
|
|
394
|
+
if line.start_with?('%%FAIL')
|
|
395
|
+
fail_mode = true
|
|
396
|
+
next
|
|
397
|
+
end
|
|
398
|
+
start_line ||= line_number
|
|
399
|
+
current << line
|
|
400
|
+
end
|
|
401
|
+
groups << [fail_mode, current, start_line] unless current.empty?
|
|
402
|
+
groups
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def run_test_groups(groups)
|
|
406
|
+
groups.each_with_object({ checked: 0, passed: 0, failed: 0, malformed: 0,
|
|
407
|
+
skipped: 0 }) do |(fail_mode, lines, line_number), results|
|
|
408
|
+
results[:checked] += 1
|
|
409
|
+
unless lines.empty?
|
|
410
|
+
@stdout.puts("Test ##{results.fetch(:checked)}: '#{lines.first}' at line number #{line_number}")
|
|
411
|
+
end
|
|
412
|
+
if lines.length < (fail_mode ? 1 : 3)
|
|
413
|
+
results[:malformed] += 1
|
|
414
|
+
elsif test_group_passed?(fail_mode, lines)
|
|
415
|
+
results[:passed] += 1
|
|
416
|
+
else
|
|
417
|
+
results[:failed] += 1
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def test_group_passed?(fail_mode, lines)
|
|
423
|
+
program, input, *expected = lines
|
|
424
|
+
if fail_mode
|
|
425
|
+
Rjq.compile(program).run(nil).to_a
|
|
426
|
+
return false
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
actual = JSON::Parser.parse(input).to_a.flat_map { |value| Rjq.run(program, value).to_a }
|
|
430
|
+
expected_values = expected.map { |line| JSON::Parser.parse_one(line) }
|
|
431
|
+
expected_values.length == actual.length && expected_values.zip(actual).all? do |left, right|
|
|
432
|
+
Value.equal?(left, right)
|
|
433
|
+
end
|
|
434
|
+
rescue Rjq::Error
|
|
435
|
+
fail_mode
|
|
436
|
+
rescue StandardError
|
|
437
|
+
false
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def print_version_and_exit
|
|
441
|
+
@stdout.puts("rjq-#{VERSION}")
|
|
442
|
+
raise EarlyExit, 0
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def print_help_and_exit
|
|
446
|
+
@stdout.print(HELP)
|
|
447
|
+
raise EarlyExit, 0
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def print_build_configuration_and_exit
|
|
451
|
+
@stdout.puts("rjq=#{VERSION}")
|
|
452
|
+
@stdout.puts("ruby=#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})")
|
|
453
|
+
@stdout.puts('regexp-engine=ruby')
|
|
454
|
+
@stdout.puts('native-math=fiddle-libm')
|
|
455
|
+
@stdout.puts('json-parser=incremental')
|
|
456
|
+
raise EarlyExit, 0
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
data/lib/rjq/color.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
module Color
|
|
5
|
+
DEFAULT = ['1;30', '0;39', '0;39', '0;39', '0;32', '1;39', '1;39', '1;34'].freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def colorize(json)
|
|
10
|
+
colors = (ENV['RJQ_COLORS'] || ENV.fetch('JQ_COLORS', nil)).to_s.split(':')
|
|
11
|
+
colors = DEFAULT unless valid_colors?(colors)
|
|
12
|
+
json.gsub(/"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:null|true|false)\b|[\[\]{}]/) do |token|
|
|
13
|
+
remainder = json[Regexp.last_match.end(0)..].to_s
|
|
14
|
+
object_key = token.start_with?('"') && remainder.match?(/\A\s*:/)
|
|
15
|
+
color = color_for(token, colors, object_key: object_key)
|
|
16
|
+
color ? "\e[#{color}m#{token}\e[0m" : token
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def color_for(token, colors, object_key: false)
|
|
21
|
+
case token
|
|
22
|
+
when 'null' then colors[0]
|
|
23
|
+
when 'false' then colors[1]
|
|
24
|
+
when 'true' then colors[2]
|
|
25
|
+
when /\A-?\d/ then colors[3]
|
|
26
|
+
when /\A"/ then object_key ? colors[7] : colors[4]
|
|
27
|
+
when '[', ']' then colors[5]
|
|
28
|
+
when '{', '}' then colors[6]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def valid_colors?(colors)
|
|
33
|
+
colors.length >= 8 && colors.first(8).all? { |color| color.match?(/\A\d{1,3}(?:;\d{1,3})*\z/) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|