markdown_exec 1.3.7 → 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 +53 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -1
- data/Rakefile +11 -6
- 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 +147 -71
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +523 -235
- data/lib/mdoc.rb +190 -58
- data/lib/menu.src.yml +323 -257
- data/lib/menu.yml +324 -258
- metadata +17 -6
- data/lib/env_opts.rb +0 -242
- data/lib/markdown_block_manager.rb +0 -195
- 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
|
#
|
@@ -199,115 +214,179 @@ module MarkdownExec
|
|
199
214
|
}
|
200
215
|
end
|
201
216
|
|
217
|
+
# Execute a code block after approval and provide user interaction options.
|
218
|
+
#
|
219
|
+
# This method displays required code blocks, asks for user approval, and
|
220
|
+
# executes the code block if approved. It also allows users to copy the
|
221
|
+
# code to the clipboard or save it to a file.
|
222
|
+
#
|
223
|
+
# @param opts [Hash] Options hash containing configuration settings.
|
224
|
+
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
225
|
+
# @return [String] The name of the executed code block.
|
226
|
+
#
|
202
227
|
def approve_and_execute_block(opts, mdoc)
|
203
|
-
|
204
|
-
if
|
205
|
-
|
206
|
-
|
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, '']
|
207
247
|
end
|
248
|
+
end
|
208
249
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
if sel == 3
|
222
|
-
text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
|
223
|
-
Clipboard.copy(text)
|
224
|
-
fout "Clipboard updated: #{required_blocks.count} blocks," /
|
225
|
-
" #{required_blocks.flatten.count} lines," /
|
226
|
-
" #{text.length} characters"
|
227
|
-
end
|
228
|
-
if sel == 4
|
229
|
-
write_command_file(opts.merge(save_executed_script: true),
|
230
|
-
required_blocks)
|
231
|
-
fout "File saved: #{@options[:saved_filespec]}"
|
232
|
-
end
|
233
|
-
break if [1, 2].include? sel
|
234
|
-
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
|
235
262
|
end
|
236
|
-
(opts[:ir_approve] = allow)
|
237
263
|
|
238
|
-
|
264
|
+
[LOAD_FILE, data.fetch('block', '')]
|
265
|
+
end
|
239
266
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
281
|
+
|
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
|
290
|
+
end
|
291
|
+
|
292
|
+
if sel == 3
|
293
|
+
copy_to_clipboard(required_lines)
|
294
|
+
elsif sel == 4
|
295
|
+
save_to_file(opts, required_lines)
|
246
296
|
end
|
247
297
|
|
248
|
-
|
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
|
249
339
|
end
|
250
340
|
|
251
341
|
def cfile
|
252
342
|
@cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern))
|
253
343
|
end
|
254
344
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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: [])
|
260
367
|
@execute_files = Hash.new([])
|
261
368
|
@execute_options = opts
|
262
369
|
@execute_started_at = Time.now.utc
|
263
370
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
Thread.new do
|
269
|
-
until (line = stdout.gets).nil?
|
270
|
-
@execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
|
271
|
-
print line if opts[:output_stdout]
|
272
|
-
yield nil, line, nil, exec_thr if block_given?
|
273
|
-
end
|
274
|
-
rescue IOError
|
275
|
-
#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?
|
276
375
|
end
|
277
|
-
|
278
|
-
|
279
|
-
until (line = stderr.gets).nil?
|
280
|
-
@execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
|
281
|
-
print line if opts[:output_stdout]
|
282
|
-
yield nil, nil, line, exec_thr if block_given?
|
283
|
-
end
|
284
|
-
rescue IOError
|
285
|
-
#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?
|
286
378
|
end
|
287
379
|
|
288
|
-
in_thr =
|
289
|
-
|
290
|
-
|
291
|
-
@execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
|
292
|
-
yield line, nil, nil, exec_thr if block_given?
|
293
|
-
end
|
294
|
-
#d 'exec_thr now dead'
|
295
|
-
rescue StandardError
|
296
|
-
#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?
|
297
383
|
end
|
298
384
|
|
299
|
-
#d 'join exec_thr'
|
300
385
|
exec_thr.join
|
301
|
-
|
302
|
-
#d 'wait before closing stdin'
|
303
386
|
sleep 0.1
|
304
|
-
|
305
|
-
#d 'kill stdin thread'
|
306
|
-
in_thr.kill
|
307
|
-
# @return_code = exec_thr.value
|
308
|
-
#d 'command end'
|
387
|
+
in_thr.kill if in_thr&.alive?
|
309
388
|
end
|
310
|
-
|
389
|
+
|
311
390
|
@execute_completed_at = Time.now.utc
|
312
391
|
rescue Errno::ENOENT => err
|
313
392
|
#d 'command error ENOENT triggered by missing command in script'
|
@@ -326,19 +405,19 @@ module MarkdownExec
|
|
326
405
|
end
|
327
406
|
|
328
407
|
def count_blocks_in_filename
|
329
|
-
|
408
|
+
fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
|
330
409
|
cnt = 0
|
331
410
|
cfile.readlines(@options[:filename]).each do |line|
|
332
|
-
cnt += 1 if line.match(
|
411
|
+
cnt += 1 if line.match(fenced_start_and_end_regex)
|
333
412
|
end
|
334
413
|
cnt / 2
|
335
414
|
end
|
336
415
|
|
337
416
|
# :reek:DuplicateMethodCall
|
338
|
-
def display_required_code(opts,
|
417
|
+
def display_required_code(opts, required_lines)
|
339
418
|
frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
|
340
419
|
fout frame
|
341
|
-
|
420
|
+
required_lines.each { |cb| fout cb }
|
342
421
|
fout frame
|
343
422
|
end
|
344
423
|
|
@@ -408,7 +487,6 @@ module MarkdownExec
|
|
408
487
|
def get_block_summary(call_options, fcb)
|
409
488
|
opts = optsmerge call_options
|
410
489
|
# return fcb.body unless opts[:struct]
|
411
|
-
|
412
490
|
return fcb unless opts[:bash]
|
413
491
|
|
414
492
|
fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
|
@@ -420,16 +498,23 @@ module MarkdownExec
|
|
420
498
|
bm = option_match_groups(titlexcall, opts[:block_name_match])
|
421
499
|
fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan])
|
422
500
|
fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan])
|
423
|
-
|
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
|
424
509
|
fcb
|
425
510
|
end
|
426
511
|
|
427
512
|
# :reek:DuplicateMethodCall
|
428
513
|
# :reek:LongYieldList
|
429
514
|
# :reek:NestedIterators
|
430
|
-
|
431
|
-
# opts = optsmerge call_options, options_block
|
515
|
+
#---
|
432
516
|
|
517
|
+
def iter_blocks_in_file(opts = {}, &block)
|
433
518
|
unless opts[:filename]&.present?
|
434
519
|
fout 'No blocks found.'
|
435
520
|
return
|
@@ -440,10 +525,10 @@ module MarkdownExec
|
|
440
525
|
return
|
441
526
|
end
|
442
527
|
|
443
|
-
|
444
|
-
|
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]
|
445
530
|
fcb = FCB.new
|
446
|
-
|
531
|
+
in_fenced_block = false
|
447
532
|
headings = []
|
448
533
|
|
449
534
|
## get type of messages to select
|
@@ -452,70 +537,105 @@ module MarkdownExec
|
|
452
537
|
|
453
538
|
cfile.readlines(opts[:filename]).each.with_index do |line, _line_num|
|
454
539
|
continue unless line
|
540
|
+
headings = update_headings(line, headings, opts) if opts[:menu_blocks_with_headings]
|
455
541
|
|
456
|
-
if
|
457
|
-
if
|
458
|
-
|
459
|
-
|
460
|
-
headings = [headings[0], lm[:name]]
|
461
|
-
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
462
|
-
headings = [lm[:name]]
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
466
|
-
if line.match(fenced_start_and_end_match)
|
467
|
-
if in_block
|
468
|
-
# end fcb
|
469
|
-
#
|
470
|
-
fcb.name = fcb.title || ''
|
471
|
-
if fcb.body
|
472
|
-
if fcb.title.nil? || fcb.title.empty?
|
473
|
-
fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64]
|
474
|
-
end
|
475
|
-
|
476
|
-
if block_given? && selected_messages.include?(:blocks) &&
|
477
|
-
Filter.fcb_select?(opts, fcb)
|
478
|
-
yield :blocks, fcb
|
479
|
-
end
|
480
|
-
end
|
481
|
-
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
|
482
546
|
else
|
483
|
-
|
484
|
-
|
485
|
-
in_block = true
|
486
|
-
|
487
|
-
fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys
|
488
|
-
fcb = FCB.new
|
489
|
-
fcb.headings = headings
|
490
|
-
fcb.name = fcb_title_groups.fetch(:name, '')
|
491
|
-
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
492
|
-
fcb.title = fcb_title_groups.fetch(:name, '')
|
493
|
-
|
494
|
-
# selected fcb
|
495
|
-
#
|
496
|
-
fcb.body = []
|
497
|
-
|
498
|
-
rest = fcb_title_groups.fetch(:rest, '')
|
499
|
-
fcb.reqs = rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }
|
500
|
-
|
501
|
-
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
502
|
-
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
503
|
-
tn.named_captures.sym_keys
|
504
|
-
end
|
505
|
-
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
506
|
-
tn.named_captures.sym_keys
|
507
|
-
end
|
547
|
+
fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
548
|
+
in_fenced_block = true
|
508
549
|
end
|
509
|
-
elsif
|
550
|
+
elsif in_fenced_block && fcb.body
|
510
551
|
dp 'append line to fcb body'
|
511
552
|
fcb.body += [line.chomp]
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
553
|
+
else
|
554
|
+
process_line(line, opts, selected_messages, &block)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
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
|
+
|
614
|
+
def split_array(arr)
|
615
|
+
true_list = []
|
616
|
+
false_list = []
|
617
|
+
|
618
|
+
arr.each do |element|
|
619
|
+
if yield(element)
|
620
|
+
true_list << element
|
621
|
+
else
|
622
|
+
false_list << element
|
517
623
|
end
|
518
624
|
end
|
625
|
+
|
626
|
+
[true_list, false_list]
|
627
|
+
end
|
628
|
+
|
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
|
519
639
|
end
|
520
640
|
|
521
641
|
# return body, title if option.struct
|
@@ -530,10 +650,11 @@ module MarkdownExec
|
|
530
650
|
blocks.push FCB.new({
|
531
651
|
# name: '',
|
532
652
|
chrome: true,
|
533
|
-
|
653
|
+
dname: format(
|
534
654
|
opts[:menu_divider_format],
|
535
655
|
opts[:menu_initial_divider]
|
536
656
|
).send(opts[:menu_divider_color].to_sym),
|
657
|
+
oname: opts[:menu_initial_divider],
|
537
658
|
disabled: '' # __LINE__.to_s
|
538
659
|
})
|
539
660
|
end
|
@@ -554,8 +675,9 @@ module MarkdownExec
|
|
554
675
|
blocks.push FCB.new(
|
555
676
|
{ chrome: true,
|
556
677
|
disabled: '',
|
557
|
-
|
558
|
-
|
678
|
+
dname: format(opts[:menu_divider_format],
|
679
|
+
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
680
|
+
oname: mbody[:name] }
|
559
681
|
)
|
560
682
|
end
|
561
683
|
elsif opts[:menu_task_match].present? &&
|
@@ -564,10 +686,14 @@ module MarkdownExec
|
|
564
686
|
blocks.push FCB.new(
|
565
687
|
{ chrome: true,
|
566
688
|
disabled: '',
|
567
|
-
|
689
|
+
dname: format(
|
690
|
+
opts[:menu_task_format],
|
691
|
+
$~.named_captures.transform_keys(&:to_sym)
|
692
|
+
).send(opts[:menu_task_color].to_sym),
|
693
|
+
oname: format(
|
568
694
|
opts[:menu_task_format],
|
569
695
|
$~.named_captures.transform_keys(&:to_sym)
|
570
|
-
)
|
696
|
+
) }
|
571
697
|
)
|
572
698
|
end
|
573
699
|
else
|
@@ -584,9 +710,10 @@ module MarkdownExec
|
|
584
710
|
blocks.push FCB.new(
|
585
711
|
{ chrome: true,
|
586
712
|
disabled: '',
|
587
|
-
|
588
|
-
|
589
|
-
.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] }
|
590
717
|
)
|
591
718
|
end
|
592
719
|
blocks
|
@@ -667,7 +794,7 @@ module MarkdownExec
|
|
667
794
|
else
|
668
795
|
# blocks.map(&:name)
|
669
796
|
blocks.map do |block|
|
670
|
-
block.fetch(:text, nil) || block.
|
797
|
+
block.fetch(:text, nil) || block.oname
|
671
798
|
end
|
672
799
|
end.compact.reject(&:empty?)
|
673
800
|
end
|
@@ -760,10 +887,10 @@ module MarkdownExec
|
|
760
887
|
when :line
|
761
888
|
if options[:menu_divider_match] &&
|
762
889
|
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
763
|
-
menu.push FCB.new({ name: mbody[:name], disabled: '' })
|
890
|
+
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
|
764
891
|
end
|
765
892
|
when :blocks
|
766
|
-
menu += [fcb.
|
893
|
+
menu += [fcb.oname]
|
767
894
|
end
|
768
895
|
end
|
769
896
|
menu
|
@@ -809,7 +936,7 @@ module MarkdownExec
|
|
809
936
|
def options_finalize(rest)
|
810
937
|
## position 0: file or folder (optional)
|
811
938
|
#
|
812
|
-
if (pos = rest.
|
939
|
+
if (pos = rest.shift)&.present?
|
813
940
|
if Dir.exist?(pos)
|
814
941
|
@options[:path] = pos
|
815
942
|
elsif File.exist?(pos)
|
@@ -821,7 +948,7 @@ module MarkdownExec
|
|
821
948
|
|
822
949
|
## position 1: block name (optional)
|
823
950
|
#
|
824
|
-
block_name = rest.
|
951
|
+
block_name = rest.shift
|
825
952
|
@options[:block_name] = block_name if block_name.present?
|
826
953
|
end
|
827
954
|
|
@@ -868,13 +995,31 @@ module MarkdownExec
|
|
868
995
|
}
|
869
996
|
end
|
870
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
|
+
|
871
1016
|
## insert exit option at head or tail
|
872
1017
|
#
|
873
|
-
def prompt_menu_add_exit(
|
1018
|
+
def prompt_menu_add_exit(items, label = EXIT_OPTION)
|
874
1019
|
if @options[:menu_exit_at_top]
|
875
|
-
(@options[:menu_with_exit] ? [
|
1020
|
+
(@options[:menu_with_exit] ? [label] : []) + items
|
876
1021
|
else
|
877
|
-
items + (@options[:menu_with_exit] ? [
|
1022
|
+
items + (@options[:menu_with_exit] ? [label] : [])
|
878
1023
|
end
|
879
1024
|
end
|
880
1025
|
|
@@ -883,10 +1028,31 @@ module MarkdownExec
|
|
883
1028
|
# return selected option or nil
|
884
1029
|
#
|
885
1030
|
def prompt_with_quit(prompt_text, items, opts = {})
|
886
|
-
|
887
|
-
|
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
|
+
),
|
888
1048
|
opts.merge(filter: true))
|
889
|
-
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
|
890
1056
|
end
|
891
1057
|
|
892
1058
|
# :reek:UtilityFunction ### temp
|
@@ -899,12 +1065,8 @@ module MarkdownExec
|
|
899
1065
|
|
900
1066
|
# :reek:NestedIterators
|
901
1067
|
def run
|
902
|
-
## default configuration
|
903
|
-
#
|
904
1068
|
@options = base_options
|
905
1069
|
|
906
|
-
## read local configuration file
|
907
|
-
#
|
908
1070
|
read_configuration_file! @options,
|
909
1071
|
".#{MarkdownExec::APP_NAME.downcase}.yml"
|
910
1072
|
|
@@ -920,12 +1082,13 @@ module MarkdownExec
|
|
920
1082
|
menu_option_append opts, options, item
|
921
1083
|
end
|
922
1084
|
end
|
923
|
-
option_parser.load
|
924
|
-
|
925
|
-
option_parser.environment # env defaults to the basename of the program
|
926
|
-
# child_argv = arguments_for_child
|
1085
|
+
option_parser.load
|
1086
|
+
option_parser.environment
|
927
1087
|
rest = option_parser.parse!(arguments_for_mde) # (into: options)
|
928
1088
|
|
1089
|
+
# pass through arguments excluded from OptionParser with `--`
|
1090
|
+
@options[:pass_args] = ARGV[rest.count + 1..]
|
1091
|
+
|
929
1092
|
begin
|
930
1093
|
options_finalize rest
|
931
1094
|
exec_block options, options[:block_name]
|
@@ -984,54 +1147,98 @@ module MarkdownExec
|
|
984
1147
|
File.write(@options[:logged_stdout_filespec], ol.join)
|
985
1148
|
end
|
986
1149
|
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
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
|
994
1174
|
|
1175
|
+
# Select and execute a code block from a Markdown document.
|
1176
|
+
#
|
1177
|
+
# This method allows the user to interactively select a code block from a
|
1178
|
+
# Markdown document, obtain approval, and execute the chosen block of code.
|
1179
|
+
#
|
1180
|
+
# @param call_options [Hash] Initial options for the method.
|
1181
|
+
# @param options_block [Block] Block of options to be merged with call_options.
|
1182
|
+
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1183
|
+
def select_approve_and_execute_block(call_options, &options_block)
|
1184
|
+
opts = optsmerge(call_options, options_block)
|
995
1185
|
repeat_menu = true && !opts[:block_name].present?
|
1186
|
+
|
1187
|
+
load_file = !LOAD_FILE
|
996
1188
|
loop do
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
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
|
1015
1228
|
|
1016
|
-
|
1017
|
-
end.compact
|
1018
|
-
return nil if bm.count.zero?
|
1229
|
+
# later: load file
|
1019
1230
|
|
1020
|
-
|
1021
|
-
per_page: opts[:select_page_height]
|
1022
|
-
return nil if sel.nil?
|
1231
|
+
load_file, block_name = approve_and_execute_block(opts, mdoc)
|
1023
1232
|
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
end
|
1029
|
-
opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
|
1030
|
-
end
|
1031
|
-
approve_and_execute_block opts, mdoc
|
1032
|
-
break unless repeat_menu
|
1233
|
+
opts[:block_name] = block_name
|
1234
|
+
if load_file == LOAD_FILE
|
1235
|
+
repeat_menu = true
|
1236
|
+
break
|
1237
|
+
end
|
1033
1238
|
|
1034
|
-
|
1239
|
+
break unless repeat_menu
|
1240
|
+
end
|
1241
|
+
break if load_file != LOAD_FILE
|
1035
1242
|
end
|
1036
1243
|
rescue StandardError => err
|
1037
1244
|
warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
|
@@ -1093,6 +1300,22 @@ module MarkdownExec
|
|
1093
1300
|
end.to_yaml
|
1094
1301
|
end
|
1095
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
|
+
|
1096
1319
|
def tab_completions(data = menu_for_optparse)
|
1097
1320
|
data.map do |item|
|
1098
1321
|
"--#{item[:long_name]}" if item[:long_name]
|
@@ -1110,7 +1333,7 @@ module MarkdownExec
|
|
1110
1333
|
@options
|
1111
1334
|
end
|
1112
1335
|
|
1113
|
-
def write_command_file(call_options,
|
1336
|
+
def write_command_file(call_options, required_lines)
|
1114
1337
|
return unless call_options[:save_executed_script]
|
1115
1338
|
|
1116
1339
|
time_now = Time.now.utc
|
@@ -1137,7 +1360,7 @@ module MarkdownExec
|
|
1137
1360
|
"# file_name: #{opts[:filename]}\n" \
|
1138
1361
|
"# block_name: #{opts[:block_name]}\n" \
|
1139
1362
|
"# time: #{time_now}\n" \
|
1140
|
-
"#{
|
1363
|
+
"#{required_lines.flatten.join("\n")}\n")
|
1141
1364
|
return if @options[:saved_script_chmod].zero?
|
1142
1365
|
|
1143
1366
|
File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
|
@@ -1145,4 +1368,69 @@ module MarkdownExec
|
|
1145
1368
|
end # class MarkParse
|
1146
1369
|
end # module MarkdownExec
|
1147
1370
|
|
1148
|
-
|
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
|