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/path.rb
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
module Path
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def get(value, path)
|
|
8
|
+
assert_path(path)
|
|
9
|
+
path.reduce(value) { |current, key| read_index(current, key) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def read_index(value, index)
|
|
13
|
+
return read_slice(value, index) if slice_component?(index)
|
|
14
|
+
|
|
15
|
+
case value
|
|
16
|
+
when NilClass
|
|
17
|
+
raise TypeError, cannot_index_message(value, index) unless index.is_a?(String) || index.is_a?(Numeric)
|
|
18
|
+
|
|
19
|
+
nil
|
|
20
|
+
when Array
|
|
21
|
+
raise TypeError, cannot_index_message(value, index) unless index.is_a?(Numeric)
|
|
22
|
+
|
|
23
|
+
read_array(value, index)
|
|
24
|
+
when Hash
|
|
25
|
+
raise TypeError, cannot_index_message(value, index) unless index.is_a?(String)
|
|
26
|
+
|
|
27
|
+
value[index]
|
|
28
|
+
when String
|
|
29
|
+
raise TypeError, cannot_index_message(value, index)
|
|
30
|
+
else
|
|
31
|
+
raise TypeError, cannot_index_message(value, index)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set(value, path, new_value)
|
|
36
|
+
assert_path(path)
|
|
37
|
+
return new_value if path.empty?
|
|
38
|
+
|
|
39
|
+
if value.nil?
|
|
40
|
+
read_index(nil, path.first)
|
|
41
|
+
value = container_for(path.first)
|
|
42
|
+
end
|
|
43
|
+
parent = ensure_parent(value, path)
|
|
44
|
+
key = path.last
|
|
45
|
+
if slice_component?(key)
|
|
46
|
+
replace_slice(parent, key, new_value)
|
|
47
|
+
return value
|
|
48
|
+
end
|
|
49
|
+
key = normalize_array_key(parent, key)
|
|
50
|
+
case parent
|
|
51
|
+
when Array
|
|
52
|
+
parent[key] = new_value
|
|
53
|
+
when Hash
|
|
54
|
+
raise TypeError, cannot_index_message(parent, key) unless key.is_a?(String)
|
|
55
|
+
|
|
56
|
+
parent[key] = new_value
|
|
57
|
+
else
|
|
58
|
+
raise TypeError, cannot_index_message(parent, key)
|
|
59
|
+
end
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def delete(value, path)
|
|
64
|
+
assert_path(path)
|
|
65
|
+
return nil if path.empty?
|
|
66
|
+
|
|
67
|
+
parent = get(value, path[0...-1])
|
|
68
|
+
key = path.last
|
|
69
|
+
if slice_component?(key)
|
|
70
|
+
replacement = parent.is_a?(String) ? '' : []
|
|
71
|
+
replace_slice(parent, key, replacement)
|
|
72
|
+
return value
|
|
73
|
+
end
|
|
74
|
+
case parent
|
|
75
|
+
when Array
|
|
76
|
+
raise TypeError, cannot_index_message(parent, key) unless key.is_a?(Numeric)
|
|
77
|
+
return value if key.respond_to?(:finite?) && !key.finite?
|
|
78
|
+
|
|
79
|
+
key = key.to_i
|
|
80
|
+
return value if key.zero? && path.last.negative?
|
|
81
|
+
|
|
82
|
+
key = parent.length + key if key.negative?
|
|
83
|
+
parent.delete_at(key) unless key.negative?
|
|
84
|
+
when Hash
|
|
85
|
+
raise TypeError, cannot_index_message(parent, key) unless key.is_a?(String)
|
|
86
|
+
|
|
87
|
+
parent.delete(key)
|
|
88
|
+
end
|
|
89
|
+
value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def paths(value, leaves_only: false)
|
|
93
|
+
out = []
|
|
94
|
+
stack = [[value, nil]]
|
|
95
|
+
until stack.empty?
|
|
96
|
+
current, path_node = stack.pop
|
|
97
|
+
scalar = !(current.is_a?(Array) || current.is_a?(Hash))
|
|
98
|
+
out << materialize_path(path_node) if path_node.nil? || !leaves_only || scalar
|
|
99
|
+
children = if current.is_a?(Array)
|
|
100
|
+
current.each_with_index.map { |item, index| [item, [path_node, index]] }
|
|
101
|
+
elsif current.is_a?(Hash)
|
|
102
|
+
current.map { |key, item| [item, [path_node, key]] }
|
|
103
|
+
else
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
stack.concat(children.reverse)
|
|
107
|
+
end
|
|
108
|
+
out
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def assert_path(path)
|
|
112
|
+
raise TypeError, 'Path must be specified as an array' unless path.is_a?(Array)
|
|
113
|
+
|
|
114
|
+
path
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def materialize_path(node)
|
|
118
|
+
path = []
|
|
119
|
+
while node
|
|
120
|
+
node, key = node
|
|
121
|
+
path << key
|
|
122
|
+
end
|
|
123
|
+
path.reverse
|
|
124
|
+
end
|
|
125
|
+
private_class_method :materialize_path
|
|
126
|
+
|
|
127
|
+
def ensure_parent(value, path)
|
|
128
|
+
current = value
|
|
129
|
+
path[0...-1].each_with_index do |key, index|
|
|
130
|
+
next_key = path[index + 1]
|
|
131
|
+
case current
|
|
132
|
+
when Array
|
|
133
|
+
key = normalize_array_key(current, key)
|
|
134
|
+
if current[key].nil?
|
|
135
|
+
read_index(nil, next_key)
|
|
136
|
+
current[key] = container_for(next_key)
|
|
137
|
+
end
|
|
138
|
+
current = current[key]
|
|
139
|
+
when Hash
|
|
140
|
+
raise TypeError, cannot_index_message(current, key) unless key.is_a?(String)
|
|
141
|
+
|
|
142
|
+
if current[key].nil?
|
|
143
|
+
read_index(nil, next_key)
|
|
144
|
+
current[key] = container_for(next_key)
|
|
145
|
+
end
|
|
146
|
+
current = current[key]
|
|
147
|
+
else
|
|
148
|
+
raise TypeError, cannot_index_message(current, key)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
current
|
|
152
|
+
end
|
|
153
|
+
private_class_method :ensure_parent
|
|
154
|
+
|
|
155
|
+
def container_for(next_key)
|
|
156
|
+
next_key.is_a?(Numeric) ? [] : {}
|
|
157
|
+
end
|
|
158
|
+
private_class_method :container_for
|
|
159
|
+
|
|
160
|
+
def normalize_array_key(parent, key)
|
|
161
|
+
return key unless parent.is_a?(Array)
|
|
162
|
+
raise TypeError, 'array index must be an integer' unless key.is_a?(Numeric)
|
|
163
|
+
raise TypeError, 'Cannot set array element at NaN index' if key.respond_to?(:nan?) && key.nan?
|
|
164
|
+
raise TypeError, 'Cannot set array element at non-finite index' if key.respond_to?(:finite?) && !key.finite?
|
|
165
|
+
|
|
166
|
+
key = key.to_i
|
|
167
|
+
return key unless key.negative?
|
|
168
|
+
|
|
169
|
+
normalized = parent.length + key
|
|
170
|
+
raise RuntimeError, 'Out of bounds negative array index' if normalized.negative?
|
|
171
|
+
|
|
172
|
+
normalized
|
|
173
|
+
end
|
|
174
|
+
private_class_method :normalize_array_key
|
|
175
|
+
|
|
176
|
+
def read_array(array, key)
|
|
177
|
+
return nil if (key.respond_to?(:nan?) && key.nan?) || (key.respond_to?(:finite?) && !key.finite?)
|
|
178
|
+
|
|
179
|
+
index = key.to_i
|
|
180
|
+
index = array.length + index if index.negative?
|
|
181
|
+
return nil if index.negative? || index >= array.length
|
|
182
|
+
|
|
183
|
+
array[index]
|
|
184
|
+
end
|
|
185
|
+
private_class_method :read_array
|
|
186
|
+
|
|
187
|
+
def slice_component?(key)
|
|
188
|
+
key.is_a?(Hash) && key.keys.all? { |name| %w[start end].include?(name) } &&
|
|
189
|
+
(key.key?('start') || key.key?('end'))
|
|
190
|
+
end
|
|
191
|
+
private_class_method :slice_component?
|
|
192
|
+
|
|
193
|
+
def read_slice(value, component)
|
|
194
|
+
return nil if value.nil?
|
|
195
|
+
|
|
196
|
+
case value
|
|
197
|
+
when Array
|
|
198
|
+
value[slice_range(value.length, component)] || []
|
|
199
|
+
when String
|
|
200
|
+
characters = value.each_char.to_a
|
|
201
|
+
characters[slice_range(characters.length, component)].to_a.join
|
|
202
|
+
else
|
|
203
|
+
raise TypeError, "cannot slice #{Value.type_of(value)}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
private_class_method :read_slice
|
|
207
|
+
|
|
208
|
+
def replace_slice(value, component, replacement)
|
|
209
|
+
case value
|
|
210
|
+
when Array
|
|
211
|
+
raise TypeError, 'can only assign an array to an array slice' unless replacement.is_a?(Array)
|
|
212
|
+
|
|
213
|
+
value[slice_range(value.length, component)] = Value.deep_copy(replacement)
|
|
214
|
+
when String
|
|
215
|
+
raise TypeError, 'can only assign a string to a string slice' unless replacement.is_a?(String)
|
|
216
|
+
|
|
217
|
+
characters = value.each_char.to_a
|
|
218
|
+
characters[slice_range(characters.length, component)] = replacement.each_char.to_a
|
|
219
|
+
value.replace(characters.join)
|
|
220
|
+
else
|
|
221
|
+
raise TypeError, "cannot slice #{Value.type_of(value)}"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
private_class_method :replace_slice
|
|
225
|
+
|
|
226
|
+
def slice_range(length, component)
|
|
227
|
+
from = slice_boundary(component['start'], length, :floor, 0)
|
|
228
|
+
to = slice_boundary(component['end'], length, :ceil, length)
|
|
229
|
+
from...to
|
|
230
|
+
end
|
|
231
|
+
private_class_method :slice_range
|
|
232
|
+
|
|
233
|
+
def slice_boundary(value, length, rounding, default)
|
|
234
|
+
return default if value.nil? || (value.respond_to?(:nan?) && value.nan?)
|
|
235
|
+
raise TypeError, 'slice index must be a number' unless value.is_a?(Numeric)
|
|
236
|
+
return value.positive? ? length : 0 if value.respond_to?(:finite?) && !value.finite?
|
|
237
|
+
|
|
238
|
+
index = rounding == :ceil ? value.ceil : value.floor
|
|
239
|
+
index = length + index if value.negative?
|
|
240
|
+
[[index, 0].max, length].min
|
|
241
|
+
end
|
|
242
|
+
private_class_method :slice_boundary
|
|
243
|
+
|
|
244
|
+
def cannot_index_message(value, key)
|
|
245
|
+
key_text = key.is_a?(String) ? "string #{key.inspect}" : Value.type_of(key)
|
|
246
|
+
"Cannot index #{Value.type_of(value)} with #{key_text}"
|
|
247
|
+
end
|
|
248
|
+
private_class_method :cannot_index_message
|
|
249
|
+
end
|
|
250
|
+
end
|
data/lib/rjq/runtime.rb
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
class Runtime
|
|
5
|
+
DEFAULT_OPTIONS = {
|
|
6
|
+
compact: false, raw_output: false, raw_output0: false, join_output: false,
|
|
7
|
+
null_input: false, raw_input: false, slurp: false, ascii: false,
|
|
8
|
+
sort_keys: false, tab: false, indent: 2, seq: false, stream: false,
|
|
9
|
+
stream_errors: false, unbuffered: false, max_call_depth: nil, max_instructions: nil,
|
|
10
|
+
max_outputs: nil, variables: {}
|
|
11
|
+
}.freeze
|
|
12
|
+
OPTION_KEYS = (DEFAULT_OPTIONS.keys + %i[
|
|
13
|
+
allow_comments color current_filename current_line exit_status input_chunk_size input_max_depth input_queue
|
|
14
|
+
jq_origin library_path max_filter_depth max_number_digits max_string_bytes module_resolver regexp_timeout
|
|
15
|
+
max_replay_cache remaining_inputs runtime_error_handler source_path stderr
|
|
16
|
+
]).freeze
|
|
17
|
+
BOOLEAN_OPTIONS = %i[
|
|
18
|
+
allow_comments ascii compact exit_status join_output null_input raw_input raw_output raw_output0 seq slurp
|
|
19
|
+
sort_keys stream stream_errors tab unbuffered
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def validate_options!(opts)
|
|
24
|
+
raise ArgumentError, 'options must be a Hash' unless opts.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
unknown = opts.keys - OPTION_KEYS
|
|
27
|
+
raise ArgumentError, "unknown runtime option: #{unknown.first.inspect}" unless unknown.empty?
|
|
28
|
+
|
|
29
|
+
BOOLEAN_OPTIONS.each { |key| validate_boolean!(opts, key) }
|
|
30
|
+
validate_color!(opts)
|
|
31
|
+
validate_integer!(opts, :indent, minimum: 0, maximum: 7)
|
|
32
|
+
validate_integer!(opts, :input_chunk_size, minimum: 1)
|
|
33
|
+
validate_integer!(opts, :input_max_depth, minimum: 0)
|
|
34
|
+
validate_integer!(opts, :max_call_depth, minimum: 1, optional: true)
|
|
35
|
+
validate_integer!(opts, :max_instructions, minimum: 0, optional: true)
|
|
36
|
+
validate_integer!(opts, :max_replay_cache, minimum: 0, optional: true)
|
|
37
|
+
validate_integer!(opts, :current_line, minimum: 1, optional: true)
|
|
38
|
+
validate_integer!(opts, :max_number_digits, minimum: 0, optional: true)
|
|
39
|
+
validate_integer!(opts, :max_outputs, minimum: 0, optional: true)
|
|
40
|
+
validate_integer!(opts, :max_string_bytes, minimum: 0, optional: true)
|
|
41
|
+
validate_regexp_timeout!(opts)
|
|
42
|
+
validate_stderr!(opts)
|
|
43
|
+
validate_runtime_error_handler!(opts)
|
|
44
|
+
validate_variables!(opts)
|
|
45
|
+
Compiler.validate_options!(Compiler.options_from(opts))
|
|
46
|
+
opts
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def normalize_options(opts)
|
|
50
|
+
validate_options!(opts)
|
|
51
|
+
normalized = opts.dup
|
|
52
|
+
normalized[:variables] = normalized[:variables].dup.freeze if normalized[:variables]
|
|
53
|
+
normalized[:library_path] = normalized[:library_path].dup.freeze if normalized[:library_path]
|
|
54
|
+
normalized.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def validate_boolean!(opts, key)
|
|
60
|
+
return unless opts.key?(key)
|
|
61
|
+
return if opts[key] == true || opts[key] == false
|
|
62
|
+
|
|
63
|
+
raise ArgumentError, "#{key} must be true or false"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_color!(opts)
|
|
67
|
+
return unless opts.key?(:color)
|
|
68
|
+
return if opts[:color].nil? || opts[:color] == true || opts[:color] == false
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, 'color must be true, false, or nil'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_integer!(opts, key, minimum:, maximum: nil, optional: false)
|
|
74
|
+
return unless opts.key?(key)
|
|
75
|
+
return if optional && opts[key].nil?
|
|
76
|
+
|
|
77
|
+
value = opts[key]
|
|
78
|
+
valid = value.is_a?(Integer) && value >= minimum && (!maximum || value <= maximum)
|
|
79
|
+
return if valid
|
|
80
|
+
|
|
81
|
+
range = maximum ? "between #{minimum} and #{maximum}" : "at least #{minimum}"
|
|
82
|
+
raise ArgumentError, "#{key} must be an Integer #{range}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_regexp_timeout!(opts)
|
|
86
|
+
return unless opts.key?(:regexp_timeout)
|
|
87
|
+
|
|
88
|
+
timeout = opts[:regexp_timeout]
|
|
89
|
+
return if timeout.nil? || (timeout.is_a?(Numeric) && timeout.respond_to?(:finite?) && timeout.finite? &&
|
|
90
|
+
timeout.respond_to?(:positive?) && timeout.positive?)
|
|
91
|
+
|
|
92
|
+
raise ArgumentError, 'regexp_timeout must be a finite positive number or nil'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_stderr!(opts)
|
|
96
|
+
return unless opts.key?(:stderr)
|
|
97
|
+
return if opts[:stderr].nil? || opts[:stderr].respond_to?(:puts)
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, 'stderr must be nil or respond to puts'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_runtime_error_handler!(opts)
|
|
103
|
+
return unless opts.key?(:runtime_error_handler)
|
|
104
|
+
return if opts[:runtime_error_handler].nil? || opts[:runtime_error_handler].respond_to?(:call)
|
|
105
|
+
|
|
106
|
+
raise ArgumentError, 'runtime_error_handler must be nil or respond to call'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_variables!(opts)
|
|
110
|
+
return unless opts.key?(:variables)
|
|
111
|
+
return if opts[:variables].is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
raise ArgumentError, 'variables must be a Hash'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
InputRecord = Struct.new(:value, :filename, :line, keyword_init: true)
|
|
119
|
+
|
|
120
|
+
class ResultStream
|
|
121
|
+
include Enumerable
|
|
122
|
+
|
|
123
|
+
def initialize(on_close: nil, &producer)
|
|
124
|
+
@producer = producer
|
|
125
|
+
@on_close = on_close
|
|
126
|
+
@closed = false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def each
|
|
130
|
+
return enum_for(:each) unless block_given?
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
@producer.call(->(value) { yield value })
|
|
134
|
+
ensure
|
|
135
|
+
close
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def close
|
|
140
|
+
return if @closed
|
|
141
|
+
|
|
142
|
+
@closed = true
|
|
143
|
+
@on_close&.call
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
class InputQueue
|
|
148
|
+
END_OF_INPUT = Object.new.freeze
|
|
149
|
+
|
|
150
|
+
attr_reader :current_record
|
|
151
|
+
|
|
152
|
+
def initialize(records)
|
|
153
|
+
@records = records.to_enum
|
|
154
|
+
@next_record = nil
|
|
155
|
+
@current_record = nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def empty?
|
|
159
|
+
peek.equal?(END_OF_INPUT)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def shift_record
|
|
163
|
+
record = peek
|
|
164
|
+
return if record.equal?(END_OF_INPUT)
|
|
165
|
+
|
|
166
|
+
@next_record = nil
|
|
167
|
+
@current_record = record
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def shift
|
|
171
|
+
shift_record&.value
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def each_remaining
|
|
175
|
+
return enum_for(:each_remaining) unless block_given?
|
|
176
|
+
|
|
177
|
+
yield shift until empty?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def remaining_values
|
|
181
|
+
each_remaining.to_a
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def peek
|
|
187
|
+
return @next_record if @next_record
|
|
188
|
+
|
|
189
|
+
@next_record = @records.next
|
|
190
|
+
rescue StopIteration
|
|
191
|
+
@next_record = END_OF_INPUT
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def initialize(filter_string, opts = {})
|
|
196
|
+
@filter_string = filter_string || '.'
|
|
197
|
+
@opts = self.class.normalize_options(DEFAULT_OPTIONS.merge(opts))
|
|
198
|
+
@program = Rjq.compile(@filter_string, Compiler.options_from(@opts))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def run_values(values, input_queue: nil)
|
|
202
|
+
records = values.lazy.map { |value| InputRecord.new(value: value, filename: nil, line: 1) }
|
|
203
|
+
queue = InputQueue.new(records)
|
|
204
|
+
builtin_queue = input_queue.is_a?(InputQueue) ? input_queue : queue
|
|
205
|
+
run_queue(queue, builtin_queue)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def run_stream(io, &block)
|
|
209
|
+
enum = run_io_streams([[io, nil, false]])
|
|
210
|
+
return enum unless block
|
|
211
|
+
|
|
212
|
+
enum.each(&block)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def run_io_streams(streams)
|
|
216
|
+
owned_streams = []
|
|
217
|
+
records = records_from_streams(streams, owned_streams)
|
|
218
|
+
close_streams = lambda do
|
|
219
|
+
owned_streams.each { |io| io.close unless io.closed? }
|
|
220
|
+
end
|
|
221
|
+
if @opts[:null_input]
|
|
222
|
+
main = InputQueue.new([InputRecord.new(value: nil, filename: nil, line: 1)])
|
|
223
|
+
return run_queue(main, InputQueue.new(records), on_close: close_streams)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
run_queue(InputQueue.new(records), on_close: close_streams)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def format_output(value)
|
|
230
|
+
return value if @opts[:raw_output] && value.is_a?(String)
|
|
231
|
+
|
|
232
|
+
indent = @opts[:compact] ? nil : @opts[:indent]
|
|
233
|
+
dumped = JSON::Dumper.dump(value, indent: indent, sort_keys: @opts[:sort_keys], ascii: @opts[:ascii],
|
|
234
|
+
tab: @opts[:tab])
|
|
235
|
+
@opts[:color] ? Color.colorize(dumped) : dumped
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def write_output(value, io)
|
|
239
|
+
if @opts[:raw_output] && value.is_a?(String)
|
|
240
|
+
io << value
|
|
241
|
+
elsif @opts[:color]
|
|
242
|
+
io << format_output(value)
|
|
243
|
+
else
|
|
244
|
+
indent = @opts[:compact] ? nil : @opts[:indent]
|
|
245
|
+
JSON::Dumper.dump(value, indent: indent, sort_keys: @opts[:sort_keys], ascii: @opts[:ascii],
|
|
246
|
+
tab: @opts[:tab], io: io)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
def run_queue(queue, builtin_queue = queue, on_close: nil)
|
|
253
|
+
ResultStream.new(on_close: on_close) do |emit|
|
|
254
|
+
output_count = 0
|
|
255
|
+
instruction_budget = VM::InstructionBudget.new(@opts[:max_instructions])
|
|
256
|
+
until queue.empty?
|
|
257
|
+
record = queue.shift_record
|
|
258
|
+
run_opts = @opts.merge(
|
|
259
|
+
variables: @opts.fetch(:variables, {}), input_queue: builtin_queue,
|
|
260
|
+
remaining_inputs: builtin_queue, current_filename: display_filename(record.filename),
|
|
261
|
+
current_line: record.line
|
|
262
|
+
)
|
|
263
|
+
begin
|
|
264
|
+
@program.run_with_instruction_budget(record.value, run_opts, instruction_budget).each do |result|
|
|
265
|
+
output_count += 1
|
|
266
|
+
max_outputs = @opts[:max_outputs]
|
|
267
|
+
raise RuntimeError, "output limit exceeded (#{max_outputs})" if max_outputs && output_count > max_outputs
|
|
268
|
+
|
|
269
|
+
emit.call(result)
|
|
270
|
+
end
|
|
271
|
+
rescue ResourceLimitError
|
|
272
|
+
raise
|
|
273
|
+
rescue Rjq::RuntimeError => e
|
|
274
|
+
handler = @opts[:runtime_error_handler]
|
|
275
|
+
raise unless handler
|
|
276
|
+
|
|
277
|
+
handler.call(e, record)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def records_from_streams(streams, owned_streams)
|
|
284
|
+
return raw_slurp_record(streams, owned_streams) if @opts[:slurp] && @opts[:raw_input]
|
|
285
|
+
|
|
286
|
+
records = uncollected_records(streams, owned_streams)
|
|
287
|
+
return records unless @opts[:slurp]
|
|
288
|
+
|
|
289
|
+
Enumerator.new do |yielder|
|
|
290
|
+
collected = records.to_a
|
|
291
|
+
yielder << InputRecord.new(value: collected.map(&:value), filename: collected.last&.filename, line: 1)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def raw_slurp_record(streams, owned_streams)
|
|
296
|
+
Enumerator.new do |yielder|
|
|
297
|
+
content = +''.b
|
|
298
|
+
last_filename = nil
|
|
299
|
+
streams.each do |io, filename, close_after|
|
|
300
|
+
owned_streams << io if close_after && !owned_streams.include?(io)
|
|
301
|
+
begin
|
|
302
|
+
while (chunk = io.read(input_chunk_size))
|
|
303
|
+
break if chunk.empty?
|
|
304
|
+
|
|
305
|
+
content << chunk.b
|
|
306
|
+
end
|
|
307
|
+
last_filename = filename
|
|
308
|
+
ensure
|
|
309
|
+
io.close if close_after && !io.closed?
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
content.force_encoding(Encoding::UTF_8)
|
|
313
|
+
yielder << InputRecord.new(value: content, filename: last_filename, line: 1)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def uncollected_records(streams, owned_streams)
|
|
318
|
+
Enumerator.new do |yielder|
|
|
319
|
+
streams.each do |io, filename, close_after|
|
|
320
|
+
owned_streams << io if close_after && !owned_streams.include?(io)
|
|
321
|
+
begin
|
|
322
|
+
records_for_io(io, filename).each { |record| yielder << record }
|
|
323
|
+
ensure
|
|
324
|
+
io.close if close_after && !io.closed?
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def records_for_io(io, filename)
|
|
331
|
+
return raw_records(io, filename) if @opts[:raw_input]
|
|
332
|
+
return stream_records(io, filename) if @opts[:stream]
|
|
333
|
+
|
|
334
|
+
JSON::Parser.parse_records(io, seq: @opts[:seq], chunk_size: input_chunk_size,
|
|
335
|
+
on_error: method(:warn_ignored_parse_error),
|
|
336
|
+
max_depth: input_max_depth, max_number_digits: @opts[:max_number_digits],
|
|
337
|
+
max_string_bytes: @opts[:max_string_bytes]).lazy.map do |parsed|
|
|
338
|
+
InputRecord.new(value: parsed.value, filename: filename, line: parsed.line)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def raw_records(io, filename)
|
|
343
|
+
Enumerator.new do |yielder|
|
|
344
|
+
io.each_line.with_index(1) do |line, line_number|
|
|
345
|
+
value = line.delete_suffix("\n").delete_suffix("\r")
|
|
346
|
+
yielder << InputRecord.new(value: value, filename: filename, line: line_number)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def stream_records(io, filename)
|
|
352
|
+
JSON::StreamParser.parse(io, seq: @opts[:seq], stream_errors: @opts[:stream_errors],
|
|
353
|
+
chunk_size: input_chunk_size,
|
|
354
|
+
on_error: method(:warn_ignored_parse_error),
|
|
355
|
+
max_depth: input_max_depth, max_number_digits: @opts[:max_number_digits],
|
|
356
|
+
max_string_bytes: @opts[:max_string_bytes], locations: true).lazy.map do |parsed|
|
|
357
|
+
InputRecord.new(value: parsed.value, filename: filename, line: parsed.line)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def input_chunk_size
|
|
362
|
+
@opts.fetch(:input_chunk_size, JSON::InputBuffer::DEFAULT_CHUNK_SIZE)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def input_max_depth
|
|
366
|
+
@opts.fetch(:input_max_depth, JSON::Parser::DEFAULT_MAX_DEPTH)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def display_filename(filename)
|
|
370
|
+
filename || '<stdin>'
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def warn_ignored_parse_error(message)
|
|
374
|
+
(@opts[:stderr] || $stderr).puts("rjq: ignoring parse error: #{message}")
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|