markdown_exec 0.1.3 → 0.2.3
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/.rubocop.yml +5 -0
- data/CHANGELOG.md +115 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +7 -1
- data/README.md +72 -17
- data/Rakefile +18 -6
- data/assets/approve_code.png +0 -0
- data/assets/example_blocks.png +0 -0
- data/assets/output_of_execution.png +0 -0
- data/assets/select_a_block.png +0 -0
- data/assets/select_a_file.png +0 -0
- data/fixtures/bash1.md +12 -0
- data/fixtures/bash2.md +15 -0
- data/fixtures/exclude1.md +6 -0
- data/fixtures/exclude2.md +9 -0
- data/fixtures/exec1.md +8 -0
- data/fixtures/heading1.md +19 -0
- data/fixtures/sample1.md +9 -0
- data/fixtures/title1.md +6 -0
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +511 -370
- metadata +19 -5
data/lib/markdown_exec.rb
CHANGED
@@ -3,18 +3,79 @@
|
|
3
3
|
|
4
4
|
# encoding=utf-8
|
5
5
|
|
6
|
-
# rubocop:disable Style/GlobalVars
|
7
|
-
$pdebug = !(ENV['MARKDOWN_EXEC_DEBUG'] || '').empty?
|
8
|
-
|
9
6
|
require 'open3'
|
10
7
|
require 'optparse'
|
11
|
-
# require 'pathname'
|
12
8
|
require 'tty-prompt'
|
13
9
|
require 'yaml'
|
10
|
+
|
11
|
+
##
|
12
|
+
# default if nil
|
13
|
+
# false if empty or '0'
|
14
|
+
# else true
|
15
|
+
|
16
|
+
def env_bool(name, default: false)
|
17
|
+
return default if (val = ENV[name]).nil?
|
18
|
+
return false if val.empty? || val == '0'
|
19
|
+
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def env_int(name, default: 0)
|
24
|
+
return default if (val = ENV[name]).nil?
|
25
|
+
return default if val.empty?
|
26
|
+
|
27
|
+
val.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def env_str(name, default: '')
|
31
|
+
ENV[name] || default
|
32
|
+
end
|
33
|
+
|
34
|
+
$pdebug = env_bool 'MDE_DEBUG'
|
35
|
+
|
14
36
|
require_relative 'markdown_exec/version'
|
15
37
|
|
38
|
+
$stderr.sync = true
|
39
|
+
$stdout.sync = true
|
40
|
+
|
16
41
|
BLOCK_SIZE = 1024
|
17
|
-
|
42
|
+
|
43
|
+
class Object # rubocop:disable Style/Documentation
|
44
|
+
def present?
|
45
|
+
self && !blank?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class String # rubocop:disable Style/Documentation
|
50
|
+
BLANK_RE = /\A[[:space:]]*\z/.freeze
|
51
|
+
def blank?
|
52
|
+
empty? || BLANK_RE.match?(self)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
public
|
57
|
+
|
58
|
+
# debug output
|
59
|
+
#
|
60
|
+
def tap_inspect(format: nil, name: 'return')
|
61
|
+
return self unless $pdebug
|
62
|
+
|
63
|
+
fn = case format
|
64
|
+
when :json
|
65
|
+
:to_json
|
66
|
+
when :string
|
67
|
+
:to_s
|
68
|
+
when :yaml
|
69
|
+
:to_yaml
|
70
|
+
else
|
71
|
+
:inspect
|
72
|
+
end
|
73
|
+
|
74
|
+
puts "-> #{caller[0].scan(/in `?(\S+)'$/)[0][0]}()" \
|
75
|
+
" #{name}: #{method(fn).call}"
|
76
|
+
|
77
|
+
self
|
78
|
+
end
|
18
79
|
|
19
80
|
module MarkdownExec
|
20
81
|
class Error < StandardError; end
|
@@ -26,494 +87,574 @@ module MarkdownExec
|
|
26
87
|
|
27
88
|
def initialize(options = {})
|
28
89
|
@options = options
|
90
|
+
@prompt = TTY::Prompt.new(interrupt: :exit)
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# options necessary to start, parse input, defaults for cli options
|
95
|
+
|
96
|
+
def base_options
|
97
|
+
{
|
98
|
+
# commands
|
99
|
+
list_blocks: false, # command
|
100
|
+
list_docs: false, # command
|
101
|
+
|
102
|
+
# command options
|
103
|
+
filename: env_str('MDE_FILENAME', default: nil), # option Filename to open
|
104
|
+
output_execution_summary: env_bool('MDE_OUTPUT_EXECUTION_SUMMARY', default: false), # option
|
105
|
+
output_script: env_bool('MDE_OUTPUT_SCRIPT', default: false), # option
|
106
|
+
output_stdout: env_bool('MDE_OUTPUT_STDOUT', default: true), # option
|
107
|
+
path: env_str('MDE_PATH', default: nil), # option Folder to search for files
|
108
|
+
save_executed_script: env_bool('MDE_SAVE_EXECUTED_SCRIPT', default: false), # option
|
109
|
+
saved_script_folder: env_str('MDE_SAVED_SCRIPT_FOLDER', default: 'logs'), # option
|
110
|
+
user_must_approve: env_bool('MDE_USER_MUST_APPROVE', default: true), # option Pause for user to approve script
|
111
|
+
|
112
|
+
# configuration options
|
113
|
+
block_name_excluded_match: env_str('MDE_BLOCK_NAME_EXCLUDED_MATCH', default: '^\(.+\)$'),
|
114
|
+
block_name_match: env_str('MDE_BLOCK_NAME_MATCH', default: ':(?<title>\S+)( |$)'),
|
115
|
+
block_required_scan: env_str('MDE_BLOCK_REQUIRED_SCAN', default: '\+\S+'),
|
116
|
+
fenced_start_and_end_match: env_str('MDE_FENCED_START_AND_END_MATCH', default: '^`{3,}'),
|
117
|
+
fenced_start_ex_match: env_str('MDE_FENCED_START_EX_MATCH', default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$'),
|
118
|
+
heading1_match: env_str('MDE_HEADING1_MATCH', default: '^# *(?<name>[^#]*?) *$'),
|
119
|
+
heading2_match: env_str('MDE_HEADING2_MATCH', default: '^## *(?<name>[^#]*?) *$'),
|
120
|
+
heading3_match: env_str('MDE_HEADING3_MATCH', default: '^### *(?<name>.+?) *$'),
|
121
|
+
md_filename_glob: env_str('MDE_MD_FILENAME_GLOB', default: '*.[Mm][Dd]'),
|
122
|
+
md_filename_match: env_str('MDE_MD_FILENAME_MATCH', default: '.+\\.md'),
|
123
|
+
mdheadings: true, # use headings (levels 1,2,3) in block lable
|
124
|
+
select_page_height: env_int('MDE_SELECT_PAGE_HEIGHT', default: 12)
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def default_options
|
129
|
+
{
|
130
|
+
bash: true, # bash block parsing in get_block_summary()
|
131
|
+
exclude_expect_blocks: true,
|
132
|
+
exclude_matching_block_names: true, # exclude hidden blocks
|
133
|
+
output_saved_script_filename: false,
|
134
|
+
prompt_select_block: 'Choose a block:', # in select_and_approve_block()
|
135
|
+
prompt_select_md: 'Choose a file:', # in select_md_file()
|
136
|
+
saved_script_filename: nil, # calculated
|
137
|
+
struct: true # allow get_block_summary()
|
138
|
+
}
|
29
139
|
end
|
30
140
|
|
31
|
-
|
141
|
+
# Returns true if all files are EOF
|
142
|
+
#
|
143
|
+
def all_at_eof(files)
|
144
|
+
files.find { |f| !f.eof }.nil?
|
145
|
+
end
|
146
|
+
|
147
|
+
def approve_block(opts, blocks_in_file)
|
148
|
+
required_blocks = list_recursively_required_blocks(blocks_in_file, opts[:block_name])
|
149
|
+
display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
|
150
|
+
|
151
|
+
allow = true
|
152
|
+
allow = @prompt.yes? 'Process?' if opts[:user_must_approve]
|
153
|
+
opts[:ir_approve] = allow
|
154
|
+
selected = get_block_by_name blocks_in_file, opts[:block_name]
|
155
|
+
|
156
|
+
if opts[:ir_approve]
|
157
|
+
write_command_file(opts, required_blocks) if opts[:save_executed_script]
|
158
|
+
command_execute opts, required_blocks.flatten.join("\n")
|
159
|
+
end
|
160
|
+
|
161
|
+
selected[:name]
|
162
|
+
end
|
163
|
+
|
164
|
+
def code(table, block)
|
165
|
+
all = [block[:name]] + recursively_required(table, block[:reqs])
|
166
|
+
all.reverse.map do |req|
|
167
|
+
get_block_by_name(table, req).fetch(:body, '')
|
168
|
+
end
|
169
|
+
.flatten(1)
|
170
|
+
.tap_inspect
|
171
|
+
end
|
172
|
+
|
173
|
+
def command_execute(opts, cmd2)
|
174
|
+
@execute_options = opts
|
175
|
+
@execute_started_at = Time.now.utc
|
176
|
+
Open3.popen3(cmd2) do |stdin, stdout, stderr|
|
177
|
+
stdin.close_write
|
178
|
+
begin
|
179
|
+
files = [stdout, stderr]
|
180
|
+
|
181
|
+
until all_at_eof(files)
|
182
|
+
ready = IO.select(files)
|
183
|
+
|
184
|
+
next unless ready
|
185
|
+
|
186
|
+
# readable = ready[0]
|
187
|
+
# # writable = ready[1]
|
188
|
+
# # exceptions = ready[2]
|
189
|
+
@execute_files = Hash.new([])
|
190
|
+
ready.each.with_index do |readable, ind|
|
191
|
+
readable.each do |f|
|
192
|
+
block = f.read_nonblock(BLOCK_SIZE)
|
193
|
+
@execute_files[ind] = @execute_files[ind] + [block]
|
194
|
+
print block if opts[:output_stdout]
|
195
|
+
rescue EOFError #=> e
|
196
|
+
# do nothing at EOF
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
rescue IOError => e
|
201
|
+
fout "IOError: #{e}"
|
202
|
+
end
|
203
|
+
@execute_completed_at = Time.now.utc
|
204
|
+
end
|
205
|
+
rescue Errno::ENOENT => e
|
206
|
+
@execute_aborted_at = Time.now.utc
|
207
|
+
@execute_error_message = e.message
|
208
|
+
@execute_error = e
|
209
|
+
fout "Error ENOENT: #{e.inspect}"
|
210
|
+
end
|
211
|
+
|
212
|
+
def count_blocks_in_filename
|
213
|
+
fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match]
|
32
214
|
cnt = 0
|
33
|
-
File.readlines(options[:
|
34
|
-
cnt += 1 if line.match(
|
215
|
+
File.readlines(@options[:filename]).each do |line|
|
216
|
+
cnt += 1 if line.match(fenced_start_and_end_match)
|
35
217
|
end
|
36
218
|
cnt / 2
|
37
219
|
end
|
38
220
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
221
|
+
def display_command(_opts, required_blocks)
|
222
|
+
required_blocks.each { |cb| fout cb }
|
223
|
+
end
|
224
|
+
|
225
|
+
def exec_block(options, block_name = '')
|
226
|
+
options = default_options.merge options
|
227
|
+
update_options options, over: false
|
228
|
+
|
229
|
+
# document and block reports
|
230
|
+
#
|
231
|
+
files = list_files_per_options(options)
|
232
|
+
if @options[:list_docs]
|
233
|
+
fout_list files
|
234
|
+
return
|
235
|
+
end
|
236
|
+
|
237
|
+
if @options[:list_blocks]
|
238
|
+
fout_list (files.map do |file|
|
239
|
+
make_block_labels(filename: file, struct: true)
|
240
|
+
end).flatten(1)
|
241
|
+
return
|
44
242
|
end
|
243
|
+
|
244
|
+
# process
|
245
|
+
#
|
246
|
+
select_and_approve_block(
|
247
|
+
bash: true,
|
248
|
+
block_name: block_name,
|
249
|
+
filename: select_md_file(files),
|
250
|
+
struct: true
|
251
|
+
)
|
252
|
+
|
253
|
+
fout "saved_filespec: #{@execute_script_filespec}" if @options[:output_saved_script_filename]
|
254
|
+
|
255
|
+
output_execution_summary if @options[:output_execution_summary]
|
45
256
|
end
|
46
257
|
|
258
|
+
# standard output; not for debug
|
259
|
+
#
|
47
260
|
def fout(str)
|
48
|
-
puts str
|
261
|
+
puts str
|
49
262
|
end
|
50
263
|
|
51
|
-
def
|
52
|
-
|
53
|
-
if options_block
|
54
|
-
options_block.call class_call_options
|
55
|
-
else
|
56
|
-
class_call_options
|
57
|
-
end.tap { |ret| puts "copts() ret: #{ret.inspect}" if $pdebug }
|
264
|
+
def fout_list(str)
|
265
|
+
puts str
|
58
266
|
end
|
59
267
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
268
|
+
def fout_section(name, data)
|
269
|
+
puts "# #{name}"
|
270
|
+
puts data.to_yaml
|
63
271
|
end
|
64
272
|
|
65
|
-
def
|
66
|
-
|
273
|
+
def get_block_by_name(table, name, default = {})
|
274
|
+
table.select { |block| block[:name] == name }.fetch(0, default)
|
275
|
+
end
|
276
|
+
|
277
|
+
def get_block_summary(opts, headings, block_title, current)
|
67
278
|
return [current] unless opts[:struct]
|
68
279
|
|
69
|
-
|
70
|
-
return [bsr(headings, block_title).merge({ body: current })] unless opts[:bash]
|
280
|
+
return [summarize_block(headings, block_title).merge({ body: current })] unless opts[:bash]
|
71
281
|
|
72
|
-
bm = block_title.match(
|
73
|
-
reqs = block_title.scan(
|
74
|
-
|
75
|
-
if $pdebug
|
76
|
-
puts ["block_summary() bm: #{bm.inspect}",
|
77
|
-
"block_summary() reqs: #{reqs.inspect}"]
|
78
|
-
end
|
282
|
+
bm = block_title.match(Regexp.new(opts[:block_name_match]))
|
283
|
+
reqs = block_title.scan(Regexp.new(opts[:block_required_scan])).map { |s| s[1..] }
|
79
284
|
|
80
285
|
if bm && bm[1]
|
81
|
-
|
82
|
-
[bsr(headings, bm[1]).merge({ body: current, reqs: reqs })]
|
286
|
+
[summarize_block(headings, bm[:title]).merge({ body: current, reqs: reqs })]
|
83
287
|
else
|
84
|
-
|
85
|
-
[bsr(headings, block_title).merge({ body: current, reqs: reqs })]
|
288
|
+
[summarize_block(headings, block_title).merge({ body: current, reqs: reqs })]
|
86
289
|
end
|
87
290
|
end
|
88
291
|
|
89
|
-
def
|
90
|
-
opts =
|
292
|
+
def list_blocks_in_file(call_options = {}, &options_block)
|
293
|
+
opts = optsmerge call_options, options_block
|
294
|
+
|
295
|
+
unless opts[:filename]&.present?
|
296
|
+
fout 'No blocks found.'
|
297
|
+
exit 1
|
298
|
+
end
|
91
299
|
|
300
|
+
fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
|
301
|
+
fenced_start_ex = Regexp.new opts[:fenced_start_ex_match]
|
302
|
+
block_title = ''
|
92
303
|
blocks = []
|
93
304
|
current = nil
|
94
|
-
in_block = false
|
95
|
-
block_title = ''
|
96
|
-
|
97
305
|
headings = []
|
98
|
-
|
99
|
-
|
306
|
+
in_block = false
|
307
|
+
File.readlines(opts[:filename]).each do |line|
|
100
308
|
continue unless line
|
101
309
|
|
102
310
|
if opts[:mdheadings]
|
103
|
-
if (lm = line.match(
|
104
|
-
headings = [headings[0], headings[1], lm[
|
105
|
-
elsif (lm = line.match(
|
106
|
-
headings = [headings[0], lm[
|
107
|
-
elsif (lm = line.match(
|
108
|
-
headings = [lm[
|
311
|
+
if (lm = line.match(Regexp.new(opts[:heading3_match])))
|
312
|
+
headings = [headings[0], headings[1], lm[:name]]
|
313
|
+
elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
|
314
|
+
headings = [headings[0], lm[:name]]
|
315
|
+
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
316
|
+
headings = [lm[:name]]
|
109
317
|
end
|
110
|
-
puts "get_blocks() headings: #{headings.inspect}" if $pdebug
|
111
318
|
end
|
112
319
|
|
113
|
-
if line.match(
|
320
|
+
if line.match(fenced_start_and_end_match)
|
114
321
|
if in_block
|
115
|
-
puts 'get_blocks() in_block' if $pdebug
|
116
322
|
if current
|
117
|
-
|
118
|
-
# block_title ||= current.join(' ').gsub(/ +/, ' ')[0..64]
|
119
323
|
block_title = current.join(' ').gsub(/ +/, ' ')[0..64] if block_title.nil? || block_title.empty?
|
120
|
-
|
121
|
-
blocks += block_summary opts, headings, block_title, current
|
324
|
+
blocks += get_block_summary opts, headings, block_title, current
|
122
325
|
current = nil
|
123
326
|
end
|
124
327
|
in_block = false
|
125
328
|
block_title = ''
|
126
329
|
else
|
127
|
-
|
330
|
+
# new block
|
128
331
|
#
|
129
|
-
|
130
|
-
# lm = line.match(/^`{3,}([^`\s]+)( .+)?$/)
|
131
|
-
lm = line.match(/^`{3,}([^`\s]*) *(.*)$/)
|
132
|
-
|
332
|
+
lm = line.match(fenced_start_ex)
|
133
333
|
do1 = false
|
134
334
|
if opts[:bash_only]
|
135
|
-
do1 = true if lm && (lm[
|
136
|
-
elsif opts[:exclude_expect_blocks]
|
137
|
-
do1 = true unless lm && (lm[1] == 'expect')
|
335
|
+
do1 = true if lm && (lm[:shell] == 'bash')
|
138
336
|
else
|
139
337
|
do1 = true
|
140
|
-
|
141
|
-
if $pdebug
|
142
|
-
puts ["get_blocks() lm: #{lm.inspect}",
|
143
|
-
"get_blocks() opts: #{opts.inspect}",
|
144
|
-
"get_blocks() do1: #{do1}"]
|
338
|
+
do1 = !(lm && (lm[:shell] == 'expect')) if opts[:exclude_expect_blocks]
|
145
339
|
end
|
146
340
|
|
147
|
-
|
341
|
+
in_block = true
|
342
|
+
if do1 && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match])))
|
148
343
|
current = []
|
149
|
-
|
150
|
-
block_title = (lm && lm[2])
|
344
|
+
block_title = (lm && lm[:name])
|
151
345
|
end
|
152
|
-
|
153
346
|
end
|
154
347
|
elsif current
|
155
348
|
current += [line.chomp]
|
156
349
|
end
|
157
350
|
end
|
158
|
-
blocks.
|
159
|
-
end # get_blocks
|
160
|
-
|
161
|
-
def make_block_label(block, call_options = {})
|
162
|
-
opts = options.merge(call_options)
|
163
|
-
puts "make_block_label() opts: #{opts.inspect}" if $pdebug
|
164
|
-
puts "make_block_label() block: #{block.inspect}" if $pdebug
|
165
|
-
if opts[:mdheadings]
|
166
|
-
heads = block.fetch(:headings, []).compact.join(' # ')
|
167
|
-
"#{block[:title]} [#{heads}] (#{opts[:mdfilename]})"
|
168
|
-
else
|
169
|
-
"#{block[:title]} (#{opts[:mdfilename]})"
|
170
|
-
end
|
351
|
+
blocks.tap_inspect
|
171
352
|
end
|
172
353
|
|
173
|
-
def
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
354
|
+
def list_files_per_options(options)
|
355
|
+
default_filename = 'README.md'
|
356
|
+
default_folder = '.'
|
357
|
+
if options[:filename]&.present?
|
358
|
+
list_files_specified(options[:filename], options[:path], default_filename, default_folder)
|
359
|
+
else
|
360
|
+
list_files_specified(nil, options[:path], default_filename, default_folder)
|
361
|
+
end.tap_inspect
|
178
362
|
end
|
179
363
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
364
|
+
def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil)
|
365
|
+
fn = File.join(if specified_filename&.present?
|
366
|
+
if specified_folder&.present?
|
367
|
+
[specified_folder, specified_filename]
|
368
|
+
elsif specified_filename.start_with? '/'
|
369
|
+
[specified_filename]
|
370
|
+
else
|
371
|
+
[default_folder, specified_filename]
|
372
|
+
end
|
373
|
+
elsif specified_folder&.present?
|
374
|
+
if filetree
|
375
|
+
[specified_folder, @options[:md_filename_match]]
|
376
|
+
else
|
377
|
+
[specified_folder, @options[:md_filename_glob]]
|
378
|
+
end
|
379
|
+
else
|
380
|
+
[default_folder, default_filename]
|
381
|
+
end)
|
382
|
+
if filetree
|
383
|
+
filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) }
|
199
384
|
else
|
200
|
-
|
201
|
-
end
|
202
|
-
|
203
|
-
return nil if block_labels.count.zero?
|
385
|
+
Dir.glob(fn)
|
386
|
+
end.tap_inspect
|
387
|
+
end
|
204
388
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
# # catch TTY::Reader::InputInterrupt
|
209
|
-
# puts "InputInterrupt"
|
210
|
-
# end
|
389
|
+
def list_markdown_files_in_path
|
390
|
+
Dir.glob(File.join(@options[:path], @options[:md_filename_glob])).tap_inspect
|
391
|
+
end
|
211
392
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
393
|
+
def list_named_blocks_in_file(call_options = {}, &options_block)
|
394
|
+
opts = optsmerge call_options, options_block
|
395
|
+
block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
|
396
|
+
list_blocks_in_file(opts).map do |block|
|
397
|
+
next if opts[:exclude_matching_block_names] && block[:name].match(block_name_excluded_match)
|
216
398
|
|
217
|
-
|
218
|
-
|
399
|
+
block
|
400
|
+
end.compact.tap_inspect
|
401
|
+
end
|
219
402
|
|
220
|
-
|
221
|
-
|
222
|
-
|
403
|
+
def list_recursively_required_blocks(table, name)
|
404
|
+
name_block = get_block_by_name(table, name)
|
405
|
+
raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
|
223
406
|
|
224
|
-
|
225
|
-
allow = prompt.yes? 'Process?' if opts[:approve]
|
226
|
-
|
227
|
-
selected = block_by_name blocks, sel
|
228
|
-
puts "select_block() selected: #{selected.inspect}" if $pdebug
|
229
|
-
if allow && opts[:execute]
|
230
|
-
|
231
|
-
## process in script, to handle line continuations
|
232
|
-
#
|
233
|
-
cmd2 = cbs.flatten.join("\n")
|
234
|
-
fout "$ #{cmd2.to_yaml}"
|
235
|
-
|
236
|
-
# Open3.popen3(cmd2) do |stdin, stdout, stderr, wait_thr|
|
237
|
-
# cnt += 1
|
238
|
-
# # stdin.puts "This is sent to the command"
|
239
|
-
# # stdin.close # we're done
|
240
|
-
# stdout_str = stdout.read # read stdout to string. note that this will block until the command is done!
|
241
|
-
# stderr_str = stderr.read # read stderr to string
|
242
|
-
# status = wait_thr.value # will block until the command finishes; returns status that responds to .success?
|
243
|
-
# fout "#{stdout_str}"
|
244
|
-
# fout "#{cnt}: err: #{stderr_str}" if stderr_str != ''
|
245
|
-
# # fout "#{cnt}: stt: #{status}"
|
246
|
-
# end
|
247
|
-
|
248
|
-
Open3.popen3(cmd2) do |stdin, stdout, stderr|
|
249
|
-
stdin.close_write
|
250
|
-
begin
|
251
|
-
files = [stdout, stderr]
|
252
|
-
|
253
|
-
until all_eof(files)
|
254
|
-
ready = IO.select(files)
|
255
|
-
|
256
|
-
next unless ready
|
257
|
-
|
258
|
-
readable = ready[0]
|
259
|
-
# writable = ready[1]
|
260
|
-
# exceptions = ready[2]
|
407
|
+
all = [name_block[:name]] + recursively_required(table, name_block[:reqs])
|
261
408
|
|
262
|
-
|
263
|
-
|
409
|
+
# in order of appearance in document
|
410
|
+
table.select { |block| all.include? block[:name] }
|
411
|
+
.map { |block| block.fetch(:body, '') }
|
412
|
+
.flatten(1)
|
413
|
+
.tap_inspect
|
414
|
+
end
|
264
415
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
rescue IOError => e
|
273
|
-
fout "IOError: #{e}"
|
274
|
-
end
|
275
|
-
end
|
416
|
+
def make_block_label(block, call_options = {})
|
417
|
+
opts = options.merge(call_options)
|
418
|
+
if opts[:mdheadings]
|
419
|
+
heads = block.fetch(:headings, []).compact.join(' # ')
|
420
|
+
"#{block[:title]} [#{heads}] (#{opts[:filename]})"
|
421
|
+
else
|
422
|
+
"#{block[:title]} (#{opts[:filename]})"
|
276
423
|
end
|
424
|
+
end
|
277
425
|
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
opts = options
|
283
|
-
files = find_files
|
284
|
-
if files.count == 1
|
285
|
-
sel = files[0]
|
286
|
-
elsif files.count >= 2
|
287
|
-
|
288
|
-
if opts[:preview_options]
|
289
|
-
select_per_page = 3
|
290
|
-
files.each do |file|
|
291
|
-
fout " - #{file}"
|
292
|
-
end
|
293
|
-
else
|
294
|
-
select_per_page = SELECT_PAGE_HEIGHT
|
295
|
-
end
|
296
|
-
|
297
|
-
prompt = TTY::Prompt.new
|
298
|
-
sel = prompt.select("#{opts.fetch(:prompt, 'Pick one')}:", files, per_page: select_per_page)
|
299
|
-
end
|
426
|
+
def make_block_labels(call_options = {})
|
427
|
+
opts = options.merge(call_options)
|
428
|
+
list_blocks_in_file(opts).map do |block|
|
429
|
+
# next if opts[:exclude_matching_block_names] && block[:name].match(%r{^:\(.+\)$})
|
300
430
|
|
301
|
-
|
431
|
+
make_block_label block, opts
|
432
|
+
end.compact.tap_inspect
|
302
433
|
end
|
303
434
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
435
|
+
def option_exclude_blocks(opts, blocks)
|
436
|
+
block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
|
437
|
+
if opts[:exclude_matching_block_names]
|
438
|
+
blocks.reject { |block| block[:name].match(block_name_excluded_match) }
|
439
|
+
else
|
440
|
+
blocks
|
441
|
+
end
|
308
442
|
end
|
309
443
|
|
310
|
-
def
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
block_by_name(table, req).fetch(:body, '')
|
318
|
-
end
|
319
|
-
.flatten(1)
|
320
|
-
.tap { |ret| puts "code() ret: #{ret.inspect}" if $pdebug }
|
444
|
+
def optsmerge(call_options = {}, options_block = nil)
|
445
|
+
class_call_options = @options.merge(call_options || {})
|
446
|
+
if options_block
|
447
|
+
options_block.call class_call_options
|
448
|
+
else
|
449
|
+
class_call_options
|
450
|
+
end.tap_inspect
|
321
451
|
end
|
322
452
|
|
323
|
-
def
|
324
|
-
|
453
|
+
def output_execution_summary
|
454
|
+
fout_section 'summary', {
|
455
|
+
execute_aborted_at: @execute_aborted_at,
|
456
|
+
execute_completed_at: @execute_completed_at,
|
457
|
+
execute_error: @execute_error,
|
458
|
+
execute_error_message: @execute_error_message,
|
459
|
+
execute_files: @execute_files,
|
460
|
+
execute_options: @execute_options,
|
461
|
+
execute_started_at: @execute_started_at,
|
462
|
+
execute_script_filespec: @execute_script_filespec
|
463
|
+
}
|
325
464
|
end
|
326
465
|
|
327
|
-
def
|
328
|
-
|
329
|
-
puts "code_blocks() name: #{name.inspect}" if $pdebug
|
330
|
-
name_block = block_by_name(table, name)
|
331
|
-
puts "code_blocks() name_block: #{name_block.inspect}" if $pdebug
|
332
|
-
all = [name_block[:name]] + unroll(table, name_block[:reqs])
|
333
|
-
puts "code_blocks() all: #{all.inspect}" if $pdebug
|
466
|
+
def read_configuration_file!(options, configuration_path)
|
467
|
+
return unless File.exist?(configuration_path)
|
334
468
|
|
335
|
-
#
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
.tap { |ret| puts "code_blocks() ret: #{ret.inspect}" if $pdebug }
|
469
|
+
# rubocop:disable Security/YAMLLoad
|
470
|
+
options.merge!((YAML.load(File.open(configuration_path)) || {})
|
471
|
+
.transform_keys(&:to_sym))
|
472
|
+
# rubocop:enable Security/YAMLLoad
|
340
473
|
end
|
341
474
|
|
342
|
-
def
|
343
|
-
puts "unroll() table: #{table.inspect}" if $pdebug
|
344
|
-
puts "unroll() reqs: #{reqs.inspect}" if $pdebug
|
475
|
+
def recursively_required(table, reqs)
|
345
476
|
all = []
|
346
477
|
rem = reqs
|
347
478
|
while rem.count.positive?
|
348
|
-
puts "unrol() rem: #{rem.inspect}" if $pdebug
|
349
479
|
rem = rem.map do |req|
|
350
|
-
puts "unrol() req: #{req.inspect}" if $pdebug
|
351
480
|
next if all.include? req
|
352
481
|
|
353
482
|
all += [req]
|
354
|
-
|
355
|
-
block_by_name(table, req).fetch(:reqs, [])
|
483
|
+
get_block_by_name(table, req).fetch(:reqs, [])
|
356
484
|
end
|
357
485
|
.compact
|
358
486
|
.flatten(1)
|
359
|
-
.
|
487
|
+
.tap_inspect(name: 'rem')
|
360
488
|
end
|
361
|
-
all.
|
362
|
-
end
|
363
|
-
|
364
|
-
# $stderr.sync = true
|
365
|
-
# $stdout.sync = true
|
366
|
-
|
367
|
-
## configuration file
|
368
|
-
#
|
369
|
-
def read_configuration!(options, configuration_path)
|
370
|
-
if File.exist?(configuration_path)
|
371
|
-
# rubocop:disable Security/YAMLLoad
|
372
|
-
options.merge!((YAML.load(File.open(configuration_path)) || {})
|
373
|
-
.transform_keys(&:to_sym))
|
374
|
-
# rubocop:enable Security/YAMLLoad
|
375
|
-
end
|
376
|
-
options
|
489
|
+
all.tap_inspect
|
377
490
|
end
|
378
491
|
|
379
492
|
def run
|
380
493
|
## default configuration
|
381
494
|
#
|
382
|
-
options =
|
383
|
-
mdheadings: true,
|
384
|
-
list_blocks: false,
|
385
|
-
list_docs: false,
|
386
|
-
mdfilename: 'README.md',
|
387
|
-
mdfolder: '.'
|
388
|
-
}
|
495
|
+
@options = base_options
|
389
496
|
|
390
|
-
|
497
|
+
## post-parse options configuration
|
498
|
+
#
|
499
|
+
options_finalize = ->(_options) {}
|
391
500
|
|
392
|
-
|
501
|
+
proc_self = ->(value) { value }
|
502
|
+
proc_to_i = ->(value) { value.to_i != 0 }
|
503
|
+
proc_true = ->(_) { true }
|
393
504
|
|
394
505
|
# read local configuration file
|
395
506
|
#
|
396
|
-
|
507
|
+
read_configuration_file! @options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
|
397
508
|
|
398
|
-
## read current details for aws resources from app_data_file
|
399
|
-
#
|
400
|
-
# load_resources! options
|
401
|
-
# puts "q31 options: #{options.to_yaml}" if $pdebug
|
402
|
-
|
403
|
-
# rubocop:disable Metrics/BlockLength
|
404
509
|
option_parser = OptionParser.new do |opts|
|
405
510
|
executable_name = File.basename($PROGRAM_NAME)
|
406
511
|
opts.banner = [
|
407
|
-
"#{MarkdownExec::APP_NAME}
|
408
|
-
"
|
512
|
+
"#{MarkdownExec::APP_NAME}" \
|
513
|
+
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
514
|
+
"Usage: #{executable_name} [path] [filename] [options]"
|
409
515
|
].join("\n")
|
410
516
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
517
|
+
summary_head = [
|
518
|
+
['config', nil, nil, 'PATH', 'Read configuration file',
|
519
|
+
nil, ->(value) { read_configuration_file! options, value }],
|
520
|
+
['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output',
|
521
|
+
nil, ->(value) { $pdebug = value.to_i != 0 }]
|
522
|
+
]
|
523
|
+
|
524
|
+
summary_body = [
|
525
|
+
['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document',
|
526
|
+
:filename, proc_self],
|
527
|
+
['list-blocks', nil, nil, nil, 'List blocks',
|
528
|
+
:list_blocks, proc_true],
|
529
|
+
['list-docs', nil, nil, nil, 'List docs in current folder',
|
530
|
+
:list_docs, proc_true],
|
531
|
+
['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution',
|
532
|
+
:output_execution_summary, proc_to_i],
|
533
|
+
['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script',
|
534
|
+
:output_script, proc_to_i],
|
535
|
+
['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution',
|
536
|
+
:output_stdout, proc_to_i],
|
537
|
+
['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents',
|
538
|
+
:path, proc_self],
|
539
|
+
['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script',
|
540
|
+
:save_executed_script, proc_to_i],
|
541
|
+
['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder',
|
542
|
+
:saved_script_folder, proc_self],
|
543
|
+
['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause to approve execution',
|
544
|
+
:user_must_approve, proc_to_i]
|
545
|
+
]
|
546
|
+
|
547
|
+
# rubocop:disable Style/Semicolon
|
548
|
+
summary_tail = [
|
549
|
+
[nil, '0', nil, nil, 'Show configuration',
|
550
|
+
nil, ->(_) { options_finalize.call options; fout options.to_yaml }],
|
551
|
+
['help', 'h', nil, nil, 'App help',
|
552
|
+
nil, ->(_) { fout option_parser.help; exit }],
|
553
|
+
['version', 'v', nil, nil, 'App version',
|
554
|
+
nil, ->(_) { fout MarkdownExec::VERSION; exit }],
|
555
|
+
['exit', 'x', nil, nil, 'Exit app',
|
556
|
+
nil, ->(_) { exit }]
|
557
|
+
]
|
558
|
+
# rubocop:enable Style/Semicolon
|
559
|
+
|
560
|
+
(summary_head + summary_body + summary_tail)
|
561
|
+
.map do |long_name, short_name, env_var, arg_name, description, opt_name, proc1| # rubocop:disable Metrics/ParameterLists
|
562
|
+
opts.on(*[long_name.present? ? "--#{long_name}#{arg_name.present? ? (' ' + arg_name) : ''}" : nil,
|
563
|
+
short_name.present? ? "-#{short_name}" : nil,
|
564
|
+
[description,
|
565
|
+
env_var.present? ? "env: #{env_var}" : nil].compact.join(' - '),
|
566
|
+
lambda { |value|
|
567
|
+
ret = proc1.call(value)
|
568
|
+
options[opt_name] = ret if opt_name
|
569
|
+
ret
|
570
|
+
}].compact)
|
433
571
|
end
|
572
|
+
end
|
573
|
+
option_parser.load # filename defaults to basename of the program without suffix in a directory ~/.options
|
574
|
+
option_parser.environment # env defaults to the basename of the program.
|
575
|
+
rest = option_parser.parse! # (into: options)
|
434
576
|
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
puts option_parser.help
|
439
|
-
exit
|
440
|
-
end
|
577
|
+
## finalize configuration
|
578
|
+
#
|
579
|
+
options_finalize.call options
|
441
580
|
|
442
|
-
|
443
|
-
|
444
|
-
|
581
|
+
## position 0: file or folder (optional)
|
582
|
+
#
|
583
|
+
if (pos = rest.fetch(0, nil))&.present?
|
584
|
+
if Dir.exist?(pos)
|
585
|
+
options[:path] = pos
|
586
|
+
elsif File.exist?(pos)
|
587
|
+
options[:filename] = pos
|
588
|
+
else
|
589
|
+
raise "Invalid parameter: #{pos}"
|
445
590
|
end
|
591
|
+
end
|
446
592
|
|
447
|
-
|
448
|
-
|
449
|
-
|
593
|
+
## position 1: block name (optional)
|
594
|
+
#
|
595
|
+
block_name = rest.fetch(1, nil)
|
450
596
|
|
451
|
-
|
452
|
-
|
453
|
-
puts options.to_yaml
|
454
|
-
end
|
455
|
-
end # OptionParser
|
456
|
-
# rubocop:enable Metrics/BlockLength
|
597
|
+
exec_block options, block_name
|
598
|
+
end
|
457
599
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
options_finalize! options
|
600
|
+
def select_and_approve_block(call_options = {}, &options_block)
|
601
|
+
opts = optsmerge call_options, options_block
|
602
|
+
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
462
603
|
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
mp = MarkParse.new options
|
468
|
-
options.merge!(
|
469
|
-
{
|
470
|
-
approve: true,
|
471
|
-
bash: true,
|
472
|
-
display: true,
|
473
|
-
exclude_expect_blocks: true,
|
474
|
-
execute: true,
|
475
|
-
prompt: 'Execute',
|
476
|
-
struct: true
|
477
|
-
}
|
478
|
-
)
|
479
|
-
|
480
|
-
## show
|
481
|
-
#
|
482
|
-
if options[:list_docs]
|
483
|
-
fout mp.find_files
|
484
|
-
break
|
485
|
-
end
|
604
|
+
unless opts[:block_name].present?
|
605
|
+
pt = (opts[:prompt_select_block]).to_s
|
606
|
+
blocks_in_file.each { |block| block.merge! label: make_block_label(block, opts) }
|
607
|
+
block_labels = option_exclude_blocks(opts, blocks_in_file).map { |block| block[:label] }
|
486
608
|
|
487
|
-
if
|
488
|
-
fout (mp.find_files.map do |file|
|
489
|
-
mp.make_block_labels(mdfilename: file, struct: true)
|
490
|
-
end).flatten(1).to_yaml
|
491
|
-
break
|
492
|
-
end
|
609
|
+
return nil if block_labels.count.zero?
|
493
610
|
|
494
|
-
|
495
|
-
|
496
|
-
|
611
|
+
sel = @prompt.select(pt, block_labels, per_page: opts[:select_page_height])
|
612
|
+
label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
|
613
|
+
opts[:block_name] = label_block[:name]
|
614
|
+
end
|
497
615
|
|
498
|
-
|
499
|
-
|
500
|
-
# rescue ArgumentError => e
|
501
|
-
# puts "User abort: #{e}"
|
616
|
+
approve_block opts, blocks_in_file
|
617
|
+
end
|
502
618
|
|
503
|
-
|
504
|
-
|
505
|
-
|
619
|
+
def select_md_file(files_ = nil)
|
620
|
+
opts = options
|
621
|
+
files = files_ || list_markdown_files_in_path
|
622
|
+
if files.count == 1
|
623
|
+
files[0]
|
624
|
+
elsif files.count >= 2
|
625
|
+
@prompt.select(opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height])
|
626
|
+
end
|
627
|
+
end
|
506
628
|
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
# rubocop:enable Style/BlockComments
|
629
|
+
def summarize_block(headings, title)
|
630
|
+
{ headings: headings, name: title, title: title }
|
631
|
+
end
|
511
632
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
633
|
+
def update_options(opts = {}, over: true)
|
634
|
+
if over
|
635
|
+
@options = @options.merge opts
|
636
|
+
else
|
637
|
+
@options.merge! opts
|
638
|
+
end
|
639
|
+
@options
|
640
|
+
end
|
516
641
|
|
517
|
-
|
518
|
-
|
519
|
-
|
642
|
+
def write_command_file(opts, required_blocks)
|
643
|
+
return unless opts[:saved_script_filename].present?
|
644
|
+
|
645
|
+
fne = File.basename(opts[:filename], '.*').gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
|
646
|
+
bne = opts[:block_name].gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
|
647
|
+
opts[:saved_script_filename] = "mde_#{Time.now.utc.strftime '%F-%H-%M-%S'}_#{fne}_#{bne}.sh"
|
648
|
+
|
649
|
+
@options[:saved_filespec] = File.join opts[:saved_script_folder], opts[:saved_script_filename]
|
650
|
+
@execute_script_filespec = @options[:saved_filespec]
|
651
|
+
dirname = File.dirname(@options[:saved_filespec])
|
652
|
+
Dir.mkdir dirname unless File.exist?(dirname)
|
653
|
+
File.write(@options[:saved_filespec], "#!/usr/bin/env bash\n" \
|
654
|
+
"# file_name: #{opts[:filename]}\n" \
|
655
|
+
"# block_name: #{opts[:block_name]}\n" \
|
656
|
+
"# time: #{Time.now.utc}\n" \
|
657
|
+
"#{required_blocks.flatten.join("\n")}\n")
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|