markdown_exec 1.3.8 → 1.3.9
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 +41 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -1
- data/Rakefile +11 -7
- data/bin/colorize_env_vars.sh +7 -0
- data/bin/tab_completion.sh +19 -19
- data/examples/duplicate_block.md +10 -0
- data/examples/import0.md +8 -0
- data/examples/import1.md +10 -0
- data/examples/include.md +12 -0
- data/examples/infile_config.md +10 -0
- data/examples/linked1.md +28 -0
- data/examples/linked2.md +28 -0
- data/examples/opts.md +13 -0
- data/examples/pass-through.md +14 -0
- data/examples/plant.md +23 -0
- data/examples/port.md +23 -0
- data/examples/vars.md +20 -0
- data/examples/wrap.md +33 -0
- data/lib/block_types.rb +5 -0
- data/lib/cached_nested_file_reader.rb +0 -1
- data/lib/colorize.rb +37 -23
- data/lib/fcb.rb +12 -30
- data/lib/filter.rb +13 -9
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +488 -249
- data/lib/mdoc.rb +73 -53
- data/lib/menu.src.yml +323 -267
- data/lib/menu.yml +324 -268
- metadata +17 -6
- data/lib/env_opts.rb +0 -242
- data/lib/markdown_block_manager.rb +0 -64
- data/lib/menu_options.rb +0 -0
- data/lib/menu_options.yml +0 -0
data/lib/markdown_exec.rb
CHANGED
@@ -37,6 +37,13 @@ $stdout.sync = true
|
|
37
37
|
|
38
38
|
BLOCK_SIZE = 1024
|
39
39
|
|
40
|
+
# macros
|
41
|
+
#
|
42
|
+
BACK_OPTION = '* Back'
|
43
|
+
EXIT_OPTION = '* Exit'
|
44
|
+
LOAD_FILE = true
|
45
|
+
VN = 'MDE_MENU_HISTORY'
|
46
|
+
|
40
47
|
# custom error: file specified is missing
|
41
48
|
#
|
42
49
|
class FileMissingError < StandardError; end
|
@@ -58,6 +65,18 @@ class Hash
|
|
58
65
|
end
|
59
66
|
end
|
60
67
|
|
68
|
+
# integer value for comparison
|
69
|
+
#
|
70
|
+
def options_fetch_display_level(options)
|
71
|
+
options.fetch(:display_level, 1)
|
72
|
+
end
|
73
|
+
|
74
|
+
# integer value for comparison
|
75
|
+
#
|
76
|
+
def options_fetch_display_level_xbase_prefix(options)
|
77
|
+
options.fetch(:level_xbase_prefix, '')
|
78
|
+
end
|
79
|
+
|
61
80
|
# stdout manager
|
62
81
|
#
|
63
82
|
module FOUT
|
@@ -77,7 +96,7 @@ module FOUT
|
|
77
96
|
end
|
78
97
|
|
79
98
|
def approved_fout?(level)
|
80
|
-
level <= @options
|
99
|
+
level <= options_fetch_display_level(@options)
|
81
100
|
end
|
82
101
|
|
83
102
|
# display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
|
@@ -85,7 +104,7 @@ module FOUT
|
|
85
104
|
def lout(str, level: DISPLAY_LEVEL_BASE)
|
86
105
|
return unless approved_fout? level
|
87
106
|
|
88
|
-
fout level == DISPLAY_LEVEL_BASE ? str : @options
|
107
|
+
fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
|
89
108
|
end
|
90
109
|
end
|
91
110
|
|
@@ -121,6 +140,13 @@ module MarkdownExec
|
|
121
140
|
FNR11 = '/'
|
122
141
|
FNR12 = ',~'
|
123
142
|
|
143
|
+
SHELL_COLOR_OPTIONS = {
|
144
|
+
'bash' => :menu_bash_color,
|
145
|
+
BLOCK_TYPE_LINK => :menu_link_color,
|
146
|
+
'opts' => :menu_opts_color,
|
147
|
+
'vars' => :menu_vars_color
|
148
|
+
}.freeze
|
149
|
+
|
124
150
|
##
|
125
151
|
#
|
126
152
|
# rubocop:disable Layout/LineLength
|
@@ -162,17 +188,6 @@ module MarkdownExec
|
|
162
188
|
end
|
163
189
|
end
|
164
190
|
|
165
|
-
# return arguments after `--`
|
166
|
-
#
|
167
|
-
def arguments_for_child(argv = ARGV)
|
168
|
-
case ind = argv.find_index('--')
|
169
|
-
when nil, argv.count - 1
|
170
|
-
[]
|
171
|
-
else
|
172
|
-
argv[ind + 1..-1]
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
191
|
##
|
177
192
|
# options necessary to start, parse input, defaults for cli options
|
178
193
|
#
|
@@ -208,124 +223,170 @@ module MarkdownExec
|
|
208
223
|
# @param opts [Hash] Options hash containing configuration settings.
|
209
224
|
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
210
225
|
# @return [String] The name of the executed code block.
|
226
|
+
#
|
211
227
|
def approve_and_execute_block(opts, mdoc)
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
228
|
+
selected = mdoc.get_block_by_name(opts[:block_name])
|
229
|
+
if selected[:shell] == BLOCK_TYPE_LINK
|
230
|
+
handle_link_shell(opts, selected)
|
231
|
+
elsif selected[:shell] == 'opts'
|
232
|
+
handle_opts_shell(opts, selected)
|
233
|
+
else
|
234
|
+
required_lines = collect_required_code_blocks(opts, mdoc, selected)
|
235
|
+
# Display required code blocks if requested or required approval.
|
236
|
+
if opts[:output_script] || opts[:user_must_approve]
|
237
|
+
display_required_code(opts, required_lines)
|
238
|
+
end
|
239
|
+
|
240
|
+
allow = true
|
241
|
+
allow = user_approval(opts, required_lines) if opts[:user_must_approve]
|
242
|
+
opts[:ir_approve] = allow
|
243
|
+
mdoc.get_block_by_name(opts[:block_name])
|
244
|
+
execute_approved_block(opts, required_lines) if opts[:ir_approve]
|
245
|
+
|
246
|
+
[!LOAD_FILE, '']
|
218
247
|
end
|
248
|
+
end
|
219
249
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
end
|
233
|
-
allow = (sel == 1)
|
234
|
-
if sel == 3
|
235
|
-
# Copy the code to the clipboard.
|
236
|
-
text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
|
237
|
-
Clipboard.copy(text)
|
238
|
-
fout "Clipboard updated: #{required_blocks.count} blocks," /
|
239
|
-
" #{required_blocks.flatten.count} lines," /
|
240
|
-
" #{text.length} characters"
|
241
|
-
end
|
242
|
-
if sel == 4
|
243
|
-
# Save the code to a file.
|
244
|
-
write_command_file(opts.merge(save_executed_script: true),
|
245
|
-
required_blocks)
|
246
|
-
fout "File saved: #{@options[:saved_filespec]}"
|
247
|
-
end
|
248
|
-
break if [1, 2].include? sel
|
249
|
-
end
|
250
|
+
def handle_link_shell(opts, selected)
|
251
|
+
data = YAML.load(selected[:body].join("\n"))
|
252
|
+
|
253
|
+
# add to front of history
|
254
|
+
#
|
255
|
+
ENV[VN] = opts[:filename] + opts[:history_document_separator] + ENV.fetch(VN, '')
|
256
|
+
|
257
|
+
opts[:filename] = data.fetch('file', nil)
|
258
|
+
return !LOAD_FILE unless opts[:filename]
|
259
|
+
|
260
|
+
data.fetch('vars', []).each do |var|
|
261
|
+
ENV[var[0]] = var[1].to_s
|
250
262
|
end
|
251
263
|
|
252
|
-
|
264
|
+
[LOAD_FILE, data.fetch('block', '')]
|
265
|
+
end
|
253
266
|
|
254
|
-
|
255
|
-
|
267
|
+
def handle_opts_shell(opts, selected)
|
268
|
+
data = YAML.load(selected[:body].join("\n"))
|
269
|
+
data.each_key do |key|
|
270
|
+
opts[key.to_sym] = value = data[key].to_s
|
271
|
+
next unless opts[:menu_opts_set_format].present?
|
272
|
+
|
273
|
+
print format(
|
274
|
+
opts[:menu_opts_set_format],
|
275
|
+
{ key: key,
|
276
|
+
value: value }
|
277
|
+
).send(opts[:menu_opts_set_color].to_sym)
|
278
|
+
end
|
279
|
+
[!LOAD_FILE, '']
|
280
|
+
end
|
256
281
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
282
|
+
def user_approval(opts, required_lines)
|
283
|
+
# Present a selection menu for user approval.
|
284
|
+
sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
|
285
|
+
menu.default 1
|
286
|
+
menu.choice opts[:prompt_yes], 1
|
287
|
+
menu.choice opts[:prompt_no], 2
|
288
|
+
menu.choice opts[:prompt_script_to_clipboard], 3
|
289
|
+
menu.choice opts[:prompt_save_script], 4
|
264
290
|
end
|
265
291
|
|
266
|
-
|
292
|
+
if sel == 3
|
293
|
+
copy_to_clipboard(required_lines)
|
294
|
+
elsif sel == 4
|
295
|
+
save_to_file(opts, required_lines)
|
296
|
+
end
|
297
|
+
|
298
|
+
sel == 1
|
299
|
+
end
|
300
|
+
|
301
|
+
def execute_approved_block(opts, required_lines)
|
302
|
+
write_command_file(opts, required_lines)
|
303
|
+
command_execute(
|
304
|
+
opts,
|
305
|
+
required_lines.flatten.join("\n"),
|
306
|
+
args: opts.fetch(:pass_args, [])
|
307
|
+
)
|
308
|
+
save_execution_output
|
309
|
+
output_execution_summary
|
310
|
+
output_execution_result
|
311
|
+
end
|
312
|
+
|
313
|
+
# Collect required code blocks based on the provided options.
|
314
|
+
#
|
315
|
+
# @param opts [Hash] Options hash containing configuration settings.
|
316
|
+
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
317
|
+
# @return [Array<String>] Required code blocks as an array of lines.
|
318
|
+
def collect_required_code_blocks(opts, mdoc, selected)
|
319
|
+
required = mdoc.collect_recursively_required_code(opts[:block_name])
|
320
|
+
required_lines = required[:code]
|
321
|
+
required[:blocks]
|
322
|
+
|
323
|
+
# Apply hash in opts block to environment variables
|
324
|
+
if selected[:shell] == BLOCK_TYPE_VARS
|
325
|
+
data = YAML.load(selected[:body].join("\n"))
|
326
|
+
data.each_key do |key|
|
327
|
+
ENV[key] = value = data[key].to_s
|
328
|
+
next unless opts[:menu_vars_set_format].present?
|
329
|
+
|
330
|
+
print format(
|
331
|
+
opts[:menu_vars_set_format],
|
332
|
+
{ key: key,
|
333
|
+
value: value }
|
334
|
+
).send(opts[:menu_vars_set_color].to_sym)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
required_lines
|
267
339
|
end
|
268
340
|
|
269
341
|
def cfile
|
270
342
|
@cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern))
|
271
343
|
end
|
272
344
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
345
|
+
EF_STDOUT = :stdout
|
346
|
+
EF_STDERR = :stderr
|
347
|
+
EF_STDIN = :stdin
|
348
|
+
|
349
|
+
# Handles reading and processing lines from a given IO stream
|
350
|
+
#
|
351
|
+
# @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
|
352
|
+
# @param file_type [Symbol] The type of file to which the stream corresponds.
|
353
|
+
def handle_stream(opts, stream, file_type, swap: false)
|
354
|
+
Thread.new do
|
355
|
+
until (line = stream.gets).nil?
|
356
|
+
@execute_files[file_type] = @execute_files[file_type] + [line.strip]
|
357
|
+
print line if opts[:output_stdout]
|
358
|
+
yield line if block_given?
|
359
|
+
end
|
360
|
+
rescue IOError
|
361
|
+
#d 'stdout IOError, thread killed, do nothing'
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Existing command_execute method
|
366
|
+
def command_execute(opts, command, args: [])
|
278
367
|
@execute_files = Hash.new([])
|
279
368
|
@execute_options = opts
|
280
369
|
@execute_started_at = Time.now.utc
|
281
370
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
Thread.new do
|
287
|
-
until (line = stdout.gets).nil?
|
288
|
-
@execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
|
289
|
-
print line if opts[:output_stdout]
|
290
|
-
yield nil, line, nil, exec_thr if block_given?
|
291
|
-
end
|
292
|
-
rescue IOError
|
293
|
-
#d 'stdout IOError, thread killed, do nothing'
|
371
|
+
Open3.popen3(opts[:shell], '-c', command, opts[:filename],
|
372
|
+
*args) do |stdin, stdout, stderr, exec_thr|
|
373
|
+
handle_stream(opts, stdout, EF_STDOUT) do |line|
|
374
|
+
yield nil, line, nil, exec_thr if block_given?
|
294
375
|
end
|
295
|
-
|
296
|
-
|
297
|
-
until (line = stderr.gets).nil?
|
298
|
-
@execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
|
299
|
-
print line if opts[:output_stdout]
|
300
|
-
yield nil, nil, line, exec_thr if block_given?
|
301
|
-
end
|
302
|
-
rescue IOError
|
303
|
-
#d 'stderr IOError, thread killed, do nothing'
|
376
|
+
handle_stream(opts, stderr, EF_STDERR) do |line|
|
377
|
+
yield nil, nil, line, exec_thr if block_given?
|
304
378
|
end
|
305
379
|
|
306
|
-
in_thr =
|
307
|
-
|
308
|
-
|
309
|
-
@execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
|
310
|
-
yield line, nil, nil, exec_thr if block_given?
|
311
|
-
end
|
312
|
-
#d 'exec_thr now dead'
|
313
|
-
rescue StandardError
|
314
|
-
#d 'stdin error, thread killed, do nothing'
|
380
|
+
in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
|
381
|
+
stdin.puts(line)
|
382
|
+
yield line, nil, nil, exec_thr if block_given?
|
315
383
|
end
|
316
384
|
|
317
|
-
#d 'join exec_thr'
|
318
385
|
exec_thr.join
|
319
|
-
|
320
|
-
#d 'wait before closing stdin'
|
321
386
|
sleep 0.1
|
322
|
-
|
323
|
-
#d 'kill stdin thread'
|
324
|
-
in_thr.kill
|
325
|
-
# @return_code = exec_thr.value
|
326
|
-
#d 'command end'
|
387
|
+
in_thr.kill if in_thr&.alive?
|
327
388
|
end
|
328
|
-
|
389
|
+
|
329
390
|
@execute_completed_at = Time.now.utc
|
330
391
|
rescue Errno::ENOENT => err
|
331
392
|
#d 'command error ENOENT triggered by missing command in script'
|
@@ -344,19 +405,19 @@ module MarkdownExec
|
|
344
405
|
end
|
345
406
|
|
346
407
|
def count_blocks_in_filename
|
347
|
-
|
408
|
+
fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
|
348
409
|
cnt = 0
|
349
410
|
cfile.readlines(@options[:filename]).each do |line|
|
350
|
-
cnt += 1 if line.match(
|
411
|
+
cnt += 1 if line.match(fenced_start_and_end_regex)
|
351
412
|
end
|
352
413
|
cnt / 2
|
353
414
|
end
|
354
415
|
|
355
416
|
# :reek:DuplicateMethodCall
|
356
|
-
def display_required_code(opts,
|
417
|
+
def display_required_code(opts, required_lines)
|
357
418
|
frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
|
358
419
|
fout frame
|
359
|
-
|
420
|
+
required_lines.each { |cb| fout cb }
|
360
421
|
fout frame
|
361
422
|
end
|
362
423
|
|
@@ -426,7 +487,6 @@ module MarkdownExec
|
|
426
487
|
def get_block_summary(call_options, fcb)
|
427
488
|
opts = optsmerge call_options
|
428
489
|
# return fcb.body unless opts[:struct]
|
429
|
-
|
430
490
|
return fcb unless opts[:bash]
|
431
491
|
|
432
492
|
fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
|
@@ -438,16 +498,23 @@ module MarkdownExec
|
|
438
498
|
bm = option_match_groups(titlexcall, opts[:block_name_match])
|
439
499
|
fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan])
|
440
500
|
fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan])
|
441
|
-
|
501
|
+
|
502
|
+
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
503
|
+
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
504
|
+
fcb.dname = if shell_color_option && opts[shell_color_option].present?
|
505
|
+
fcb.oname.send(opts[shell_color_option].to_sym)
|
506
|
+
else
|
507
|
+
fcb.oname
|
508
|
+
end
|
442
509
|
fcb
|
443
510
|
end
|
444
511
|
|
445
512
|
# :reek:DuplicateMethodCall
|
446
513
|
# :reek:LongYieldList
|
447
514
|
# :reek:NestedIterators
|
448
|
-
|
449
|
-
# opts = optsmerge call_options, options_block
|
515
|
+
#---
|
450
516
|
|
517
|
+
def iter_blocks_in_file(opts = {}, &block)
|
451
518
|
unless opts[:filename]&.present?
|
452
519
|
fout 'No blocks found.'
|
453
520
|
return
|
@@ -458,10 +525,10 @@ module MarkdownExec
|
|
458
525
|
return
|
459
526
|
end
|
460
527
|
|
461
|
-
|
462
|
-
|
528
|
+
fenced_start_and_end_regex = Regexp.new opts[:fenced_start_and_end_regex]
|
529
|
+
fenced_start_extended_regex = Regexp.new opts[:fenced_start_extended_regex]
|
463
530
|
fcb = FCB.new
|
464
|
-
|
531
|
+
in_fenced_block = false
|
465
532
|
headings = []
|
466
533
|
|
467
534
|
## get type of messages to select
|
@@ -470,75 +537,80 @@ module MarkdownExec
|
|
470
537
|
|
471
538
|
cfile.readlines(opts[:filename]).each.with_index do |line, _line_num|
|
472
539
|
continue unless line
|
540
|
+
headings = update_headings(line, headings, opts) if opts[:menu_blocks_with_headings]
|
473
541
|
|
474
|
-
if
|
475
|
-
if
|
476
|
-
|
477
|
-
|
478
|
-
headings = [headings[0], lm[:name]]
|
479
|
-
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
480
|
-
headings = [lm[:name]]
|
481
|
-
end
|
482
|
-
end
|
483
|
-
|
484
|
-
if line.match(fenced_start_and_end_match)
|
485
|
-
if in_block
|
486
|
-
# end fcb
|
487
|
-
#
|
488
|
-
fcb.name = fcb.title || ''
|
489
|
-
if fcb.body
|
490
|
-
if fcb.title.nil? || fcb.title.empty?
|
491
|
-
fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64]
|
492
|
-
end
|
493
|
-
|
494
|
-
if block_given? &&
|
495
|
-
selected_messages.include?(:blocks) &&
|
496
|
-
Filter.fcb_select?(opts, fcb)
|
497
|
-
yield :blocks, fcb
|
498
|
-
end
|
499
|
-
end
|
500
|
-
in_block = false
|
542
|
+
if line.match(fenced_start_and_end_regex)
|
543
|
+
if in_fenced_block
|
544
|
+
process_fenced_block(fcb, opts, selected_messages, &block)
|
545
|
+
in_fenced_block = false
|
501
546
|
else
|
502
|
-
|
503
|
-
|
504
|
-
in_block = true
|
505
|
-
|
506
|
-
fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys
|
507
|
-
fcb = FCB.new
|
508
|
-
fcb.headings = headings
|
509
|
-
fcb.name = fcb_title_groups.fetch(:name, '')
|
510
|
-
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
511
|
-
fcb.title = fcb_title_groups.fetch(:name, '')
|
512
|
-
|
513
|
-
# selected fcb
|
514
|
-
#
|
515
|
-
fcb.body = []
|
516
|
-
|
517
|
-
rest = fcb_title_groups.fetch(:rest, '')
|
518
|
-
fcb.reqs, fcb.wraps =
|
519
|
-
split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
|
520
|
-
!name.match(Regexp.new(opts[:block_name_wrapper_match]))
|
521
|
-
end
|
522
|
-
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
523
|
-
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
524
|
-
tn.named_captures.sym_keys
|
525
|
-
end
|
526
|
-
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
527
|
-
tn.named_captures.sym_keys
|
528
|
-
end
|
547
|
+
fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
548
|
+
in_fenced_block = true
|
529
549
|
end
|
530
|
-
elsif
|
550
|
+
elsif in_fenced_block && fcb.body
|
531
551
|
dp 'append line to fcb body'
|
532
552
|
fcb.body += [line.chomp]
|
533
|
-
|
534
|
-
|
535
|
-
fcb = FCB.new
|
536
|
-
fcb.body = [line]
|
537
|
-
yield :line, fcb
|
553
|
+
else
|
554
|
+
process_line(line, opts, selected_messages, &block)
|
538
555
|
end
|
539
556
|
end
|
540
557
|
end
|
541
558
|
|
559
|
+
def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
560
|
+
fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
|
561
|
+
fcb = FCB.new
|
562
|
+
fcb.headings = headings
|
563
|
+
fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
|
564
|
+
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
565
|
+
fcb.title = fcb_title_groups.fetch(:name, '')
|
566
|
+
|
567
|
+
# selected fcb
|
568
|
+
fcb.body = []
|
569
|
+
|
570
|
+
rest = fcb_title_groups.fetch(:rest, '')
|
571
|
+
fcb.reqs, fcb.wraps =
|
572
|
+
split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
|
573
|
+
!name.match(Regexp.new(opts[:block_name_wrapper_match]))
|
574
|
+
end
|
575
|
+
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
576
|
+
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
577
|
+
tn.named_captures.sym_keys
|
578
|
+
end
|
579
|
+
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
580
|
+
tn.named_captures.sym_keys
|
581
|
+
end
|
582
|
+
fcb
|
583
|
+
end
|
584
|
+
|
585
|
+
def process_fenced_block(fcb, opts, selected_messages, &block)
|
586
|
+
fcb.oname = fcb.dname = fcb.title || ''
|
587
|
+
return unless fcb.body
|
588
|
+
|
589
|
+
set_fcb_title(fcb)
|
590
|
+
|
591
|
+
if block &&
|
592
|
+
selected_messages.include?(:blocks) &&
|
593
|
+
Filter.fcb_select?(opts, fcb)
|
594
|
+
block.call :blocks, fcb
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
def process_line(line, _opts, selected_messages, &block)
|
599
|
+
return unless block && selected_messages.include?(:line)
|
600
|
+
|
601
|
+
# dp 'text outside of fcb'
|
602
|
+
fcb = FCB.new
|
603
|
+
fcb.body = [line]
|
604
|
+
block.call(:line, fcb)
|
605
|
+
end
|
606
|
+
|
607
|
+
# set the title of an FCB object based on its body if it is nil or empty
|
608
|
+
def set_fcb_title(fcb)
|
609
|
+
return unless fcb.title.nil? || fcb.title.empty?
|
610
|
+
|
611
|
+
fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
|
612
|
+
end
|
613
|
+
|
542
614
|
def split_array(arr)
|
543
615
|
true_list = []
|
544
616
|
false_list = []
|
@@ -554,11 +626,17 @@ module MarkdownExec
|
|
554
626
|
[true_list, false_list]
|
555
627
|
end
|
556
628
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
629
|
+
def update_headings(line, headings, opts)
|
630
|
+
if (lm = line.match(Regexp.new(opts[:heading3_match])))
|
631
|
+
[headings[0], headings[1], lm[:name]]
|
632
|
+
elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
|
633
|
+
[headings[0], lm[:name]]
|
634
|
+
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
635
|
+
[lm[:name]]
|
636
|
+
else
|
637
|
+
headings
|
638
|
+
end
|
639
|
+
end
|
562
640
|
|
563
641
|
# return body, title if option.struct
|
564
642
|
# return body if not struct
|
@@ -572,10 +650,11 @@ module MarkdownExec
|
|
572
650
|
blocks.push FCB.new({
|
573
651
|
# name: '',
|
574
652
|
chrome: true,
|
575
|
-
|
653
|
+
dname: format(
|
576
654
|
opts[:menu_divider_format],
|
577
655
|
opts[:menu_initial_divider]
|
578
656
|
).send(opts[:menu_divider_color].to_sym),
|
657
|
+
oname: opts[:menu_initial_divider],
|
579
658
|
disabled: '' # __LINE__.to_s
|
580
659
|
})
|
581
660
|
end
|
@@ -596,8 +675,9 @@ module MarkdownExec
|
|
596
675
|
blocks.push FCB.new(
|
597
676
|
{ chrome: true,
|
598
677
|
disabled: '',
|
599
|
-
|
600
|
-
|
678
|
+
dname: format(opts[:menu_divider_format],
|
679
|
+
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
680
|
+
oname: mbody[:name] }
|
601
681
|
)
|
602
682
|
end
|
603
683
|
elsif opts[:menu_task_match].present? &&
|
@@ -606,10 +686,14 @@ module MarkdownExec
|
|
606
686
|
blocks.push FCB.new(
|
607
687
|
{ chrome: true,
|
608
688
|
disabled: '',
|
609
|
-
|
689
|
+
dname: format(
|
610
690
|
opts[:menu_task_format],
|
611
691
|
$~.named_captures.transform_keys(&:to_sym)
|
612
|
-
).send(opts[:menu_task_color].to_sym)
|
692
|
+
).send(opts[:menu_task_color].to_sym),
|
693
|
+
oname: format(
|
694
|
+
opts[:menu_task_format],
|
695
|
+
$~.named_captures.transform_keys(&:to_sym)
|
696
|
+
) }
|
613
697
|
)
|
614
698
|
end
|
615
699
|
else
|
@@ -626,9 +710,10 @@ module MarkdownExec
|
|
626
710
|
blocks.push FCB.new(
|
627
711
|
{ chrome: true,
|
628
712
|
disabled: '',
|
629
|
-
|
630
|
-
|
631
|
-
.send(opts[:menu_divider_color].to_sym)
|
713
|
+
dname: format(opts[:menu_divider_format],
|
714
|
+
opts[:menu_final_divider])
|
715
|
+
.send(opts[:menu_divider_color].to_sym),
|
716
|
+
oname: opts[:menu_final_divider] }
|
632
717
|
)
|
633
718
|
end
|
634
719
|
blocks
|
@@ -709,7 +794,7 @@ module MarkdownExec
|
|
709
794
|
else
|
710
795
|
# blocks.map(&:name)
|
711
796
|
blocks.map do |block|
|
712
|
-
block.fetch(:text, nil) || block.
|
797
|
+
block.fetch(:text, nil) || block.oname
|
713
798
|
end
|
714
799
|
end.compact.reject(&:empty?)
|
715
800
|
end
|
@@ -802,10 +887,10 @@ module MarkdownExec
|
|
802
887
|
when :line
|
803
888
|
if options[:menu_divider_match] &&
|
804
889
|
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
805
|
-
menu.push FCB.new({ name: mbody[:name], disabled: '' })
|
890
|
+
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
|
806
891
|
end
|
807
892
|
when :blocks
|
808
|
-
menu += [fcb.
|
893
|
+
menu += [fcb.oname]
|
809
894
|
end
|
810
895
|
end
|
811
896
|
menu
|
@@ -851,7 +936,7 @@ module MarkdownExec
|
|
851
936
|
def options_finalize(rest)
|
852
937
|
## position 0: file or folder (optional)
|
853
938
|
#
|
854
|
-
if (pos = rest.
|
939
|
+
if (pos = rest.shift)&.present?
|
855
940
|
if Dir.exist?(pos)
|
856
941
|
@options[:path] = pos
|
857
942
|
elsif File.exist?(pos)
|
@@ -863,7 +948,7 @@ module MarkdownExec
|
|
863
948
|
|
864
949
|
## position 1: block name (optional)
|
865
950
|
#
|
866
|
-
block_name = rest.
|
951
|
+
block_name = rest.shift
|
867
952
|
@options[:block_name] = block_name if block_name.present?
|
868
953
|
end
|
869
954
|
|
@@ -910,13 +995,31 @@ module MarkdownExec
|
|
910
995
|
}
|
911
996
|
end
|
912
997
|
|
998
|
+
## insert back option at head or tail
|
999
|
+
#
|
1000
|
+
## Adds a back option at the head or tail of a menu
|
1001
|
+
#
|
1002
|
+
def prompt_menu_add_back(items, label = BACK_OPTION)
|
1003
|
+
return items unless @options[:menu_with_back]
|
1004
|
+
|
1005
|
+
history = ENV.fetch('MDE_MENU_HISTORY', '')
|
1006
|
+
return items unless history.present?
|
1007
|
+
|
1008
|
+
@hs_curr, @hs_rest = split_string_on_first_char(
|
1009
|
+
history,
|
1010
|
+
@options[:history_document_separator]
|
1011
|
+
)
|
1012
|
+
|
1013
|
+
@options[:menu_back_at_top] ? [label] + items : items + [label]
|
1014
|
+
end
|
1015
|
+
|
913
1016
|
## insert exit option at head or tail
|
914
1017
|
#
|
915
|
-
def prompt_menu_add_exit(
|
1018
|
+
def prompt_menu_add_exit(items, label = EXIT_OPTION)
|
916
1019
|
if @options[:menu_exit_at_top]
|
917
|
-
(@options[:menu_with_exit] ? [
|
1020
|
+
(@options[:menu_with_exit] ? [label] : []) + items
|
918
1021
|
else
|
919
|
-
items + (@options[:menu_with_exit] ? [
|
1022
|
+
items + (@options[:menu_with_exit] ? [label] : [])
|
920
1023
|
end
|
921
1024
|
end
|
922
1025
|
|
@@ -925,10 +1028,31 @@ module MarkdownExec
|
|
925
1028
|
# return selected option or nil
|
926
1029
|
#
|
927
1030
|
def prompt_with_quit(prompt_text, items, opts = {})
|
928
|
-
|
929
|
-
|
1031
|
+
obj = prompt_with_quit2(prompt_text, items, opts)
|
1032
|
+
if obj.fetch(:option, nil)
|
1033
|
+
nil
|
1034
|
+
else
|
1035
|
+
obj[:selected]
|
1036
|
+
end
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
## tty prompt to select
|
1040
|
+
# insert exit option at head or tail
|
1041
|
+
# return option:, selected option:
|
1042
|
+
#
|
1043
|
+
def prompt_with_quit2(prompt_text, items, opts = {})
|
1044
|
+
sel = @prompt.select(prompt_text,
|
1045
|
+
prompt_menu_add_exit(
|
1046
|
+
prompt_menu_add_back(items)
|
1047
|
+
),
|
930
1048
|
opts.merge(filter: true))
|
931
|
-
sel ==
|
1049
|
+
if sel == BACK_OPTION
|
1050
|
+
{ option: sel, curr: @hs_curr, rest: @hs_rest }
|
1051
|
+
elsif sel == EXIT_OPTION
|
1052
|
+
{ option: sel }
|
1053
|
+
else
|
1054
|
+
{ selected: sel }
|
1055
|
+
end
|
932
1056
|
end
|
933
1057
|
|
934
1058
|
# :reek:UtilityFunction ### temp
|
@@ -941,12 +1065,8 @@ module MarkdownExec
|
|
941
1065
|
|
942
1066
|
# :reek:NestedIterators
|
943
1067
|
def run
|
944
|
-
## default configuration
|
945
|
-
#
|
946
1068
|
@options = base_options
|
947
1069
|
|
948
|
-
## read local configuration file
|
949
|
-
#
|
950
1070
|
read_configuration_file! @options,
|
951
1071
|
".#{MarkdownExec::APP_NAME.downcase}.yml"
|
952
1072
|
|
@@ -962,12 +1082,13 @@ module MarkdownExec
|
|
962
1082
|
menu_option_append opts, options, item
|
963
1083
|
end
|
964
1084
|
end
|
965
|
-
option_parser.load
|
966
|
-
|
967
|
-
option_parser.environment # env defaults to the basename of the program
|
968
|
-
# child_argv = arguments_for_child
|
1085
|
+
option_parser.load
|
1086
|
+
option_parser.environment
|
969
1087
|
rest = option_parser.parse!(arguments_for_mde) # (into: options)
|
970
1088
|
|
1089
|
+
# pass through arguments excluded from OptionParser with `--`
|
1090
|
+
@options[:pass_args] = ARGV[rest.count + 1..]
|
1091
|
+
|
971
1092
|
begin
|
972
1093
|
options_finalize rest
|
973
1094
|
exec_block options, options[:block_name]
|
@@ -1026,6 +1147,31 @@ module MarkdownExec
|
|
1026
1147
|
File.write(@options[:logged_stdout_filespec], ol.join)
|
1027
1148
|
end
|
1028
1149
|
|
1150
|
+
# Prepare the blocks menu by adding labels and other necessary details.
|
1151
|
+
#
|
1152
|
+
# @param blocks_in_file [Array<Hash>] The list of blocks from the file.
|
1153
|
+
# @param opts [Hash] The options hash.
|
1154
|
+
# @return [Array<Hash>] The updated blocks menu.
|
1155
|
+
def prepare_blocks_menu(blocks_in_file, opts)
|
1156
|
+
# next if fcb.fetch(:disabled, false)
|
1157
|
+
# next unless fcb.fetch(:name, '').present?
|
1158
|
+
blocks_in_file.map do |fcb|
|
1159
|
+
fcb.merge!(
|
1160
|
+
name: fcb.dname,
|
1161
|
+
label: BlockLabel.make(
|
1162
|
+
body: fcb[:body],
|
1163
|
+
filename: opts[:filename],
|
1164
|
+
headings: fcb.fetch(:headings, []),
|
1165
|
+
menu_blocks_with_docname: opts[:menu_blocks_with_docname],
|
1166
|
+
menu_blocks_with_headings: opts[:menu_blocks_with_headings],
|
1167
|
+
text: fcb[:text],
|
1168
|
+
title: fcb[:title]
|
1169
|
+
)
|
1170
|
+
)
|
1171
|
+
fcb.to_h
|
1172
|
+
end.compact
|
1173
|
+
end
|
1174
|
+
|
1029
1175
|
# Select and execute a code block from a Markdown document.
|
1030
1176
|
#
|
1031
1177
|
# This method allows the user to interactively select a code block from a
|
@@ -1036,51 +1182,63 @@ module MarkdownExec
|
|
1036
1182
|
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1037
1183
|
def select_approve_and_execute_block(call_options, &options_block)
|
1038
1184
|
opts = optsmerge(call_options, options_block)
|
1039
|
-
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
1040
|
-
mdoc = MDoc.new(blocks_in_file) do |nopts|
|
1041
|
-
opts.merge!(nopts)
|
1042
|
-
end
|
1043
|
-
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
1044
|
-
|
1045
1185
|
repeat_menu = true && !opts[:block_name].present?
|
1186
|
+
|
1187
|
+
load_file = !LOAD_FILE
|
1046
1188
|
loop do
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1189
|
+
# load file
|
1190
|
+
#
|
1191
|
+
loop do
|
1192
|
+
# repeat menu
|
1193
|
+
#
|
1194
|
+
load_file = !LOAD_FILE
|
1195
|
+
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
1196
|
+
mdoc = MDoc.new(blocks_in_file) do |nopts|
|
1197
|
+
opts.merge!(nopts)
|
1198
|
+
end
|
1199
|
+
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
1200
|
+
unless opts[:block_name].present?
|
1201
|
+
pt = opts[:prompt_select_block].to_s
|
1202
|
+
bm = prepare_blocks_menu(blocks_menu, opts)
|
1203
|
+
return nil if bm.count.zero?
|
1204
|
+
|
1205
|
+
# sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
|
1206
|
+
# return nil if sel.nil?
|
1207
|
+
obj = prompt_with_quit2(pt, bm, per_page: opts[:select_page_height])
|
1208
|
+
case obj.fetch(:option, nil)
|
1209
|
+
when EXIT_OPTION
|
1210
|
+
return nil
|
1211
|
+
when BACK_OPTION
|
1212
|
+
opts[:filename] = obj[:curr]
|
1213
|
+
opts[:block_name] = @options[:block_name] = ''
|
1214
|
+
ENV['MDE_MENU_HISTORY'] = obj[:rest]
|
1215
|
+
load_file = LOAD_FILE # later: exit menu, load file
|
1216
|
+
else
|
1217
|
+
sel = obj[:selected]
|
1218
|
+
|
1219
|
+
## store selected option
|
1220
|
+
#
|
1221
|
+
label_block = blocks_in_file.select do |fcb|
|
1222
|
+
fcb.dname == sel
|
1223
|
+
end.fetch(0, nil)
|
1224
|
+
opts[:block_name] = @options[:block_name] = label_block.oname
|
1225
|
+
end
|
1226
|
+
end
|
1227
|
+
break if load_file == LOAD_FILE
|
1065
1228
|
|
1066
|
-
|
1067
|
-
end.compact
|
1068
|
-
return nil if bm.count.zero?
|
1229
|
+
# later: load file
|
1069
1230
|
|
1070
|
-
|
1071
|
-
return nil if sel.nil?
|
1231
|
+
load_file, block_name = approve_and_execute_block(opts, mdoc)
|
1072
1232
|
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
end
|
1078
|
-
opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
|
1079
|
-
end
|
1080
|
-
approve_and_execute_block(opts, mdoc)
|
1081
|
-
break unless repeat_menu
|
1233
|
+
opts[:block_name] = block_name
|
1234
|
+
if load_file == LOAD_FILE
|
1235
|
+
repeat_menu = true
|
1236
|
+
break
|
1237
|
+
end
|
1082
1238
|
|
1083
|
-
|
1239
|
+
break unless repeat_menu
|
1240
|
+
end
|
1241
|
+
break if load_file != LOAD_FILE
|
1084
1242
|
end
|
1085
1243
|
rescue StandardError => err
|
1086
1244
|
warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
|
@@ -1142,6 +1300,22 @@ module MarkdownExec
|
|
1142
1300
|
end.to_yaml
|
1143
1301
|
end
|
1144
1302
|
|
1303
|
+
# Splits the given string on the first occurrence of the specified character.
|
1304
|
+
# Returns an array containing the portion of the string before the character and the rest of the string.
|
1305
|
+
#
|
1306
|
+
# @param input_str [String] The string to be split.
|
1307
|
+
# @param split_char [String] The character on which to split the string.
|
1308
|
+
# @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
|
1309
|
+
def split_string_on_first_char(input_str, split_char)
|
1310
|
+
split_index = input_str.index(split_char)
|
1311
|
+
|
1312
|
+
if split_index.nil?
|
1313
|
+
[input_str, '']
|
1314
|
+
else
|
1315
|
+
[input_str[0...split_index], input_str[(split_index + 1)..-1]]
|
1316
|
+
end
|
1317
|
+
end
|
1318
|
+
|
1145
1319
|
def tab_completions(data = menu_for_optparse)
|
1146
1320
|
data.map do |item|
|
1147
1321
|
"--#{item[:long_name]}" if item[:long_name]
|
@@ -1159,7 +1333,7 @@ module MarkdownExec
|
|
1159
1333
|
@options
|
1160
1334
|
end
|
1161
1335
|
|
1162
|
-
def write_command_file(call_options,
|
1336
|
+
def write_command_file(call_options, required_lines)
|
1163
1337
|
return unless call_options[:save_executed_script]
|
1164
1338
|
|
1165
1339
|
time_now = Time.now.utc
|
@@ -1186,7 +1360,7 @@ module MarkdownExec
|
|
1186
1360
|
"# file_name: #{opts[:filename]}\n" \
|
1187
1361
|
"# block_name: #{opts[:block_name]}\n" \
|
1188
1362
|
"# time: #{time_now}\n" \
|
1189
|
-
"#{
|
1363
|
+
"#{required_lines.flatten.join("\n")}\n")
|
1190
1364
|
return if @options[:saved_script_chmod].zero?
|
1191
1365
|
|
1192
1366
|
File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
|
@@ -1194,4 +1368,69 @@ module MarkdownExec
|
|
1194
1368
|
end # class MarkParse
|
1195
1369
|
end # module MarkdownExec
|
1196
1370
|
|
1197
|
-
|
1371
|
+
if $PROGRAM_NAME == __FILE__
|
1372
|
+
require 'bundler/setup'
|
1373
|
+
Bundler.require(:default)
|
1374
|
+
|
1375
|
+
require 'minitest/autorun'
|
1376
|
+
|
1377
|
+
module MarkdownExec
|
1378
|
+
class TestMarkParse < Minitest::Test
|
1379
|
+
require 'mocha/minitest'
|
1380
|
+
|
1381
|
+
def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
|
1382
|
+
pigeon = 'E'
|
1383
|
+
obj = { pass_args: pigeon }
|
1384
|
+
|
1385
|
+
c = MarkdownExec::MarkParse.new
|
1386
|
+
|
1387
|
+
# Expect that method command_execute is called with argument args having value pigeon
|
1388
|
+
c.expects(:command_execute).with(
|
1389
|
+
obj,
|
1390
|
+
'',
|
1391
|
+
args: pigeon)
|
1392
|
+
|
1393
|
+
# Call method execute_approved_block
|
1394
|
+
c.execute_approved_block(obj, [])
|
1395
|
+
end
|
1396
|
+
|
1397
|
+
def setup
|
1398
|
+
@mark_parse = MarkdownExec::MarkParse.new
|
1399
|
+
end
|
1400
|
+
|
1401
|
+
def test_set_fcb_title
|
1402
|
+
# sample input and output data for testing set_fcb_title method
|
1403
|
+
input_output_data = [
|
1404
|
+
{
|
1405
|
+
input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
|
1406
|
+
output: "puts 'Hello, world!'"
|
1407
|
+
},
|
1408
|
+
{
|
1409
|
+
input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
|
1410
|
+
output: 'def add(x, y) x + y end'
|
1411
|
+
},
|
1412
|
+
{
|
1413
|
+
input: FCB.new(title: 'foo', body: %w[bar baz]),
|
1414
|
+
output: 'foo' # expect the title to remain unchanged
|
1415
|
+
}
|
1416
|
+
]
|
1417
|
+
|
1418
|
+
# iterate over the input and output data and assert that the method sets the title as expected
|
1419
|
+
input_output_data.each do |data|
|
1420
|
+
input = data[:input]
|
1421
|
+
output = data[:output]
|
1422
|
+
@mark_parse.set_fcb_title(input)
|
1423
|
+
assert_equal output, input.title
|
1424
|
+
end
|
1425
|
+
end
|
1426
|
+
end
|
1427
|
+
|
1428
|
+
###
|
1429
|
+
|
1430
|
+
# result = split_string_on_first_char("hello-world", "-")
|
1431
|
+
# puts result.inspect # Output should be ["hello", "world"]
|
1432
|
+
|
1433
|
+
# result = split_string_on_first_char("hello", "-")
|
1434
|
+
# puts result.inspect # Output should be ["hello", ""]
|
1435
|
+
end
|
1436
|
+
end
|