markdown_exec 2.8.5 → 3.0.1
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 +25 -1
- data/Gemfile.lock +1 -1
- data/Rakefile +0 -33
- data/bats/bats.bats +2 -0
- data/bats/block-type-link.bats +1 -1
- data/bats/block-type-ux-allowed.bats +2 -2
- data/bats/block-type-ux-invalid.bats +1 -1
- data/bats/block-type-ux-required-variables.bats +20 -0
- data/bats/block-type-ux-row-format.bats +1 -1
- data/bats/block-type-ux-sources.bats +36 -0
- data/bats/border.bats +1 -1
- data/bats/cli.bats +2 -2
- data/bats/command-substitution-options.bats +14 -0
- data/bats/command-substitution.bats +1 -1
- data/bats/fail.bats +5 -2
- data/bats/import.bats +8 -0
- data/bats/indented-block-type-vars.bats +1 -1
- data/bats/markup.bats +1 -1
- data/bats/option-expansion.bats +8 -0
- data/bats/table-column-truncate.bats +1 -1
- data/bats/test_helper.bash +50 -5
- data/docs/dev/bats-document-configuration.md +1 -1
- data/docs/dev/block-type-ux-allowed.md +5 -7
- data/docs/dev/block-type-ux-auto.md +8 -5
- data/docs/dev/block-type-ux-chained.md +4 -2
- data/docs/dev/block-type-ux-echo-hash.md +6 -7
- data/docs/dev/block-type-ux-echo.md +2 -2
- data/docs/dev/block-type-ux-exec.md +3 -5
- data/docs/dev/block-type-ux-hidden.md +3 -0
- data/docs/dev/{block-type-ux-preconditions.md → block-type-ux-required-variables.md} +2 -3
- data/docs/dev/block-type-ux-row-format.md +3 -4
- data/docs/dev/block-type-ux-sources.md +57 -0
- data/docs/dev/block-type-ux-transform.md +0 -4
- data/docs/dev/command-substitution-options.md +61 -0
- data/docs/dev/indented-block-type-vars.md +1 -0
- data/docs/dev/menu-pagination-indent.md +123 -0
- data/docs/dev/menu-pagination.md +111 -0
- data/docs/dev/option-expansion.md +10 -0
- data/lib/ansi_formatter.rb +2 -0
- data/lib/block_cache.rb +197 -0
- data/lib/cached_nested_file_reader.rb +3 -1
- data/lib/command_result.rb +57 -0
- data/lib/constants.rb +19 -1
- data/lib/error_reporting.rb +38 -0
- data/lib/evaluate_shell_expressions.rb +43 -18
- data/lib/fcb.rb +98 -7
- data/lib/hash_delegator.rb +544 -330
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +136 -45
- data/lib/mdoc.rb +59 -10
- data/lib/menu.src.yml +23 -11
- data/lib/menu.yml +22 -12
- data/lib/value_or_exception.rb +76 -0
- metadata +16 -4
- data/bats/block-type-ux-preconditions.bats +0 -8
@@ -39,6 +39,8 @@ class CachedNestedFileReader
|
|
39
39
|
)
|
40
40
|
end
|
41
41
|
|
42
|
+
# yield each line to the block
|
43
|
+
# return the processed lines
|
42
44
|
def readlines(filename, depth = 0, context: '', import_paths: nil,
|
43
45
|
indention: '', &block)
|
44
46
|
if @file_cache.key?(filename)
|
@@ -70,7 +72,7 @@ class CachedNestedFileReader
|
|
70
72
|
indention: import_indention,
|
71
73
|
&block)
|
72
74
|
else
|
73
|
-
nested_line = NestedLine.new(line, depth, indention)
|
75
|
+
nested_line = NestedLine.new(line, depth, indention, filename, ind)
|
74
76
|
processed_lines.push(nested_line)
|
75
77
|
block&.call(nested_line)
|
76
78
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env -S bundle exec ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# encoding=utf-8
|
5
|
+
|
6
|
+
# Encapsulates the result of executing a system command, storing its output,
|
7
|
+
# exit status, and any number of additional, arbitrary attributes.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# result = CommandResult.new(stdout: output, exit_status: $?.exitstatus, duration: 1.23)
|
11
|
+
# result.stdout # => output
|
12
|
+
# result.exit_status # => 0
|
13
|
+
# result.duration # => 1.23
|
14
|
+
# result.new_field = 42
|
15
|
+
# result.new_field # => 42
|
16
|
+
# result.success? # => true
|
17
|
+
class CommandResult
|
18
|
+
# @param attributes [Hash{Symbol=>Object}] initial named attributes
|
19
|
+
def initialize(**attributes)
|
20
|
+
@attributes = {}
|
21
|
+
@attributes[:exit_status] = 0
|
22
|
+
@attributes[:stdout] = ''
|
23
|
+
attributes.each { |name, value| @attributes[name] = value }
|
24
|
+
end
|
25
|
+
|
26
|
+
def failure?
|
27
|
+
!success?
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Boolean] true if the exit status is zero
|
31
|
+
def success?
|
32
|
+
exit_status.zero?
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(name, *args)
|
36
|
+
key = name.to_s.chomp('=').to_sym
|
37
|
+
|
38
|
+
if name.to_s.end_with?('=') # setter
|
39
|
+
@attributes[key] = args.first
|
40
|
+
elsif attribute?(name) # getter
|
41
|
+
@attributes[name]
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def respond_to_missing?(name, include_private = false)
|
48
|
+
key = name.to_s.chomp('=').to_sym
|
49
|
+
attribute?(key) || super
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def attribute?(name)
|
55
|
+
@attributes.key?(name)
|
56
|
+
end
|
57
|
+
end
|
data/lib/constants.rb
CHANGED
@@ -92,7 +92,7 @@ class MenuState
|
|
92
92
|
end
|
93
93
|
|
94
94
|
# a struct to hold the data for a single line
|
95
|
-
NestedLine = Struct.new(:text, :depth, :indention) do
|
95
|
+
NestedLine = Struct.new(:text, :depth, :indention, :filename, :index) do
|
96
96
|
def to_s
|
97
97
|
indention + text
|
98
98
|
end
|
@@ -125,3 +125,21 @@ class TtyMenu
|
|
125
125
|
ENABLE = nil
|
126
126
|
DISABLE = ''
|
127
127
|
end
|
128
|
+
|
129
|
+
class ExportValueSource
|
130
|
+
ALLOW = ':allow'
|
131
|
+
DEFAULT = ':default'
|
132
|
+
ECHO = ':echo'
|
133
|
+
EDIT = ':edit'
|
134
|
+
EXEC = ':exec'
|
135
|
+
FALSE = false
|
136
|
+
end
|
137
|
+
|
138
|
+
class UxActSource
|
139
|
+
ALLOW = :allow
|
140
|
+
DEFAULT = :default
|
141
|
+
ECHO = :echo
|
142
|
+
EDIT = :edit
|
143
|
+
EXEC = :exec
|
144
|
+
FALSE = 'false'
|
145
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# encoding=utf-8
|
4
|
+
|
5
|
+
##
|
6
|
+
# Module providing standardized error‐reporting:
|
7
|
+
# logs either an Exception’s details or a simple
|
8
|
+
# string message—optionally with context—and then
|
9
|
+
# re‐raises either the original Exception or a new
|
10
|
+
# RuntimeError for string messages.
|
11
|
+
#
|
12
|
+
# Including this module gives you:
|
13
|
+
# • instance method → report_and_reraise(...)
|
14
|
+
# • class method → report_and_reraise(...)
|
15
|
+
module ErrorReporting
|
16
|
+
def self.included(base)
|
17
|
+
base.extend(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def report_and_reraise(error_or_message, context: nil)
|
21
|
+
if error_or_message.is_a?(Exception)
|
22
|
+
header = +"#{error_or_message.class}: #{error_or_message.message}"
|
23
|
+
header << " (#{context})" if context
|
24
|
+
|
25
|
+
ww header
|
26
|
+
ww error_or_message.backtrace.join("\n") if error_or_message.backtrace
|
27
|
+
|
28
|
+
raise error_or_message
|
29
|
+
else
|
30
|
+
header = +error_or_message.to_s
|
31
|
+
header << " (#{context})" if context
|
32
|
+
|
33
|
+
ww header
|
34
|
+
|
35
|
+
raise error_or_message.to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -10,12 +10,11 @@ class EvaluateShellExpression
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
|
13
|
-
|
14
|
-
|
13
|
+
initial_code_required: false,
|
14
|
+
occurrence_expressions: nil)
|
15
15
|
# !!p initial_code expressions key_format shell
|
16
16
|
return if (initial_code_required && (initial_code.nil? || initial_code.empty?)) ||
|
17
|
-
expressions.nil? || expressions.empty?
|
18
|
-
key_format.nil? || key_format.empty?
|
17
|
+
expressions.nil? || expressions.empty?
|
19
18
|
|
20
19
|
# token to separate output
|
21
20
|
token = "__TOKEN__#{Time.now.to_i}__"
|
@@ -28,7 +27,7 @@ def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
|
|
28
27
|
end
|
29
28
|
|
30
29
|
# Execute
|
31
|
-
stdout_str,
|
30
|
+
stdout_str, _, status = Open3.capture3(shell, '-c', script)
|
32
31
|
|
33
32
|
unless status.success?
|
34
33
|
return EvaluateShellExpression::StatusFail
|
@@ -40,7 +39,7 @@ def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
|
|
40
39
|
unless part.empty?
|
41
40
|
part[1..-1].tap do |output_parts|
|
42
41
|
expressions.each_with_index do |(key, _expression), index|
|
43
|
-
result_hash[
|
42
|
+
result_hash[occurrence_expressions[key]] = output_parts[index].chomp
|
44
43
|
end
|
45
44
|
end
|
46
45
|
end
|
@@ -54,7 +53,6 @@ require 'bundler/setup'
|
|
54
53
|
Bundler.require(:default)
|
55
54
|
|
56
55
|
require 'minitest/autorun'
|
57
|
-
require 'open3'
|
58
56
|
|
59
57
|
class TestShellExpressionEvaluator < Minitest::Test
|
60
58
|
def setup
|
@@ -66,7 +64,11 @@ class TestShellExpressionEvaluator < Minitest::Test
|
|
66
64
|
|
67
65
|
def test_single_expression
|
68
66
|
expressions = { 'greeting' => "echo 'Hello, World!'" }
|
69
|
-
|
67
|
+
occurrence_expressions = { 'greeting' => '%<greeting>' }
|
68
|
+
result = evaluate_shell_expressions(
|
69
|
+
@initial_code, expressions,
|
70
|
+
occurrence_expressions: occurrence_expressions
|
71
|
+
)
|
70
72
|
|
71
73
|
assert_equal 'Hello, World!', result['%<greeting>']
|
72
74
|
end
|
@@ -77,7 +79,15 @@ class TestShellExpressionEvaluator < Minitest::Test
|
|
77
79
|
'date' => 'date +%Y-%m-%d',
|
78
80
|
'kernel' => 'uname -r'
|
79
81
|
}
|
80
|
-
|
82
|
+
occurrence_expressions = {
|
83
|
+
'date' => '%<date>',
|
84
|
+
'greeting' => '%<greeting>',
|
85
|
+
'kernel' => '%<kernel>'
|
86
|
+
}
|
87
|
+
result = evaluate_shell_expressions(
|
88
|
+
@initial_code, expressions,
|
89
|
+
occurrence_expressions: occurrence_expressions
|
90
|
+
)
|
81
91
|
|
82
92
|
assert_equal 'Hello, World!', result['%<greeting>']
|
83
93
|
assert_match(/\d{4}-\d{2}-\d{2}/, result['%<date>'])
|
@@ -86,15 +96,22 @@ class TestShellExpressionEvaluator < Minitest::Test
|
|
86
96
|
|
87
97
|
def test_empty_expressions_list
|
88
98
|
expressions = {}
|
89
|
-
|
99
|
+
occurrence_expressions = {}
|
100
|
+
result = evaluate_shell_expressions(
|
101
|
+
@initial_code, expressions,
|
102
|
+
occurrence_expressions: occurrence_expressions
|
103
|
+
)
|
90
104
|
|
91
105
|
assert_nil result
|
92
106
|
end
|
93
107
|
|
94
108
|
def test_invalid_expression
|
95
109
|
expressions = { 'invalid' => 'invalid_command' }
|
96
|
-
|
97
|
-
result = evaluate_shell_expressions(
|
110
|
+
occurrence_expressions = {}
|
111
|
+
result = evaluate_shell_expressions(
|
112
|
+
@initial_code, expressions,
|
113
|
+
occurrence_expressions: occurrence_expressions
|
114
|
+
)
|
98
115
|
|
99
116
|
assert_equal EvaluateShellExpression::StatusFail, result
|
100
117
|
end
|
@@ -105,18 +122,26 @@ class TestShellExpressionEvaluator < Minitest::Test
|
|
105
122
|
echo "Custom setup message"
|
106
123
|
BASH
|
107
124
|
expressions = { 'test' => 'echo Test after initial setup' }
|
108
|
-
|
109
|
-
result = evaluate_shell_expressions(
|
125
|
+
occurrence_expressions = { 'test' => '%<test>' }
|
126
|
+
result = evaluate_shell_expressions(
|
127
|
+
@initial_code, expressions,
|
128
|
+
occurrence_expressions: occurrence_expressions
|
129
|
+
)
|
110
130
|
|
111
131
|
assert_equal 'Test after initial setup', result['%<test>']
|
112
132
|
end
|
113
133
|
|
114
134
|
def test_large_number_of_expressions
|
115
|
-
expressions = (1..100).
|
135
|
+
expressions = (1..100).to_h do |i|
|
116
136
|
["expr_#{i}", "echo Expression #{i}"]
|
117
|
-
end
|
118
|
-
|
119
|
-
|
137
|
+
end
|
138
|
+
occurrence_expressions = (1..100).to_h do |i|
|
139
|
+
["expr_#{i}", "%<expr_#{i}>"]
|
140
|
+
end
|
141
|
+
result = evaluate_shell_expressions(
|
142
|
+
@initial_code, expressions,
|
143
|
+
occurrence_expressions: occurrence_expressions
|
144
|
+
)
|
120
145
|
|
121
146
|
expressions.each_with_index do |(key, _expression), index|
|
122
147
|
assert_equal "Expression #{index + 1}", result["%<#{key}>"]
|
data/lib/fcb.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# encoding=utf-8
|
5
5
|
require 'digest'
|
6
6
|
require_relative 'namer'
|
7
|
-
|
7
|
+
BT_UX_FLD_REQUIRED = 'required'
|
8
8
|
def parse_yaml_of_ux_block(
|
9
9
|
data,
|
10
10
|
menu_format: nil,
|
@@ -18,15 +18,17 @@ def parse_yaml_of_ux_block(
|
|
18
18
|
raise "Name is missing in UX block: #{data.inspect}" unless name.present?
|
19
19
|
|
20
20
|
OpenStruct.new(
|
21
|
-
|
21
|
+
act: export['act'],
|
22
|
+
allow: export['allow'] || export['allowed'],
|
22
23
|
default: export['default'],
|
23
24
|
echo: export['echo'],
|
24
25
|
exec: export['exec'],
|
25
|
-
|
26
|
+
init: export['init'],
|
27
|
+
menu_format: export['format'] || export['menu_format'] || menu_format,
|
26
28
|
name: name,
|
27
|
-
preconditions: export['preconditions'],
|
28
29
|
prompt: export['prompt'] || prompt,
|
29
30
|
readonly: export['readonly'].nil? ? false : export['readonly'],
|
31
|
+
required: export['require'] || export['required'] || export[BT_UX_FLD_REQUIRED],
|
30
32
|
transform: export['transform'],
|
31
33
|
validate: export['validate'] || validate
|
32
34
|
)
|
@@ -137,8 +139,10 @@ module MarkdownExec
|
|
137
139
|
)&.fetch(1, nil)
|
138
140
|
titlexcall = call ? @attrs[:title].sub("%#{call}", '') : @attrs[:title]
|
139
141
|
|
140
|
-
oname = if
|
141
|
-
|
142
|
+
oname = if is_split?
|
143
|
+
@attrs[:text]
|
144
|
+
elsif block_name_nick_match.present? &&
|
145
|
+
@attrs[:oname] =~ Regexp.new(block_name_nick_match)
|
142
146
|
@attrs[:nickname] = $~[0]
|
143
147
|
derive_title_from_body
|
144
148
|
else
|
@@ -185,7 +189,61 @@ module MarkdownExec
|
|
185
189
|
def self.format_multiline_body_as_title(body_lines)
|
186
190
|
body_lines.map.with_index do |line, index|
|
187
191
|
index.zero? ? line : " #{line}"
|
188
|
-
end.join("\n")
|
192
|
+
end.join("\n")
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.act_source(export)
|
196
|
+
# If `false`, the UX block is not activated.
|
197
|
+
# If one of `:allow`, `:echo`, `:edit`, or `:exec` is specified,
|
198
|
+
# the value is calculated or the user is prompted.
|
199
|
+
# If not present, the default value is `:edit`.
|
200
|
+
if export.act.nil?
|
201
|
+
export.act = if export.init.to_s == 'false'
|
202
|
+
if export.allow.present?
|
203
|
+
UxActSource::ALLOW
|
204
|
+
elsif export.echo.present?
|
205
|
+
UxActSource::ECHO
|
206
|
+
elsif export.edit.present?
|
207
|
+
UxActSource::EDIT
|
208
|
+
elsif export.exec.present?
|
209
|
+
UxActSource::EXEC
|
210
|
+
else
|
211
|
+
UxActSource::EDIT
|
212
|
+
end
|
213
|
+
elsif export.allow.present?
|
214
|
+
UxActSource::ALLOW
|
215
|
+
else
|
216
|
+
UxActSource::EDIT
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
export.act
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.init_source(export)
|
224
|
+
# If `false`, there is no initial value set.
|
225
|
+
# If a string, it is the initial value of the object variable.
|
226
|
+
# Otherwise, if one of `:allow`, `:echo`, or `:exec` is specified,
|
227
|
+
# the value is the output of the `echo` or `exec` evaluation
|
228
|
+
# or the first allowed value.
|
229
|
+
# If not present, the default value is whichever of
|
230
|
+
# `:allow`, `:default`, `:echo`, or `:exec` is present.
|
231
|
+
if export.init.nil?
|
232
|
+
export.init = case
|
233
|
+
when export.allow.present?
|
234
|
+
UxActSource::ALLOW
|
235
|
+
when export.default.present?
|
236
|
+
UxActSource::DEFAULT
|
237
|
+
when export.echo.present?
|
238
|
+
UxActSource::ECHO
|
239
|
+
when export.exec.present?
|
240
|
+
UxActSource::EXEC
|
241
|
+
else
|
242
|
+
UxActSource::FALSE
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
export.init
|
189
247
|
end
|
190
248
|
|
191
249
|
# :reek:ManualDispatch
|
@@ -198,6 +256,14 @@ module MarkdownExec
|
|
198
256
|
dependency_names.include?(@attrs[:s2title])
|
199
257
|
end
|
200
258
|
|
259
|
+
def is_disabled?
|
260
|
+
@attrs[:disabled] == TtyMenu::DISABLE
|
261
|
+
end
|
262
|
+
|
263
|
+
def is_enabled?
|
264
|
+
!is_disabled?
|
265
|
+
end
|
266
|
+
|
201
267
|
def is_named?(name)
|
202
268
|
@attrs[:dname] == name ||
|
203
269
|
@attrs[:nickname] == name ||
|
@@ -206,6 +272,31 @@ module MarkdownExec
|
|
206
272
|
@attrs[:s2title] == name
|
207
273
|
end
|
208
274
|
|
275
|
+
# true if this is a line split block
|
276
|
+
def is_split?
|
277
|
+
is_split_first? || is_split_rest?
|
278
|
+
end
|
279
|
+
|
280
|
+
# true if this block displays its split body
|
281
|
+
# names and nicknames are displayed instead of the body
|
282
|
+
# ux blocks display a single line for the named variable
|
283
|
+
# split blocks are: opts, shell, vars
|
284
|
+
def is_split_displayed?(opts)
|
285
|
+
@attrs[:type] != BlockType::UX &&
|
286
|
+
!(@attrs[:start_line] =~ Regexp.new(opts[:block_name_nick_match]) ||
|
287
|
+
@attrs[:start_line] =~ Regexp.new(opts[:block_name_match]))
|
288
|
+
end
|
289
|
+
|
290
|
+
# true if this is the first line in a split block
|
291
|
+
def is_split_first?
|
292
|
+
@attrs.fetch(:is_split_first, false)
|
293
|
+
end
|
294
|
+
|
295
|
+
# true if this is the second or later line in a split block
|
296
|
+
def is_split_rest?
|
297
|
+
@attrs.fetch(:is_split_rest, false)
|
298
|
+
end
|
299
|
+
|
209
300
|
# :reek:ManualDispatch
|
210
301
|
def method_missing(method, *args, &block)
|
211
302
|
method_name = method.to_s
|