jmespath-ruby 0.1.3 → 1.0.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 +4 -4
- data/CHANGELOG.md +18 -1
- data/README.md +76 -214
- data/completions/_jmespath +9 -0
- data/completions/jmespath.bash +13 -0
- data/docs/guides/command-line.md +233 -0
- data/docs/guides/compilation.md +2 -1
- data/docs/guides/completion.md +5 -0
- data/lib/jmespath/cli/arguments.rb +5 -8
- data/lib/jmespath/cli/help.rb +115 -33
- data/lib/jmespath/cli/inspection_report.rb +76 -0
- data/lib/jmespath/cli/interactive_commands.rb +58 -0
- data/lib/jmespath/cli/interactive_completion.rb +93 -0
- data/lib/jmespath/cli/interactive_editor.rb +129 -0
- data/lib/jmespath/cli/interactive_editor_state.rb +52 -0
- data/lib/jmespath/cli/interactive_help.rb +36 -0
- data/lib/jmespath/cli/interactive_key_stroke.rb +80 -0
- data/lib/jmespath/cli/interactive_line_editor.rb +11 -0
- data/lib/jmespath/cli/interactive_session.rb +35 -9
- data/lib/jmespath/cli.rb +22 -11
- data/lib/jmespath/version.rb +1 -1
- metadata +24 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
# @api private
|
|
6
|
+
class InteractiveCompletion
|
|
7
|
+
def initialize(functions: Functions.default)
|
|
8
|
+
@names = functions.names.sort
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def complete(line, point:)
|
|
12
|
+
# @type var unchanged: completion_result
|
|
13
|
+
unchanged = { line: line, point: point, candidates: [] }
|
|
14
|
+
token = token(line, point)
|
|
15
|
+
return unchanged unless token
|
|
16
|
+
|
|
17
|
+
prefix, start, finish = token
|
|
18
|
+
names = @names.select { |name| name.start_with?(prefix) }
|
|
19
|
+
unchanged[:candidates] = names
|
|
20
|
+
return unchanged if names.empty?
|
|
21
|
+
|
|
22
|
+
common = common_prefix(names)
|
|
23
|
+
return unchanged if names.length > 1 && common == prefix
|
|
24
|
+
|
|
25
|
+
{ line: replace(line, start, finish, common),
|
|
26
|
+
point: start + common.bytesize, candidates: names }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def replace(line, start, finish, common)
|
|
32
|
+
line.byteslice(0, start).to_s + common + line.byteslice(finish..).to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def token(line, point)
|
|
36
|
+
return unless valid_cursor?(line, point)
|
|
37
|
+
|
|
38
|
+
before = line.byteslice(0, point).to_s
|
|
39
|
+
return if quoted?(before)
|
|
40
|
+
|
|
41
|
+
prefix = identifier_prefix(before)
|
|
42
|
+
return unless prefix&.match?(/\A[A-Za-z_]/)
|
|
43
|
+
|
|
44
|
+
suffix = line.byteslice(point..).to_s[/\A[A-Za-z0-9_]*/].to_s
|
|
45
|
+
[prefix, point - prefix.bytesize, point + suffix.bytesize]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def identifier_prefix(before)
|
|
49
|
+
before.reverse[/\A[A-Za-z0-9_]+/]&.reverse
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def valid_cursor?(line, point)
|
|
53
|
+
source = line.dup.force_encoding(Encoding::UTF_8)
|
|
54
|
+
return false unless source.valid_encoding? && point.between?(0, source.bytesize)
|
|
55
|
+
return false unless source.byteslice(0, point).to_s.valid_encoding?
|
|
56
|
+
return false if source.include?("\n")
|
|
57
|
+
|
|
58
|
+
query?(source, point)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def query?(line, point)
|
|
62
|
+
return true unless line.match?(/\A[ \t]*:/)
|
|
63
|
+
|
|
64
|
+
command = /\A[ \t]*:(?:explain|trace)[ \t]+/.match(line)
|
|
65
|
+
return false unless command
|
|
66
|
+
|
|
67
|
+
point >= command[0].bytesize
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def quoted?(before)
|
|
71
|
+
index = 0
|
|
72
|
+
quote = nil
|
|
73
|
+
while index < before.bytesize
|
|
74
|
+
byte = before.getbyte(index)
|
|
75
|
+
if quote
|
|
76
|
+
index += 1 if byte == 92
|
|
77
|
+
quote = nil if byte == quote
|
|
78
|
+
elsif [34, 39, 96].include?(byte)
|
|
79
|
+
quote = byte
|
|
80
|
+
end
|
|
81
|
+
index += 1
|
|
82
|
+
end
|
|
83
|
+
!quote.nil?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def common_prefix(names)
|
|
87
|
+
common = names.fetch(0)
|
|
88
|
+
common = common[0...-1] until names.fetch(-1).start_with?(common)
|
|
89
|
+
common
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "interactive_editor_state"
|
|
4
|
+
require_relative "interactive_completion"
|
|
5
|
+
|
|
6
|
+
module JMESPath
|
|
7
|
+
module CLI
|
|
8
|
+
# @api private
|
|
9
|
+
class InteractiveEditor
|
|
10
|
+
HISTORY_LIMIT = 500
|
|
11
|
+
|
|
12
|
+
def self.supported?(input:, errors:)
|
|
13
|
+
return false unless terminal_environment?
|
|
14
|
+
|
|
15
|
+
return false unless [input, errors].all? { |stream| stream.is_a?(IO) && !stream.closed? && stream.tty? }
|
|
16
|
+
|
|
17
|
+
require "io/console"
|
|
18
|
+
backend_supported?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.backend_supported?
|
|
22
|
+
require "reline"
|
|
23
|
+
backend = Reline::IOGate
|
|
24
|
+
!backend.dumb? && backend.respond_to?(:input=) && backend.respond_to?(:output=)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.terminal_environment?
|
|
28
|
+
![nil, "", "dumb"].include?(ENV.fetch("TERM", nil)) && !ENV.key?("RELINE_STDERR_TTY")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(input:, errors:)
|
|
32
|
+
require "reline"
|
|
33
|
+
require_relative "interactive_key_stroke"
|
|
34
|
+
require_relative "interactive_line_editor"
|
|
35
|
+
@input = input
|
|
36
|
+
@errors = errors
|
|
37
|
+
@history = []
|
|
38
|
+
@pending = []
|
|
39
|
+
@completion = InteractiveCompletion.new
|
|
40
|
+
@writing_errors = false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def open
|
|
44
|
+
state = InteractiveEditorState.new
|
|
45
|
+
begin
|
|
46
|
+
state.configure(@input, @errors)
|
|
47
|
+
Reline.completion_proc = method(:complete)
|
|
48
|
+
yield
|
|
49
|
+
ensure
|
|
50
|
+
state.restore
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def gets
|
|
55
|
+
return @pending.shift unless @pending.empty?
|
|
56
|
+
|
|
57
|
+
read_line
|
|
58
|
+
rescue Interrupt
|
|
59
|
+
@errors.puts("^C")
|
|
60
|
+
retry
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remember(query)
|
|
64
|
+
@history << query
|
|
65
|
+
@history.shift if @history.length > HISTORY_LIMIT
|
|
66
|
+
Reline::HISTORY.replace(@history)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def writing_errors?
|
|
70
|
+
@writing_errors
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def read_line
|
|
76
|
+
@writing_errors = true
|
|
77
|
+
line = Reline.readline("jmespath> ", false)
|
|
78
|
+
@writing_errors = false
|
|
79
|
+
return unless line
|
|
80
|
+
|
|
81
|
+
lines = Reline.core.line_editor.whole_lines
|
|
82
|
+
return line if lines.length == 1
|
|
83
|
+
|
|
84
|
+
@pending = lines
|
|
85
|
+
@pending.shift
|
|
86
|
+
rescue InteractiveKeyStroke::InvalidInput => e
|
|
87
|
+
reject_invalid(e)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reject_invalid(error)
|
|
91
|
+
terminator = error.ended? ? error.byte : discard_line
|
|
92
|
+
@errors.puts("")
|
|
93
|
+
@writing_errors = false
|
|
94
|
+
return if terminator.nil? || terminator == 4
|
|
95
|
+
|
|
96
|
+
"\xFF".b
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def discard_line
|
|
100
|
+
# @type var input: Object & _RawInput
|
|
101
|
+
input = @input
|
|
102
|
+
input.raw(intr: true) do
|
|
103
|
+
loop do
|
|
104
|
+
byte = Reline::IOGate.getc(Float::INFINITY)
|
|
105
|
+
break byte if byte.nil? || [4, 10, 13].include?(byte)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def complete(_target)
|
|
111
|
+
line = Reline.line_buffer
|
|
112
|
+
result = @completion.complete(line, point: Reline.point)
|
|
113
|
+
if result[:line] != line
|
|
114
|
+
Reline.delete_text
|
|
115
|
+
Reline.insert_text(result[:line])
|
|
116
|
+
Reline.point = result[:point]
|
|
117
|
+
elsif result[:candidates].length > 1
|
|
118
|
+
list_candidates(result[:candidates])
|
|
119
|
+
end
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def list_candidates(candidates)
|
|
124
|
+
Reline.core.line_editor.render_finished
|
|
125
|
+
@errors.puts(candidates.join(" "))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
# @api private
|
|
6
|
+
class InteractiveEditorState
|
|
7
|
+
PROPERTIES = %i[completion_proc output_modifier_proc prompt_proc auto_indent_proc pre_input_hook
|
|
8
|
+
dig_perfect_match_proc last_incremental_search].freeze
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@core = Reline.core
|
|
12
|
+
# Reline 0.7 exposes only input=; retain the previous reader for restoration.
|
|
13
|
+
@input = Reline::IOGate.instance_variable_get(:@input)
|
|
14
|
+
@output = @core.output
|
|
15
|
+
@configuration = [@core.config, @core.key_stroke, @core.line_editor]
|
|
16
|
+
@callbacks = PROPERTIES.to_h { |key| [key, Reline.public_send(key)] }
|
|
17
|
+
@history = Reline::HISTORY.to_a
|
|
18
|
+
@history_size = @core.config.history_size
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configure(input, errors)
|
|
22
|
+
Reline.input = input
|
|
23
|
+
Reline.output = errors
|
|
24
|
+
@core.config.history_size = InteractiveEditor::HISTORY_LIMIT
|
|
25
|
+
configure_core
|
|
26
|
+
PROPERTIES.each { |key| Reline.public_send(:"#{key}=", nil) }
|
|
27
|
+
Reline::HISTORY.clear
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configure_core
|
|
31
|
+
config = Reline::Config.new
|
|
32
|
+
config.read
|
|
33
|
+
config.autocompletion = false
|
|
34
|
+
config.disable_completion = false
|
|
35
|
+
config.history_size = InteractiveEditor::HISTORY_LIMIT
|
|
36
|
+
Reline::IOGate.set_default_key_bindings(config)
|
|
37
|
+
@core.config = config
|
|
38
|
+
@core.key_stroke = InteractiveKeyStroke.new(config, Reline::IOGate.encoding)
|
|
39
|
+
@core.line_editor = InteractiveLineEditor.new(config)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def restore
|
|
43
|
+
Reline.input = @input
|
|
44
|
+
Reline.output = @output
|
|
45
|
+
@core.config, @core.key_stroke, @core.line_editor = @configuration
|
|
46
|
+
@core.config.history_size = @history_size
|
|
47
|
+
@callbacks.each { |key, value| Reline.public_send(:"#{key}=", value) }
|
|
48
|
+
Reline::HISTORY.replace(@history)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
# @api private
|
|
6
|
+
module InteractiveHelp
|
|
7
|
+
TEXT = <<~TEXT
|
|
8
|
+
Interactive: jmespath --interactive --file PATH
|
|
9
|
+
jmespath --interactive --file document.json
|
|
10
|
+
Enter one UTF-8 query per line; empty lines and lines containing only spaces or tabs are ignored.
|
|
11
|
+
Queries accept LF, CRLF, and a final line without a newline.
|
|
12
|
+
:help shows session help; :quit or EOF exits with status 0, even after query failures.
|
|
13
|
+
:functions lists built-in names, signatures, and descriptions on stderr.
|
|
14
|
+
:explain QUERY shows the query's operation tree and source locations on stderr without evaluating data.
|
|
15
|
+
:trace QUERY evaluates once, writes events to stderr, and writes one compact JSON result to stdout.
|
|
16
|
+
Traces retain at most 1000 events and report truncation after evaluation completes.
|
|
17
|
+
Query errors, unknown commands, and invalid UTF-8 lines report diagnostics; the session continues.
|
|
18
|
+
Failed queries print no result. Successful queries print compact JSON and a newline to stdout.
|
|
19
|
+
Prompts, help, and diagnostics use stderr. Prompts appear only when stdin is a terminal.
|
|
20
|
+
Results flush after each query when supported; piped query input produces no prompts.
|
|
21
|
+
The document loads once and its file closes before the first prompt or query read.
|
|
22
|
+
Interactive mode requires --file and rejects query sources, other modes, information flags, and repeats.
|
|
23
|
+
Startup or stream failures exit with status 1; usage failures return 2 before reading input.
|
|
24
|
+
CLI.run input needs gets; tty? is optional and absent detection means no prompts.
|
|
25
|
+
Writers need puts; output flush is optional. Terminal prompts require stderr print and flush.
|
|
26
|
+
Supported terminals provide editing and recall of the last 500 queries for this session only.
|
|
27
|
+
Ctrl-C cancels the current edit. History includes failed queries, excludes commands, and creates no files.
|
|
28
|
+
Editing requires terminal IO on stdin and stderr and a usable TERM; other streams use a plain reader.
|
|
29
|
+
Tab completes built-in function names outside quoted identifiers and literals, preserving surrounding text.
|
|
30
|
+
Unique matches insert only a name. Ambiguous matches extend a common prefix or list sorted alternatives.
|
|
31
|
+
Empty or unknown prefixes remain unchanged. Caller-owned streams remain open.
|
|
32
|
+
Multiline editing and document reloads are unavailable.
|
|
33
|
+
TEXT
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
# @api private
|
|
6
|
+
class InteractiveKeyStroke < Reline::KeyStroke
|
|
7
|
+
# @api private
|
|
8
|
+
class InvalidInput < StandardError
|
|
9
|
+
attr_reader :byte
|
|
10
|
+
|
|
11
|
+
def initialize(byte)
|
|
12
|
+
@byte = byte
|
|
13
|
+
super("Invalid UTF-8 query")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ended?
|
|
17
|
+
[4, 10, 13].include?(@byte)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def match_status(input)
|
|
22
|
+
validate_prefix(input)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def expand(input)
|
|
27
|
+
keys, rest = super
|
|
28
|
+
keys = keys.map do |key|
|
|
29
|
+
case key.method_symbol
|
|
30
|
+
when :bracketed_paste_start
|
|
31
|
+
Reline::Key.new(read_paste, :insert_multiline_text)
|
|
32
|
+
when :quoted_insert, :ed_quoted_insert
|
|
33
|
+
Reline::Key.new(read_quoted, :insert_raw_char)
|
|
34
|
+
else
|
|
35
|
+
key
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
[keys, rest]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def validate_prefix(input)
|
|
44
|
+
decoder = Encoding::Converter.new("UTF-8", "UTF-16LE")
|
|
45
|
+
source = input.pack("C*")
|
|
46
|
+
status = decoder.primitive_convert(source, +"", nil, nil, Encoding::Converter::PARTIAL_INPUT)
|
|
47
|
+
raise InvalidInput, input.fetch(-1) unless status == :source_buffer_empty
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read_paste
|
|
51
|
+
marker = "\e[201~".b
|
|
52
|
+
buffer = +"".b
|
|
53
|
+
until buffer.end_with?(marker)
|
|
54
|
+
byte = Reline::IOGate.getc(Float::INFINITY)
|
|
55
|
+
break unless byte
|
|
56
|
+
|
|
57
|
+
buffer << byte
|
|
58
|
+
end
|
|
59
|
+
text = buffer.delete_suffix(marker).force_encoding(Encoding::UTF_8)
|
|
60
|
+
raise InvalidInput, 255 unless text.valid_encoding?
|
|
61
|
+
|
|
62
|
+
text
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def read_quoted
|
|
66
|
+
# @type var bytes: Array[Integer]
|
|
67
|
+
bytes = []
|
|
68
|
+
loop do
|
|
69
|
+
byte = Reline::IOGate.getc(Float::INFINITY)
|
|
70
|
+
return "" unless byte
|
|
71
|
+
|
|
72
|
+
bytes << byte
|
|
73
|
+
validate_prefix(bytes)
|
|
74
|
+
text = bytes.pack("C*").force_encoding(Encoding::UTF_8)
|
|
75
|
+
return text if text.valid_encoding?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
# @api private
|
|
6
|
+
class InteractiveLineEditor < Reline::LineEditor
|
|
7
|
+
# Dialogs registered by another Reline consumer do not belong to this session.
|
|
8
|
+
def add_dialog_proc(_name, _callback, _context = nil); end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -4,7 +4,13 @@ module JMESPath
|
|
|
4
4
|
module CLI
|
|
5
5
|
# @api private
|
|
6
6
|
class InteractiveSession
|
|
7
|
-
HELP = "Enter one query per line; blank lines are ignored.\n:help shows help; :quit ends the session
|
|
7
|
+
HELP = "Enter one query per line; blank lines are ignored.\n:help shows help; :quit ends the session.\n" \
|
|
8
|
+
":functions lists built-ins; :explain QUERY shows structure without evaluating the document.\n" \
|
|
9
|
+
":trace QUERY evaluates once; at most 1000 events use stderr and one JSON result uses stdout.\n" \
|
|
10
|
+
"Supported terminals provide editing and recall of the last 500 queries for this session only.\n" \
|
|
11
|
+
"Ctrl-C cancels the current edit. Other streams use the plain line reader.\n" \
|
|
12
|
+
"Tab completes function names outside quotes; ambiguous prefixes list sorted names.\n" \
|
|
13
|
+
"Prompts and diagnostics use stderr; successful JSON results use stdout."
|
|
8
14
|
|
|
9
15
|
def initialize(data, input:, output:, errors:)
|
|
10
16
|
@data = data
|
|
@@ -15,13 +21,13 @@ module JMESPath
|
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
def run
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if InteractiveEditor.supported?(input: @input, errors: @errors)
|
|
25
|
+
editor = InteractiveEditor.new(input: @input, errors: @errors)
|
|
26
|
+
@editor = editor
|
|
27
|
+
editor.open { read_queries }
|
|
28
|
+
else
|
|
29
|
+
read_queries
|
|
23
30
|
end
|
|
24
|
-
0
|
|
25
31
|
rescue IOError, SystemCallError => e
|
|
26
32
|
report_stream_failure(e)
|
|
27
33
|
1
|
|
@@ -29,6 +35,17 @@ module JMESPath
|
|
|
29
35
|
|
|
30
36
|
private
|
|
31
37
|
|
|
38
|
+
def read_queries
|
|
39
|
+
loop do
|
|
40
|
+
prompt unless @editor
|
|
41
|
+
editor = @editor
|
|
42
|
+
line = editor ? editor.gets : @input.gets
|
|
43
|
+
break unless line
|
|
44
|
+
break if process?(line)
|
|
45
|
+
end
|
|
46
|
+
0
|
|
47
|
+
end
|
|
48
|
+
|
|
32
49
|
def process?(line)
|
|
33
50
|
expression = line.dup.force_encoding(Encoding::UTF_8)
|
|
34
51
|
unless expression.valid_encoding?
|
|
@@ -47,14 +64,21 @@ module JMESPath
|
|
|
47
64
|
if command == ":help"
|
|
48
65
|
diagnostic(HELP)
|
|
49
66
|
elsif command.start_with?(":")
|
|
50
|
-
|
|
67
|
+
execute_command(expression)
|
|
51
68
|
else
|
|
69
|
+
@editor&.remember(expression)
|
|
52
70
|
result = evaluate(expression)
|
|
53
71
|
write_result(result) if result
|
|
54
72
|
end
|
|
55
73
|
false
|
|
56
74
|
end
|
|
57
75
|
|
|
76
|
+
def execute_command(expression)
|
|
77
|
+
report, result = InteractiveCommands.call(expression, data: @data)
|
|
78
|
+
diagnostic(report)
|
|
79
|
+
write_result(result) if result
|
|
80
|
+
end
|
|
81
|
+
|
|
58
82
|
def evaluate(expression)
|
|
59
83
|
JSON.generate(JMESPath.search(expression, @data))
|
|
60
84
|
rescue JSON::NestingError
|
|
@@ -97,7 +121,9 @@ module JMESPath
|
|
|
97
121
|
end
|
|
98
122
|
|
|
99
123
|
def report_stream_failure(error)
|
|
100
|
-
|
|
124
|
+
return if @writing_errors || @editor&.writing_errors?
|
|
125
|
+
|
|
126
|
+
diagnostic(FailureDiagnostic.format(error, nil))
|
|
101
127
|
rescue IOError, SystemCallError
|
|
102
128
|
nil
|
|
103
129
|
end
|
data/lib/jmespath/cli.rb
CHANGED
|
@@ -4,6 +4,8 @@ require_relative "../jmespath"
|
|
|
4
4
|
require_relative "cli/failure_diagnostic"
|
|
5
5
|
require_relative "cli/arguments"
|
|
6
6
|
require_relative "cli/function_listing"
|
|
7
|
+
require_relative "cli/interactive_commands"
|
|
8
|
+
require_relative "cli/interactive_editor"
|
|
7
9
|
require_relative "cli/interactive_session"
|
|
8
10
|
require_relative "cli/help"
|
|
9
11
|
|
|
@@ -17,11 +19,12 @@ module JMESPath
|
|
|
17
19
|
# @return [Integer] zero on success, one on input failure, two on usage failure
|
|
18
20
|
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
19
21
|
return validation(argv, stderr) if literal_validation?(argv)
|
|
20
|
-
return
|
|
22
|
+
return help(argv, stdout) if %w[--help -h].include?(argv.first)
|
|
23
|
+
return information(argv, stdout) if [%w[--version], %w[--functions]].include?(argv)
|
|
21
24
|
|
|
22
25
|
query(argv, stdin, stdout, stderr)
|
|
23
26
|
rescue OptionParser::ParseError
|
|
24
|
-
stderr.puts(USAGE)
|
|
27
|
+
stderr.puts("#{USAGE}\nUse jmespath --help for options and topic help.")
|
|
25
28
|
2
|
|
26
29
|
rescue JSON::ParserError, Errors::Error, IOError, SystemCallError => e
|
|
27
30
|
failure(e, nil, stderr)
|
|
@@ -29,18 +32,18 @@ module JMESPath
|
|
|
29
32
|
|
|
30
33
|
# @api private
|
|
31
34
|
def self.query(argv, stdin, stdout, stderr)
|
|
32
|
-
expression, path, query_path,
|
|
33
|
-
return interactive_session(path, stdin, stdout, stderr) if interactive
|
|
35
|
+
expression, path, query_path, mode = parse_arguments(argv)
|
|
36
|
+
return interactive_session(path, stdin, stdout, stderr) if mode == "interactive"
|
|
34
37
|
|
|
35
38
|
expression = read_query(expression, query_path)
|
|
36
|
-
return validation(["--validate", expression], stderr) if validate
|
|
37
|
-
if
|
|
39
|
+
return validation(["--validate", expression], stderr) if mode == "validate"
|
|
40
|
+
if mode == "json-lines"
|
|
38
41
|
# @type var stdin: _LineReader
|
|
39
42
|
return execute_lines(expression, path, stdin, stdout, stderr)
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
# @type var stdin: _Reader
|
|
43
|
-
execute(expression, read_document(path, stdin), stdout,
|
|
46
|
+
execute(expression, read_document(path, stdin), stdout, mode)
|
|
44
47
|
rescue JSON::ParserError, Errors::Error => e
|
|
45
48
|
failure(e, expression, stderr)
|
|
46
49
|
end
|
|
@@ -75,12 +78,12 @@ module JMESPath
|
|
|
75
78
|
end
|
|
76
79
|
|
|
77
80
|
# @api private
|
|
78
|
-
def self.execute(expression, data, stdout,
|
|
81
|
+
def self.execute(expression, data, stdout, mode)
|
|
79
82
|
result = JMESPath.search(expression, data)
|
|
80
|
-
output = if
|
|
83
|
+
output = if mode == "raw-output" && result.is_a?(String)
|
|
81
84
|
"#{result}\n"
|
|
82
85
|
else
|
|
83
|
-
pretty ? JSON.pretty_generate(result) : JSON.generate(result)
|
|
86
|
+
mode == "pretty" ? JSON.pretty_generate(result) : JSON.generate(result)
|
|
84
87
|
end
|
|
85
88
|
stdout.puts(output)
|
|
86
89
|
0
|
|
@@ -123,6 +126,14 @@ module JMESPath
|
|
|
123
126
|
1
|
|
124
127
|
end
|
|
125
128
|
|
|
129
|
+
# @api private
|
|
130
|
+
def self.help(argv, output)
|
|
131
|
+
raise OptionParser::InvalidArgument, "expected at most one help topic" if argv.length > 2
|
|
132
|
+
|
|
133
|
+
output.puts(Help.render(argv[1]))
|
|
134
|
+
0
|
|
135
|
+
end
|
|
136
|
+
|
|
126
137
|
# @api private
|
|
127
138
|
def self.information(argv, output)
|
|
128
139
|
if argv == ["--functions"]
|
|
@@ -130,7 +141,7 @@ module JMESPath
|
|
|
130
141
|
return 0
|
|
131
142
|
end
|
|
132
143
|
|
|
133
|
-
output.puts(
|
|
144
|
+
output.puts("jmespath-ruby #{VERSION}")
|
|
134
145
|
0
|
|
135
146
|
end
|
|
136
147
|
end
|
data/lib/jmespath/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jmespath-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leonid Svyatov
|
|
@@ -29,6 +29,20 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '4'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: reline
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 0.7.0
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.7.0
|
|
32
46
|
- !ruby/object:Gem::Dependency
|
|
33
47
|
name: stringio
|
|
34
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +80,7 @@ files:
|
|
|
66
80
|
- VERSIONING.md
|
|
67
81
|
- completions/_jmespath
|
|
68
82
|
- completions/jmespath.bash
|
|
83
|
+
- docs/guides/command-line.md
|
|
69
84
|
- docs/guides/comparison.md
|
|
70
85
|
- docs/guides/compatibility.md
|
|
71
86
|
- docs/guides/compilation.md
|
|
@@ -85,6 +100,14 @@ files:
|
|
|
85
100
|
- lib/jmespath/cli/failure_diagnostic.rb
|
|
86
101
|
- lib/jmespath/cli/function_listing.rb
|
|
87
102
|
- lib/jmespath/cli/help.rb
|
|
103
|
+
- lib/jmespath/cli/inspection_report.rb
|
|
104
|
+
- lib/jmespath/cli/interactive_commands.rb
|
|
105
|
+
- lib/jmespath/cli/interactive_completion.rb
|
|
106
|
+
- lib/jmespath/cli/interactive_editor.rb
|
|
107
|
+
- lib/jmespath/cli/interactive_editor_state.rb
|
|
108
|
+
- lib/jmespath/cli/interactive_help.rb
|
|
109
|
+
- lib/jmespath/cli/interactive_key_stroke.rb
|
|
110
|
+
- lib/jmespath/cli/interactive_line_editor.rb
|
|
88
111
|
- lib/jmespath/cli/interactive_session.rb
|
|
89
112
|
- lib/jmespath/data_access.rb
|
|
90
113
|
- lib/jmespath/diagnostic.rb
|