markdown_exec 1.3.9 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.pryrc +13 -0
- data/.rubocop.yml +15 -1
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +1 -1
- data/bin/bmde +11 -0
- data/bin/tab_completion.sh +2 -2
- data/examples/linked1.md +14 -13
- data/examples/linked2.md +14 -14
- data/examples/linked3.md +12 -0
- data/lib/block_types.rb +2 -0
- data/lib/colorize.rb +52 -55
- data/lib/filter.rb +3 -3
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +882 -612
- data/lib/mdoc.rb +53 -51
- data/lib/menu.src.yml +19 -1
- data/lib/menu.yml +20 -2
- data/lib/method_sorter.rb +76 -0
- data/lib/sort_yaml_gpt4.rb +32 -0
- metadata +7 -2
data/lib/markdown_exec.rb
CHANGED
|
@@ -9,6 +9,7 @@ require 'fileutils'
|
|
|
9
9
|
require 'open3'
|
|
10
10
|
require 'optparse'
|
|
11
11
|
require 'shellwords'
|
|
12
|
+
require 'tmpdir'
|
|
12
13
|
require 'tty-prompt'
|
|
13
14
|
require 'yaml'
|
|
14
15
|
|
|
@@ -19,13 +20,13 @@ require_relative 'colorize'
|
|
|
19
20
|
require_relative 'env'
|
|
20
21
|
require_relative 'fcb'
|
|
21
22
|
require_relative 'filter'
|
|
23
|
+
require_relative 'markdown_exec/version'
|
|
22
24
|
require_relative 'mdoc'
|
|
23
25
|
require_relative 'option_value'
|
|
24
26
|
require_relative 'saved_assets'
|
|
25
27
|
require_relative 'saved_files_matcher'
|
|
26
28
|
require_relative 'shared'
|
|
27
29
|
require_relative 'tap'
|
|
28
|
-
require_relative 'markdown_exec/version'
|
|
29
30
|
|
|
30
31
|
include CLI
|
|
31
32
|
include Tap
|
|
@@ -35,14 +36,11 @@ tap_config envvar: MarkdownExec::TAP_DEBUG
|
|
|
35
36
|
$stderr.sync = true
|
|
36
37
|
$stdout.sync = true
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
|
|
39
40
|
|
|
40
41
|
# macros
|
|
41
42
|
#
|
|
42
|
-
BACK_OPTION = '* Back'
|
|
43
|
-
EXIT_OPTION = '* Exit'
|
|
44
43
|
LOAD_FILE = true
|
|
45
|
-
VN = 'MDE_MENU_HISTORY'
|
|
46
44
|
|
|
47
45
|
# custom error: file specified is missing
|
|
48
46
|
#
|
|
@@ -129,10 +127,45 @@ end
|
|
|
129
127
|
# convert regex match groups to a hash with symbol keys
|
|
130
128
|
#
|
|
131
129
|
# :reek:UtilityFunction
|
|
132
|
-
def
|
|
130
|
+
def extract_named_captures_from_option(str, option)
|
|
133
131
|
str.match(Regexp.new(option))&.named_captures&.sym_keys
|
|
134
132
|
end
|
|
135
133
|
|
|
134
|
+
module ArrayUtil
|
|
135
|
+
def self.partition_by_predicate(arr)
|
|
136
|
+
true_list = []
|
|
137
|
+
false_list = []
|
|
138
|
+
|
|
139
|
+
arr.each do |element|
|
|
140
|
+
if yield(element)
|
|
141
|
+
true_list << element
|
|
142
|
+
else
|
|
143
|
+
false_list << element
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
[true_list, false_list]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
module StringUtil
|
|
152
|
+
# Splits the given string on the first occurrence of the specified character.
|
|
153
|
+
# Returns an array containing the portion of the string before the character and the rest of the string.
|
|
154
|
+
#
|
|
155
|
+
# @param input_str [String] The string to be split.
|
|
156
|
+
# @param split_char [String] The character on which to split the string.
|
|
157
|
+
# @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
|
|
158
|
+
def self.partition_at_first(input_str, split_char)
|
|
159
|
+
split_index = input_str.index(split_char)
|
|
160
|
+
|
|
161
|
+
if split_index.nil?
|
|
162
|
+
[input_str, '']
|
|
163
|
+
else
|
|
164
|
+
[input_str[0...split_index], input_str[(split_index + 1)..-1]]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
136
169
|
# execute markdown documents
|
|
137
170
|
#
|
|
138
171
|
module MarkdownExec
|
|
@@ -141,10 +174,10 @@ module MarkdownExec
|
|
|
141
174
|
FNR12 = ',~'
|
|
142
175
|
|
|
143
176
|
SHELL_COLOR_OPTIONS = {
|
|
144
|
-
|
|
177
|
+
BLOCK_TYPE_BASH => :menu_bash_color,
|
|
145
178
|
BLOCK_TYPE_LINK => :menu_link_color,
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
BLOCK_TYPE_OPTS => :menu_opts_color,
|
|
180
|
+
BLOCK_TYPE_VARS => :menu_vars_color
|
|
148
181
|
}.freeze
|
|
149
182
|
|
|
150
183
|
##
|
|
@@ -158,12 +191,11 @@ module MarkdownExec
|
|
|
158
191
|
class MarkParse
|
|
159
192
|
attr_reader :options
|
|
160
193
|
|
|
194
|
+
include ArrayUtil
|
|
195
|
+
include StringUtil
|
|
161
196
|
include FOUT
|
|
162
197
|
|
|
163
198
|
def initialize(options = {})
|
|
164
|
-
@options = options
|
|
165
|
-
# hide disabled symbol
|
|
166
|
-
@prompt = TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
|
|
167
199
|
@execute_aborted_at = nil
|
|
168
200
|
@execute_completed_at = nil
|
|
169
201
|
@execute_error = nil
|
|
@@ -173,6 +205,74 @@ module MarkdownExec
|
|
|
173
205
|
@execute_script_filespec = nil
|
|
174
206
|
@execute_started_at = nil
|
|
175
207
|
@option_parser = nil
|
|
208
|
+
@options = options
|
|
209
|
+
@prompt = tty_prompt_without_disabled_symbol
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
##
|
|
213
|
+
# Appends a summary of a block (FCB) to the blocks array.
|
|
214
|
+
#
|
|
215
|
+
def append_block_summary(blocks, fcb, opts)
|
|
216
|
+
## enhance fcb with block summary
|
|
217
|
+
#
|
|
218
|
+
blocks.push get_block_summary(opts, fcb)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
##
|
|
222
|
+
# Appends a final divider to the blocks array if it is specified in options.
|
|
223
|
+
#
|
|
224
|
+
def append_final_divider(blocks, opts)
|
|
225
|
+
return unless opts[:menu_divider_format].present? && opts[:menu_final_divider].present?
|
|
226
|
+
|
|
227
|
+
blocks.push FCB.new(
|
|
228
|
+
{ chrome: true,
|
|
229
|
+
disabled: '',
|
|
230
|
+
dname: format(opts[:menu_divider_format],
|
|
231
|
+
opts[:menu_final_divider])
|
|
232
|
+
.send(opts[:menu_divider_color].to_sym),
|
|
233
|
+
oname: opts[:menu_final_divider] }
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
##
|
|
238
|
+
# Appends an initial divider to the blocks array if it is specified in options.
|
|
239
|
+
#
|
|
240
|
+
def append_initial_divider(blocks, opts)
|
|
241
|
+
return unless opts[:menu_initial_divider].present?
|
|
242
|
+
|
|
243
|
+
blocks.push FCB.new({
|
|
244
|
+
# name: '',
|
|
245
|
+
chrome: true,
|
|
246
|
+
dname: format(
|
|
247
|
+
opts[:menu_divider_format],
|
|
248
|
+
opts[:menu_initial_divider]
|
|
249
|
+
).send(opts[:menu_divider_color].to_sym),
|
|
250
|
+
oname: opts[:menu_initial_divider],
|
|
251
|
+
disabled: '' # __LINE__.to_s
|
|
252
|
+
})
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Execute a code block after approval and provide user interaction options.
|
|
256
|
+
#
|
|
257
|
+
# This method displays required code blocks, asks for user approval, and
|
|
258
|
+
# executes the code block if approved. It also allows users to copy the
|
|
259
|
+
# code to the clipboard or save it to a file.
|
|
260
|
+
#
|
|
261
|
+
# @param opts [Hash] Options hash containing configuration settings.
|
|
262
|
+
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
|
263
|
+
#
|
|
264
|
+
def approve_and_execute_block(opts, mdoc)
|
|
265
|
+
selected = mdoc.get_block_by_name(opts[:block_name])
|
|
266
|
+
|
|
267
|
+
if selected.fetch(:shell, '') == BLOCK_TYPE_LINK
|
|
268
|
+
handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
|
|
269
|
+
elsif opts.fetch(:back, false)
|
|
270
|
+
handle_back_link(opts)
|
|
271
|
+
elsif selected[:shell] == BLOCK_TYPE_OPTS
|
|
272
|
+
handle_shell_opts(opts, selected)
|
|
273
|
+
else
|
|
274
|
+
handle_remainder_blocks(mdoc, opts, selected)
|
|
275
|
+
end
|
|
176
276
|
end
|
|
177
277
|
|
|
178
278
|
# return arguments before `--`
|
|
@@ -206,6 +306,14 @@ module MarkdownExec
|
|
|
206
306
|
end.compact.to_h
|
|
207
307
|
end
|
|
208
308
|
|
|
309
|
+
def blocks_per_opts(blocks, opts)
|
|
310
|
+
return blocks if opts[:struct]
|
|
311
|
+
|
|
312
|
+
blocks.map do |block|
|
|
313
|
+
block.fetch(:text, nil) || block.oname
|
|
314
|
+
end.compact.reject(&:empty?)
|
|
315
|
+
end
|
|
316
|
+
|
|
209
317
|
def calculated_options
|
|
210
318
|
{
|
|
211
319
|
bash: true, # bash block parsing in get_block_summary()
|
|
@@ -214,100 +322,22 @@ module MarkdownExec
|
|
|
214
322
|
}
|
|
215
323
|
end
|
|
216
324
|
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
#
|
|
227
|
-
def approve_and_execute_block(opts, mdoc)
|
|
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, '']
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
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
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
[LOAD_FILE, data.fetch('block', '')]
|
|
265
|
-
end
|
|
266
|
-
|
|
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
|
|
325
|
+
# Check whether the document exists and is readable
|
|
326
|
+
def check_file_existence(filename)
|
|
327
|
+
unless filename&.present?
|
|
328
|
+
fout 'No blocks found.'
|
|
329
|
+
return false
|
|
290
330
|
end
|
|
291
331
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
save_to_file(opts, required_lines)
|
|
332
|
+
unless File.exist? filename
|
|
333
|
+
fout 'Document is missing.'
|
|
334
|
+
return false
|
|
296
335
|
end
|
|
297
|
-
|
|
298
|
-
sel == 1
|
|
336
|
+
true
|
|
299
337
|
end
|
|
300
338
|
|
|
301
|
-
def
|
|
302
|
-
|
|
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
|
|
339
|
+
def clear_required_file
|
|
340
|
+
ENV['MDE_LINK_REQUIRED_FILE'] = ''
|
|
311
341
|
end
|
|
312
342
|
|
|
313
343
|
# Collect required code blocks based on the provided options.
|
|
@@ -315,11 +345,7 @@ module MarkdownExec
|
|
|
315
345
|
# @param opts [Hash] Options hash containing configuration settings.
|
|
316
346
|
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
|
317
347
|
# @return [Array<String>] Required code blocks as an array of lines.
|
|
318
|
-
def
|
|
319
|
-
required = mdoc.collect_recursively_required_code(opts[:block_name])
|
|
320
|
-
required_lines = required[:code]
|
|
321
|
-
required[:blocks]
|
|
322
|
-
|
|
348
|
+
def collect_required_code_lines(mdoc, selected, opts: {})
|
|
323
349
|
# Apply hash in opts block to environment variables
|
|
324
350
|
if selected[:shell] == BLOCK_TYPE_VARS
|
|
325
351
|
data = YAML.load(selected[:body].join("\n"))
|
|
@@ -335,33 +361,20 @@ module MarkdownExec
|
|
|
335
361
|
end
|
|
336
362
|
end
|
|
337
363
|
|
|
338
|
-
|
|
364
|
+
required = mdoc.collect_recursively_required_code(opts[:block_name], opts: opts)
|
|
365
|
+
read_required_blocks_from_temp_file + required[:code]
|
|
339
366
|
end
|
|
340
367
|
|
|
341
368
|
def cfile
|
|
342
|
-
@cfile ||= CachedNestedFileReader.new(
|
|
369
|
+
@cfile ||= CachedNestedFileReader.new(
|
|
370
|
+
import_pattern: @options.fetch(:import_pattern)
|
|
371
|
+
)
|
|
343
372
|
end
|
|
344
373
|
|
|
345
374
|
EF_STDOUT = :stdout
|
|
346
375
|
EF_STDERR = :stderr
|
|
347
376
|
EF_STDIN = :stdin
|
|
348
377
|
|
|
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
378
|
# Existing command_execute method
|
|
366
379
|
def command_execute(opts, command, args: [])
|
|
367
380
|
@execute_files = Hash.new([])
|
|
@@ -404,6 +417,14 @@ module MarkdownExec
|
|
|
404
417
|
fout "Error ENOENT: #{err.inspect}"
|
|
405
418
|
end
|
|
406
419
|
|
|
420
|
+
def copy_to_clipboard(required_lines)
|
|
421
|
+
text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
|
|
422
|
+
Clipboard.copy(text)
|
|
423
|
+
fout "Clipboard updated: #{required_lines.count} blocks," \
|
|
424
|
+
" #{required_lines.flatten.count} lines," \
|
|
425
|
+
" #{text.length} characters"
|
|
426
|
+
end
|
|
427
|
+
|
|
407
428
|
def count_blocks_in_filename
|
|
408
429
|
fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
|
|
409
430
|
cnt = 0
|
|
@@ -413,6 +434,45 @@ module MarkdownExec
|
|
|
413
434
|
cnt / 2
|
|
414
435
|
end
|
|
415
436
|
|
|
437
|
+
def create_and_write_file_with_permissions(file_path, content, chmod_value)
|
|
438
|
+
dirname = File.dirname(file_path)
|
|
439
|
+
FileUtils.mkdir_p dirname
|
|
440
|
+
File.write(file_path, content)
|
|
441
|
+
return if chmod_value.zero?
|
|
442
|
+
|
|
443
|
+
File.chmod chmod_value, file_path
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Deletes a required temporary file specified by an environment variable.
|
|
447
|
+
# The function checks if the file exists before attempting to delete it.
|
|
448
|
+
# Clears the environment variable after deletion.
|
|
449
|
+
#
|
|
450
|
+
def delete_required_temp_file
|
|
451
|
+
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
|
452
|
+
|
|
453
|
+
return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
|
454
|
+
|
|
455
|
+
FileUtils.rm_f(temp_blocks_file_path)
|
|
456
|
+
|
|
457
|
+
clear_required_file
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
## Determines the correct filename to use for searching files
|
|
461
|
+
#
|
|
462
|
+
def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
|
|
463
|
+
default_folder: nil, filetree: nil)
|
|
464
|
+
if specified_filename&.present?
|
|
465
|
+
return specified_filename if specified_filename.start_with?('/')
|
|
466
|
+
|
|
467
|
+
File.join(specified_folder || default_folder, specified_filename)
|
|
468
|
+
elsif specified_folder&.present?
|
|
469
|
+
File.join(specified_folder,
|
|
470
|
+
filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
|
|
471
|
+
else
|
|
472
|
+
File.join(default_folder, default_filename)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
416
476
|
# :reek:DuplicateMethodCall
|
|
417
477
|
def display_required_code(opts, required_lines)
|
|
418
478
|
frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
|
|
@@ -421,14 +481,41 @@ module MarkdownExec
|
|
|
421
481
|
fout frame
|
|
422
482
|
end
|
|
423
483
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
484
|
+
def execute_approved_block(opts, required_lines)
|
|
485
|
+
write_command_file(opts, required_lines)
|
|
486
|
+
command_execute(
|
|
487
|
+
opts,
|
|
488
|
+
required_lines.flatten.join("\n"),
|
|
489
|
+
args: opts.fetch(:pass_args, [])
|
|
490
|
+
)
|
|
491
|
+
initialize_and_save_execution_output
|
|
492
|
+
output_execution_summary
|
|
493
|
+
output_execution_result
|
|
494
|
+
end
|
|
428
495
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
496
|
+
# Reports and executes block logic
|
|
497
|
+
def execute_block_logic(files)
|
|
498
|
+
@options[:filename] = select_document_if_multiple(files)
|
|
499
|
+
select_approve_and_execute_block({
|
|
500
|
+
bash: true,
|
|
501
|
+
struct: true
|
|
502
|
+
})
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
## Executes the block specified in the options
|
|
506
|
+
#
|
|
507
|
+
def execute_block_with_error_handling(rest)
|
|
508
|
+
finalize_cli_argument_processing(rest)
|
|
509
|
+
@options[:cli_rest] = rest
|
|
510
|
+
execute_code_block_based_on_options(@options)
|
|
511
|
+
rescue FileMissingError => err
|
|
512
|
+
puts "File missing: #{err}"
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Main method to execute a block based on options and block_name
|
|
516
|
+
def execute_code_block_based_on_options(options)
|
|
517
|
+
options = calculated_options.merge(options)
|
|
518
|
+
update_options(options, over: false)
|
|
432
519
|
|
|
433
520
|
simple_commands = {
|
|
434
521
|
doc_glob: -> { fout options[:md_filename_glob] },
|
|
@@ -459,27 +546,59 @@ module MarkdownExec
|
|
|
459
546
|
tab_completions: -> { fout tab_completions },
|
|
460
547
|
menu_export: -> { fout menu_export }
|
|
461
548
|
}
|
|
549
|
+
|
|
550
|
+
return if execute_simple_commands(simple_commands)
|
|
551
|
+
|
|
552
|
+
files = prepare_file_list(options)
|
|
553
|
+
execute_block_logic(files)
|
|
554
|
+
return unless @options[:output_saved_script_filename]
|
|
555
|
+
|
|
556
|
+
fout "saved_filespec: #{@execute_script_filespec}"
|
|
557
|
+
rescue StandardError => err
|
|
558
|
+
warn(error = "ERROR ** MarkParse.execute_code_block_based_on_options(); #{err.inspect}")
|
|
559
|
+
binding.pry if $tap_enable
|
|
560
|
+
raise ArgumentError, error
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Executes command based on the provided option keys
|
|
564
|
+
def execute_simple_commands(simple_commands)
|
|
462
565
|
simple_commands.each_key do |key|
|
|
463
566
|
if @options[key]
|
|
464
567
|
simple_commands[key].call
|
|
465
|
-
return
|
|
568
|
+
return true
|
|
466
569
|
end
|
|
467
570
|
end
|
|
571
|
+
false
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
##
|
|
575
|
+
# Determines the types of blocks to select based on the filter.
|
|
576
|
+
#
|
|
577
|
+
def filter_block_types
|
|
578
|
+
## return type of blocks to select
|
|
579
|
+
#
|
|
580
|
+
%i[blocks line]
|
|
581
|
+
end
|
|
468
582
|
|
|
469
|
-
|
|
583
|
+
## post-parse options configuration
|
|
584
|
+
#
|
|
585
|
+
def finalize_cli_argument_processing(rest)
|
|
586
|
+
## position 0: file or folder (optional)
|
|
470
587
|
#
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
588
|
+
if (pos = rest.shift)&.present?
|
|
589
|
+
if Dir.exist?(pos)
|
|
590
|
+
@options[:path] = pos
|
|
591
|
+
elsif File.exist?(pos)
|
|
592
|
+
@options[:filename] = pos
|
|
593
|
+
else
|
|
594
|
+
raise FileMissingError, pos, caller
|
|
595
|
+
end
|
|
596
|
+
end
|
|
477
597
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
raise ArgumentError, error
|
|
598
|
+
## position 1: block name (optional)
|
|
599
|
+
#
|
|
600
|
+
block_name = rest.shift
|
|
601
|
+
@options[:block_name] = block_name if block_name.present?
|
|
483
602
|
end
|
|
484
603
|
|
|
485
604
|
## summarize blocks
|
|
@@ -495,9 +614,9 @@ module MarkdownExec
|
|
|
495
614
|
else
|
|
496
615
|
fcb.title
|
|
497
616
|
end
|
|
498
|
-
bm =
|
|
499
|
-
fcb.stdin =
|
|
500
|
-
fcb.stdout =
|
|
617
|
+
bm = extract_named_captures_from_option(titlexcall, opts[:block_name_match])
|
|
618
|
+
fcb.stdin = extract_named_captures_from_option(titlexcall, opts[:block_stdin_scan])
|
|
619
|
+
fcb.stdout = extract_named_captures_from_option(titlexcall, opts[:block_stdout_scan])
|
|
501
620
|
|
|
502
621
|
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
|
503
622
|
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
|
@@ -509,218 +628,268 @@ module MarkdownExec
|
|
|
509
628
|
fcb
|
|
510
629
|
end
|
|
511
630
|
|
|
512
|
-
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
|
|
631
|
+
##
|
|
632
|
+
# Handles errors that occur during the block listing process.
|
|
633
|
+
#
|
|
634
|
+
def handle_error(err)
|
|
635
|
+
warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
|
|
636
|
+
warn(caller[0..4])
|
|
637
|
+
raise StandardError, error
|
|
638
|
+
end
|
|
516
639
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
640
|
+
# Handles the link-back operation.
|
|
641
|
+
#
|
|
642
|
+
# @param opts [Hash] Configuration options hash.
|
|
643
|
+
# @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
|
|
644
|
+
def handle_back_link(opts)
|
|
645
|
+
history_state_pop(opts)
|
|
646
|
+
[LOAD_FILE, '']
|
|
647
|
+
end
|
|
522
648
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
649
|
+
# Handles the execution and display of remainder blocks from a selected menu item.
|
|
650
|
+
#
|
|
651
|
+
# @param mdoc [Object] Document object containing code blocks.
|
|
652
|
+
# @param opts [Hash] Configuration options hash.
|
|
653
|
+
# @param selected [Hash] Selected item from the menu.
|
|
654
|
+
# @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
|
|
655
|
+
# @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
|
|
656
|
+
def handle_remainder_blocks(mdoc, opts, selected)
|
|
657
|
+
required_lines = collect_required_code_lines(mdoc, selected, opts: opts)
|
|
658
|
+
if opts[:output_script] || opts[:user_must_approve]
|
|
659
|
+
display_required_code(opts, required_lines)
|
|
526
660
|
end
|
|
661
|
+
allow = opts[:user_must_approve] ? prompt_for_user_approval(opts, required_lines) : true
|
|
662
|
+
opts[:ir_approve] = allow
|
|
663
|
+
execute_approved_block(opts, required_lines) if opts[:ir_approve]
|
|
527
664
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
fcb = FCB.new
|
|
531
|
-
in_fenced_block = false
|
|
532
|
-
headings = []
|
|
665
|
+
[!LOAD_FILE, '']
|
|
666
|
+
end
|
|
533
667
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
668
|
+
# Handles the link-shell operation.
|
|
669
|
+
#
|
|
670
|
+
# @param opts [Hash] Configuration options hash.
|
|
671
|
+
# @param body [Array<String>] The body content.
|
|
672
|
+
# @param mdoc [Object] Document object containing code blocks.
|
|
673
|
+
# @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and a block name.
|
|
674
|
+
def handle_shell_link(opts, body, mdoc)
|
|
675
|
+
data = body.present? ? YAML.load(body.join("\n")) : {}
|
|
676
|
+
data_file = data.fetch('file', nil)
|
|
677
|
+
return [!LOAD_FILE, ''] unless data_file
|
|
537
678
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
546
|
-
else
|
|
547
|
-
fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
|
548
|
-
in_fenced_block = true
|
|
549
|
-
end
|
|
550
|
-
elsif in_fenced_block && fcb.body
|
|
551
|
-
dp 'append line to fcb body'
|
|
552
|
-
fcb.body += [line.chomp]
|
|
553
|
-
else
|
|
554
|
-
process_line(line, opts, selected_messages, &block)
|
|
555
|
-
end
|
|
679
|
+
history_state_push(mdoc, data_file, opts)
|
|
680
|
+
|
|
681
|
+
data.fetch('vars', []).each do |var|
|
|
682
|
+
ENV[var[0]] = var[1].to_s
|
|
556
683
|
end
|
|
684
|
+
|
|
685
|
+
[LOAD_FILE, data.fetch('block', '')]
|
|
557
686
|
end
|
|
558
687
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
688
|
+
# Handles options for the shell.
|
|
689
|
+
#
|
|
690
|
+
# @param opts [Hash] Configuration options hash.
|
|
691
|
+
# @param selected [Hash] Selected item from the menu.
|
|
692
|
+
# @return [Array<Symbol, String>] A tuple containing a NOT_LOAD_FILE flag and an empty string.
|
|
693
|
+
def handle_shell_opts(opts, selected)
|
|
694
|
+
data = YAML.load(selected[:body].join("\n"))
|
|
695
|
+
data.each_key do |key|
|
|
696
|
+
opts[key.to_sym] = value = data[key].to_s
|
|
697
|
+
next unless opts[:menu_opts_set_format].present?
|
|
569
698
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
699
|
+
print format(
|
|
700
|
+
opts[:menu_opts_set_format],
|
|
701
|
+
{ key: key,
|
|
702
|
+
value: value }
|
|
703
|
+
).send(opts[:menu_opts_set_color].to_sym)
|
|
574
704
|
end
|
|
575
|
-
|
|
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
|
|
705
|
+
[!LOAD_FILE, '']
|
|
583
706
|
end
|
|
584
707
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
708
|
+
# Handles reading and processing lines from a given IO stream
|
|
709
|
+
#
|
|
710
|
+
# @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
|
|
711
|
+
# @param file_type [Symbol] The type of file to which the stream corresponds.
|
|
712
|
+
def handle_stream(opts, stream, file_type, swap: false)
|
|
713
|
+
Thread.new do
|
|
714
|
+
until (line = stream.gets).nil?
|
|
715
|
+
@execute_files[file_type] = @execute_files[file_type] + [line.strip]
|
|
716
|
+
print line if opts[:output_stdout]
|
|
717
|
+
yield line if block_given?
|
|
718
|
+
end
|
|
719
|
+
rescue IOError
|
|
720
|
+
#d 'stdout IOError, thread killed, do nothing'
|
|
595
721
|
end
|
|
596
722
|
end
|
|
597
723
|
|
|
598
|
-
def
|
|
599
|
-
|
|
724
|
+
def history_state_exist?
|
|
725
|
+
history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
|
|
726
|
+
history.present? ? history : nil
|
|
727
|
+
end
|
|
600
728
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
729
|
+
def history_state_partition(opts)
|
|
730
|
+
unit, rest = StringUtil.partition_at_first(
|
|
731
|
+
ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
|
|
732
|
+
opts[:history_document_separator]
|
|
733
|
+
)
|
|
734
|
+
{ unit: unit, rest: rest }.tap_inspect
|
|
605
735
|
end
|
|
606
736
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
737
|
+
def history_state_pop(opts)
|
|
738
|
+
state = history_state_partition(opts)
|
|
739
|
+
opts[:filename] = state[:unit]
|
|
740
|
+
ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
|
|
741
|
+
delete_required_temp_file
|
|
742
|
+
end
|
|
610
743
|
|
|
611
|
-
|
|
744
|
+
def history_state_push(mdoc, data_file, opts)
|
|
745
|
+
[data_file, opts[:block_name]].tap_inspect 'filename, blockname'
|
|
746
|
+
new_history = opts[:filename] +
|
|
747
|
+
opts[:history_document_separator] +
|
|
748
|
+
ENV.fetch(MDE_HISTORY_ENV_NAME, '')
|
|
749
|
+
opts[:filename] = data_file
|
|
750
|
+
write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
|
|
751
|
+
ENV[MDE_HISTORY_ENV_NAME] = new_history
|
|
612
752
|
end
|
|
613
753
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
754
|
+
## Sets up the options and returns the parsed arguments
|
|
755
|
+
#
|
|
756
|
+
def initialize_and_parse_cli_options
|
|
757
|
+
@options = base_options
|
|
758
|
+
read_configuration_file!(@options, ".#{MarkdownExec::APP_NAME.downcase}.yml")
|
|
617
759
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
760
|
+
@option_parser = OptionParser.new do |opts|
|
|
761
|
+
executable_name = File.basename($PROGRAM_NAME)
|
|
762
|
+
opts.banner = [
|
|
763
|
+
"#{MarkdownExec::APP_NAME}" \
|
|
764
|
+
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
|
765
|
+
"Usage: #{executable_name} [(path | filename [block_name])] [options]"
|
|
766
|
+
].join("\n")
|
|
767
|
+
|
|
768
|
+
menu_iter do |item|
|
|
769
|
+
menu_option_append opts, @options, item
|
|
623
770
|
end
|
|
624
771
|
end
|
|
772
|
+
@option_parser.load
|
|
773
|
+
@option_parser.environment
|
|
625
774
|
|
|
626
|
-
|
|
775
|
+
rest = @option_parser.parse!(arguments_for_mde)
|
|
776
|
+
@options[:pass_args] = ARGV[rest.count + 1..]
|
|
777
|
+
|
|
778
|
+
rest
|
|
627
779
|
end
|
|
628
780
|
|
|
629
|
-
def
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
781
|
+
def initialize_and_save_execution_output
|
|
782
|
+
return unless @options[:save_execution_output]
|
|
783
|
+
|
|
784
|
+
@options[:logged_stdout_filename] =
|
|
785
|
+
SavedAsset.stdout_name(blockname: @options[:block_name],
|
|
786
|
+
filename: File.basename(@options[:filename], '.*'),
|
|
787
|
+
prefix: @options[:logged_stdout_filename_prefix],
|
|
788
|
+
time: Time.now.utc)
|
|
789
|
+
|
|
790
|
+
@logged_stdout_filespec =
|
|
791
|
+
@options[:logged_stdout_filespec] =
|
|
792
|
+
File.join @options[:saved_stdout_folder],
|
|
793
|
+
@options[:logged_stdout_filename]
|
|
794
|
+
@logged_stdout_filespec = @options[:logged_stdout_filespec]
|
|
795
|
+
write_execution_output_to_file
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
# Initializes variables for regex and other states
|
|
799
|
+
def initialize_state(opts)
|
|
800
|
+
{
|
|
801
|
+
fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
|
|
802
|
+
fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
|
|
803
|
+
fcb: FCB.new,
|
|
804
|
+
in_fenced_block: false,
|
|
805
|
+
headings: []
|
|
806
|
+
}
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Main function to iterate through blocks in file
|
|
810
|
+
def iter_blocks_in_file(opts = {}, &block)
|
|
811
|
+
return unless check_file_existence(opts[:filename])
|
|
812
|
+
|
|
813
|
+
state = initialize_state(opts)
|
|
814
|
+
|
|
815
|
+
# get type of messages to select
|
|
816
|
+
selected_messages = yield :filter
|
|
817
|
+
|
|
818
|
+
cfile.readlines(opts[:filename]).each do |line|
|
|
819
|
+
next unless line
|
|
820
|
+
|
|
821
|
+
update_line_and_block_state(line, state, opts, selected_messages, &block)
|
|
638
822
|
end
|
|
639
823
|
end
|
|
640
824
|
|
|
641
|
-
|
|
642
|
-
#
|
|
825
|
+
##
|
|
826
|
+
# Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
|
|
827
|
+
# The list can be customized via call_options and options_block.
|
|
828
|
+
#
|
|
829
|
+
# @param call_options [Hash] Options passed as an argument.
|
|
830
|
+
# @param options_block [Proc] Block for dynamic option manipulation.
|
|
831
|
+
# @return [Array<FCB>] An array of FCB objects representing the blocks.
|
|
643
832
|
#
|
|
644
833
|
def list_blocks_in_file(call_options = {}, &options_block)
|
|
645
834
|
opts = optsmerge(call_options, options_block)
|
|
646
835
|
use_chrome = !opts[:no_chrome]
|
|
647
836
|
|
|
648
837
|
blocks = []
|
|
649
|
-
|
|
650
|
-
blocks.push FCB.new({
|
|
651
|
-
# name: '',
|
|
652
|
-
chrome: true,
|
|
653
|
-
dname: format(
|
|
654
|
-
opts[:menu_divider_format],
|
|
655
|
-
opts[:menu_initial_divider]
|
|
656
|
-
).send(opts[:menu_divider_color].to_sym),
|
|
657
|
-
oname: opts[:menu_initial_divider],
|
|
658
|
-
disabled: '' # __LINE__.to_s
|
|
659
|
-
})
|
|
660
|
-
end
|
|
838
|
+
append_initial_divider(blocks, opts) if use_chrome
|
|
661
839
|
|
|
662
840
|
iter_blocks_in_file(opts) do |btype, fcb|
|
|
663
841
|
case btype
|
|
664
842
|
when :filter
|
|
665
|
-
|
|
666
|
-
#
|
|
667
|
-
%i[blocks line]
|
|
668
|
-
|
|
843
|
+
filter_block_types
|
|
669
844
|
when :line
|
|
670
|
-
|
|
671
|
-
#
|
|
672
|
-
if opts[:menu_divider_match].present? &&
|
|
673
|
-
(mbody = fcb.body[0].match opts[:menu_divider_match])
|
|
674
|
-
if use_chrome
|
|
675
|
-
blocks.push FCB.new(
|
|
676
|
-
{ chrome: true,
|
|
677
|
-
disabled: '',
|
|
678
|
-
dname: format(opts[:menu_divider_format],
|
|
679
|
-
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
|
680
|
-
oname: mbody[:name] }
|
|
681
|
-
)
|
|
682
|
-
end
|
|
683
|
-
elsif opts[:menu_task_match].present? &&
|
|
684
|
-
(fcb.body[0].match opts[:menu_task_match])
|
|
685
|
-
if use_chrome
|
|
686
|
-
blocks.push FCB.new(
|
|
687
|
-
{ chrome: true,
|
|
688
|
-
disabled: '',
|
|
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(
|
|
694
|
-
opts[:menu_task_format],
|
|
695
|
-
$~.named_captures.transform_keys(&:to_sym)
|
|
696
|
-
) }
|
|
697
|
-
)
|
|
698
|
-
end
|
|
699
|
-
else
|
|
700
|
-
# line not added
|
|
701
|
-
end
|
|
845
|
+
process_line_blocks(blocks, fcb, opts, use_chrome)
|
|
702
846
|
when :blocks
|
|
703
|
-
|
|
704
|
-
#
|
|
705
|
-
blocks.push get_block_summary(opts, fcb) ### if Filter.fcb_select? opts, fcb
|
|
847
|
+
append_block_summary(blocks, fcb, opts)
|
|
706
848
|
end
|
|
707
849
|
end
|
|
708
850
|
|
|
709
|
-
|
|
710
|
-
blocks.push FCB.new(
|
|
711
|
-
{ chrome: true,
|
|
712
|
-
disabled: '',
|
|
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] }
|
|
717
|
-
)
|
|
718
|
-
end
|
|
851
|
+
append_final_divider(blocks, opts) if use_chrome
|
|
719
852
|
blocks
|
|
720
853
|
rescue StandardError => err
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
854
|
+
handle_error(err)
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
##
|
|
858
|
+
# Processes lines within the file and converts them into blocks if they match certain criteria.
|
|
859
|
+
#
|
|
860
|
+
def process_line_blocks(blocks, fcb, opts, use_chrome)
|
|
861
|
+
## convert line to block
|
|
862
|
+
#
|
|
863
|
+
if opts[:menu_divider_match].present? &&
|
|
864
|
+
(mbody = fcb.body[0].match opts[:menu_divider_match])
|
|
865
|
+
if use_chrome
|
|
866
|
+
blocks.push FCB.new(
|
|
867
|
+
{ chrome: true,
|
|
868
|
+
disabled: '',
|
|
869
|
+
dname: format(opts[:menu_divider_format],
|
|
870
|
+
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
|
871
|
+
oname: mbody[:name] }
|
|
872
|
+
)
|
|
873
|
+
end
|
|
874
|
+
elsif opts[:menu_task_match].present? &&
|
|
875
|
+
(fcb.body[0].match opts[:menu_task_match])
|
|
876
|
+
if use_chrome
|
|
877
|
+
blocks.push FCB.new(
|
|
878
|
+
{ chrome: true,
|
|
879
|
+
disabled: '',
|
|
880
|
+
dname: format(
|
|
881
|
+
opts[:menu_task_format],
|
|
882
|
+
$~.named_captures.transform_keys(&:to_sym)
|
|
883
|
+
).send(opts[:menu_task_color].to_sym),
|
|
884
|
+
oname: format(
|
|
885
|
+
opts[:menu_task_format],
|
|
886
|
+
$~.named_captures.transform_keys(&:to_sym)
|
|
887
|
+
) }
|
|
888
|
+
)
|
|
889
|
+
end
|
|
890
|
+
else
|
|
891
|
+
# line not added
|
|
892
|
+
end
|
|
724
893
|
end
|
|
725
894
|
|
|
726
895
|
def list_default_env
|
|
@@ -747,39 +916,22 @@ module MarkdownExec
|
|
|
747
916
|
|
|
748
917
|
def list_files_per_options(options)
|
|
749
918
|
list_files_specified(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
919
|
+
determine_filename(
|
|
920
|
+
specified_filename: options[:filename]&.present? ? options[:filename] : nil,
|
|
921
|
+
specified_folder: options[:path],
|
|
922
|
+
default_filename: 'README.md',
|
|
923
|
+
default_folder: '.'
|
|
924
|
+
)
|
|
754
925
|
)
|
|
755
926
|
end
|
|
756
927
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
[specified_folder, specified_filename]
|
|
765
|
-
else
|
|
766
|
-
[default_folder, specified_filename]
|
|
767
|
-
end
|
|
768
|
-
elsif specified_folder&.present?
|
|
769
|
-
if filetree
|
|
770
|
-
[specified_folder, @options[:md_filename_match]]
|
|
771
|
-
else
|
|
772
|
-
[specified_folder, @options[:md_filename_glob]]
|
|
773
|
-
end
|
|
774
|
-
else
|
|
775
|
-
[default_folder, default_filename]
|
|
776
|
-
end)
|
|
777
|
-
if filetree
|
|
778
|
-
filetree.select do |filename|
|
|
779
|
-
filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
|
|
780
|
-
end
|
|
781
|
-
else
|
|
782
|
-
Dir.glob(fn)
|
|
928
|
+
## Searches for files based on the specified or default filenames and folders
|
|
929
|
+
#
|
|
930
|
+
def list_files_specified(fn, filetree = nil)
|
|
931
|
+
return Dir.glob(fn) unless filetree
|
|
932
|
+
|
|
933
|
+
filetree.select do |filename|
|
|
934
|
+
filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
|
|
783
935
|
end
|
|
784
936
|
end
|
|
785
937
|
|
|
@@ -788,17 +940,6 @@ module MarkdownExec
|
|
|
788
940
|
@options[:md_filename_glob]))
|
|
789
941
|
end
|
|
790
942
|
|
|
791
|
-
def blocks_per_opts(blocks, opts)
|
|
792
|
-
if opts[:struct]
|
|
793
|
-
blocks
|
|
794
|
-
else
|
|
795
|
-
# blocks.map(&:name)
|
|
796
|
-
blocks.map do |block|
|
|
797
|
-
block.fetch(:text, nil) || block.oname
|
|
798
|
-
end
|
|
799
|
-
end.compact.reject(&:empty?)
|
|
800
|
-
end
|
|
801
|
-
|
|
802
943
|
## output type (body string or full object) per option struct and bash
|
|
803
944
|
#
|
|
804
945
|
def list_named_blocks_in_file(call_options = {}, &options_block)
|
|
@@ -811,6 +952,17 @@ module MarkdownExec
|
|
|
811
952
|
blocks_per_opts(blocks, opts)
|
|
812
953
|
end
|
|
813
954
|
|
|
955
|
+
## Handles the file loading and returns the blocks in the file and MDoc instance
|
|
956
|
+
#
|
|
957
|
+
def load_file_and_prepare_menu(opts)
|
|
958
|
+
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
|
959
|
+
mdoc = MDoc.new(blocks_in_file) do |nopts|
|
|
960
|
+
opts.merge!(nopts)
|
|
961
|
+
end
|
|
962
|
+
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
|
963
|
+
[blocks_in_file, blocks_menu, mdoc]
|
|
964
|
+
end
|
|
965
|
+
|
|
814
966
|
def make_block_labels(call_options = {})
|
|
815
967
|
opts = options.merge(call_options)
|
|
816
968
|
list_blocks_in_file(opts).map do |fcb|
|
|
@@ -826,6 +978,32 @@ module MarkdownExec
|
|
|
826
978
|
end.compact
|
|
827
979
|
end
|
|
828
980
|
|
|
981
|
+
def menu_export(data = menu_for_optparse)
|
|
982
|
+
data.map do |item|
|
|
983
|
+
item.delete(:procname)
|
|
984
|
+
item
|
|
985
|
+
end.to_yaml
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
def menu_for_blocks(menu_options)
|
|
989
|
+
options = calculated_options.merge menu_options
|
|
990
|
+
menu = []
|
|
991
|
+
iter_blocks_in_file(options) do |btype, fcb|
|
|
992
|
+
case btype
|
|
993
|
+
when :filter
|
|
994
|
+
%i[blocks line]
|
|
995
|
+
when :line
|
|
996
|
+
if options[:menu_divider_match] &&
|
|
997
|
+
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
|
998
|
+
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
|
|
999
|
+
end
|
|
1000
|
+
when :blocks
|
|
1001
|
+
menu += [fcb.oname]
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
menu
|
|
1005
|
+
end
|
|
1006
|
+
|
|
829
1007
|
# :reek:DuplicateMethodCall
|
|
830
1008
|
# :reek:NestedIterators
|
|
831
1009
|
def menu_for_optparse
|
|
@@ -849,11 +1027,11 @@ module MarkdownExec
|
|
|
849
1027
|
}
|
|
850
1028
|
when 'path'
|
|
851
1029
|
lambda { |value|
|
|
852
|
-
read_configuration_file!
|
|
1030
|
+
read_configuration_file!(options, value)
|
|
853
1031
|
}
|
|
854
1032
|
when 'show_config'
|
|
855
1033
|
lambda { |_|
|
|
856
|
-
|
|
1034
|
+
finalize_cli_argument_processing(options)
|
|
857
1035
|
fout options.sort_by_key.to_yaml
|
|
858
1036
|
}
|
|
859
1037
|
when 'val_as_bool'
|
|
@@ -877,33 +1055,14 @@ module MarkdownExec
|
|
|
877
1055
|
end
|
|
878
1056
|
end
|
|
879
1057
|
|
|
880
|
-
def
|
|
881
|
-
|
|
882
|
-
menu = []
|
|
883
|
-
iter_blocks_in_file(options) do |btype, fcb|
|
|
884
|
-
case btype
|
|
885
|
-
when :filter
|
|
886
|
-
%i[blocks line]
|
|
887
|
-
when :line
|
|
888
|
-
if options[:menu_divider_match] &&
|
|
889
|
-
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
|
890
|
-
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
|
|
891
|
-
end
|
|
892
|
-
when :blocks
|
|
893
|
-
menu += [fcb.oname]
|
|
894
|
-
end
|
|
895
|
-
end
|
|
896
|
-
menu
|
|
1058
|
+
def menu_help
|
|
1059
|
+
@option_parser.help
|
|
897
1060
|
end
|
|
898
1061
|
|
|
899
1062
|
def menu_iter(data = menu_for_optparse, &block)
|
|
900
1063
|
data.map(&block)
|
|
901
1064
|
end
|
|
902
1065
|
|
|
903
|
-
def menu_help
|
|
904
|
-
@option_parser.help
|
|
905
|
-
end
|
|
906
|
-
|
|
907
1066
|
def menu_option_append(opts, options, item)
|
|
908
1067
|
return unless item[:long_name].present? || item[:short_name].present?
|
|
909
1068
|
|
|
@@ -931,25 +1090,13 @@ module MarkdownExec
|
|
|
931
1090
|
].compact)
|
|
932
1091
|
end
|
|
933
1092
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if Dir.exist?(pos)
|
|
941
|
-
@options[:path] = pos
|
|
942
|
-
elsif File.exist?(pos)
|
|
943
|
-
@options[:filename] = pos
|
|
944
|
-
else
|
|
945
|
-
raise FileMissingError, pos, caller
|
|
946
|
-
end
|
|
1093
|
+
def next_block_name_from_command_line_arguments(opts)
|
|
1094
|
+
if opts[:cli_rest].present?
|
|
1095
|
+
opts[:block_name] = opts[:cli_rest].pop
|
|
1096
|
+
false # repeat_menu
|
|
1097
|
+
else
|
|
1098
|
+
true # repeat_menu
|
|
947
1099
|
end
|
|
948
|
-
|
|
949
|
-
## position 1: block name (optional)
|
|
950
|
-
#
|
|
951
|
-
block_name = rest.shift
|
|
952
|
-
@options[:block_name] = block_name if block_name.present?
|
|
953
1100
|
end
|
|
954
1101
|
|
|
955
1102
|
# :reek:ControlParameter
|
|
@@ -995,63 +1142,112 @@ module MarkdownExec
|
|
|
995
1142
|
}
|
|
996
1143
|
end
|
|
997
1144
|
|
|
998
|
-
|
|
999
|
-
#
|
|
1000
|
-
## Adds a back option at the head or tail of a menu
|
|
1145
|
+
# Prepare the blocks menu by adding labels and other necessary details.
|
|
1001
1146
|
#
|
|
1002
|
-
|
|
1003
|
-
|
|
1147
|
+
# @param blocks_in_file [Array<Hash>] The list of blocks from the file.
|
|
1148
|
+
# @param opts [Hash] The options hash.
|
|
1149
|
+
# @return [Array<Hash>] The updated blocks menu.
|
|
1150
|
+
def prepare_blocks_menu(blocks_in_file, opts)
|
|
1151
|
+
# next if fcb.fetch(:disabled, false)
|
|
1152
|
+
# next unless fcb.fetch(:name, '').present?
|
|
1153
|
+
blocks_in_file.map do |fcb|
|
|
1154
|
+
fcb.merge!(
|
|
1155
|
+
name: fcb.dname,
|
|
1156
|
+
label: BlockLabel.make(
|
|
1157
|
+
body: fcb[:body],
|
|
1158
|
+
filename: opts[:filename],
|
|
1159
|
+
headings: fcb.fetch(:headings, []),
|
|
1160
|
+
menu_blocks_with_docname: opts[:menu_blocks_with_docname],
|
|
1161
|
+
menu_blocks_with_headings: opts[:menu_blocks_with_headings],
|
|
1162
|
+
text: fcb[:text],
|
|
1163
|
+
title: fcb[:title]
|
|
1164
|
+
)
|
|
1165
|
+
)
|
|
1166
|
+
fcb.to_h
|
|
1167
|
+
end.compact
|
|
1168
|
+
end
|
|
1004
1169
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1170
|
+
# Prepares and fetches file listings
|
|
1171
|
+
def prepare_file_list(options)
|
|
1172
|
+
list_files_per_options(options)
|
|
1173
|
+
end
|
|
1007
1174
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
)
|
|
1175
|
+
def process_fenced_block(fcb, opts, selected_messages, &block)
|
|
1176
|
+
fcb.oname = fcb.dname = fcb.title || ''
|
|
1177
|
+
return unless fcb.body
|
|
1012
1178
|
|
|
1013
|
-
|
|
1179
|
+
set_fcb_title(fcb)
|
|
1180
|
+
|
|
1181
|
+
if block &&
|
|
1182
|
+
selected_messages.include?(:blocks) &&
|
|
1183
|
+
Filter.fcb_select?(opts, fcb)
|
|
1184
|
+
block.call :blocks, fcb
|
|
1185
|
+
end
|
|
1014
1186
|
end
|
|
1015
1187
|
|
|
1016
|
-
|
|
1188
|
+
def process_line(line, _opts, selected_messages, &block)
|
|
1189
|
+
return unless block && selected_messages.include?(:line)
|
|
1190
|
+
|
|
1191
|
+
# dp 'text outside of fcb'
|
|
1192
|
+
fcb = FCB.new
|
|
1193
|
+
fcb.body = [line]
|
|
1194
|
+
block.call(:line, fcb)
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
##
|
|
1198
|
+
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
|
1199
|
+
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
|
1017
1200
|
#
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1201
|
+
# @param opts [Hash] A hash containing various options for the menu.
|
|
1202
|
+
# @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
|
|
1203
|
+
#
|
|
1204
|
+
# @option opts [String] :prompt_approve_block Prompt text for the approval menu.
|
|
1205
|
+
# @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
|
|
1206
|
+
# @option opts [String] :prompt_no Text for the 'No' choice in the menu.
|
|
1207
|
+
# @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
|
|
1208
|
+
# @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
|
|
1209
|
+
#
|
|
1210
|
+
# @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
|
|
1211
|
+
##
|
|
1212
|
+
def prompt_for_user_approval(opts, required_lines)
|
|
1213
|
+
# Present a selection menu for user approval.
|
|
1214
|
+
sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
|
|
1215
|
+
menu.default 1
|
|
1216
|
+
menu.choice opts[:prompt_yes], 1
|
|
1217
|
+
menu.choice opts[:prompt_no], 2
|
|
1218
|
+
menu.choice opts[:prompt_script_to_clipboard], 3
|
|
1219
|
+
menu.choice opts[:prompt_save_script], 4
|
|
1023
1220
|
end
|
|
1221
|
+
|
|
1222
|
+
if sel == 3
|
|
1223
|
+
copy_to_clipboard(required_lines)
|
|
1224
|
+
elsif sel == 4
|
|
1225
|
+
save_to_file(opts, required_lines)
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1228
|
+
sel == 1
|
|
1024
1229
|
end
|
|
1025
1230
|
|
|
1026
|
-
##
|
|
1027
|
-
# insert exit option at head or tail
|
|
1028
|
-
# return selected option or nil
|
|
1231
|
+
## insert back option at head or tail
|
|
1029
1232
|
#
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1233
|
+
## Adds a back option at the head or tail of a menu
|
|
1234
|
+
#
|
|
1235
|
+
def prompt_menu_add_back(items, label)
|
|
1236
|
+
return items unless @options[:menu_with_back] && history_state_exist?
|
|
1237
|
+
|
|
1238
|
+
state = history_state_partition(@options)
|
|
1239
|
+
@hs_curr = state[:unit]
|
|
1240
|
+
@hs_rest = state[:rest]
|
|
1241
|
+
@options[:menu_back_at_top] ? [label] + items : items + [label]
|
|
1037
1242
|
end
|
|
1038
1243
|
|
|
1039
|
-
##
|
|
1040
|
-
# insert exit option at head or tail
|
|
1041
|
-
# return option:, selected option:
|
|
1244
|
+
## insert exit option at head or tail
|
|
1042
1245
|
#
|
|
1043
|
-
def
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
prompt_menu_add_back(items)
|
|
1047
|
-
),
|
|
1048
|
-
opts.merge(filter: true))
|
|
1049
|
-
if sel == BACK_OPTION
|
|
1050
|
-
{ option: sel, curr: @hs_curr, rest: @hs_rest }
|
|
1051
|
-
elsif sel == EXIT_OPTION
|
|
1052
|
-
{ option: sel }
|
|
1246
|
+
def prompt_menu_add_exit(items, label)
|
|
1247
|
+
if @options[:menu_exit_at_top]
|
|
1248
|
+
(@options[:menu_with_exit] ? [label] : []) + items
|
|
1053
1249
|
else
|
|
1054
|
-
|
|
1250
|
+
items + (@options[:menu_with_exit] ? [label] : [])
|
|
1055
1251
|
end
|
|
1056
1252
|
end
|
|
1057
1253
|
|
|
@@ -1063,54 +1259,33 @@ module MarkdownExec
|
|
|
1063
1259
|
.transform_keys(&:to_sym))
|
|
1064
1260
|
end
|
|
1065
1261
|
|
|
1066
|
-
#
|
|
1067
|
-
|
|
1068
|
-
|
|
1262
|
+
# Reads required code blocks from a temporary file specified by an environment variable.
|
|
1263
|
+
#
|
|
1264
|
+
# @return [Array<String>] An array containing the lines read from the temporary file.
|
|
1265
|
+
# @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
|
|
1266
|
+
def read_required_blocks_from_temp_file
|
|
1267
|
+
temp_blocks = []
|
|
1069
1268
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1269
|
+
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
|
1270
|
+
return temp_blocks if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
|
1072
1271
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
opts.banner = [
|
|
1076
|
-
"#{MarkdownExec::APP_NAME}" \
|
|
1077
|
-
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
|
1078
|
-
"Usage: #{executable_name} [(path | filename [block_name])] [options]"
|
|
1079
|
-
].join("\n")
|
|
1080
|
-
|
|
1081
|
-
menu_iter do |item|
|
|
1082
|
-
menu_option_append opts, options, item
|
|
1083
|
-
end
|
|
1272
|
+
if File.exist?(temp_blocks_file_path)
|
|
1273
|
+
temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
|
|
1084
1274
|
end
|
|
1085
|
-
option_parser.load
|
|
1086
|
-
option_parser.environment
|
|
1087
|
-
rest = option_parser.parse!(arguments_for_mde) # (into: options)
|
|
1088
1275
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1276
|
+
temp_blocks
|
|
1277
|
+
end
|
|
1091
1278
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
puts "File missing: #{err}"
|
|
1097
|
-
end
|
|
1279
|
+
def run
|
|
1280
|
+
clear_required_file
|
|
1281
|
+
execute_block_with_error_handling(initialize_and_parse_cli_options)
|
|
1282
|
+
delete_required_temp_file
|
|
1098
1283
|
rescue StandardError => err
|
|
1099
1284
|
warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
|
|
1100
1285
|
binding.pry if $tap_enable
|
|
1101
1286
|
raise ArgumentError, error
|
|
1102
1287
|
end
|
|
1103
1288
|
|
|
1104
|
-
def saved_name_split(name)
|
|
1105
|
-
# rubocop:disable Layout/LineLength
|
|
1106
|
-
mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
|
|
1107
|
-
# rubocop:enable Layout/LineLength
|
|
1108
|
-
return unless mf
|
|
1109
|
-
|
|
1110
|
-
@options[:block_name] = mf[:block]
|
|
1111
|
-
@options[:filename] = mf[:file].gsub(FNR12, FNR11)
|
|
1112
|
-
end
|
|
1113
|
-
|
|
1114
1289
|
def run_last_script
|
|
1115
1290
|
filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
|
|
1116
1291
|
@options[:saved_script_glob])
|
|
@@ -1121,55 +1296,20 @@ module MarkdownExec
|
|
|
1121
1296
|
select_approve_and_execute_block({})
|
|
1122
1297
|
end
|
|
1123
1298
|
|
|
1124
|
-
def
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
@options[:
|
|
1128
|
-
SavedAsset.stdout_name(blockname: @options[:block_name],
|
|
1129
|
-
filename: File.basename(@options[:filename], '.*'),
|
|
1130
|
-
prefix: @options[:logged_stdout_filename_prefix],
|
|
1131
|
-
time: Time.now.utc)
|
|
1132
|
-
|
|
1133
|
-
@options[:logged_stdout_filespec] =
|
|
1134
|
-
File.join @options[:saved_stdout_folder],
|
|
1135
|
-
@options[:logged_stdout_filename]
|
|
1136
|
-
@logged_stdout_filespec = @options[:logged_stdout_filespec]
|
|
1137
|
-
(dirname = File.dirname(@options[:logged_stdout_filespec]))
|
|
1138
|
-
FileUtils.mkdir_p dirname
|
|
1139
|
-
|
|
1140
|
-
ol = ["-STDOUT-\n"]
|
|
1141
|
-
ol += @execute_files&.fetch(EF_STDOUT, [])
|
|
1142
|
-
ol += ["\n-STDERR-\n"]
|
|
1143
|
-
ol += @execute_files&.fetch(EF_STDERR, [])
|
|
1144
|
-
ol += ["\n-STDIN-\n"]
|
|
1145
|
-
ol += @execute_files&.fetch(EF_STDIN, [])
|
|
1146
|
-
ol += ["\n"]
|
|
1147
|
-
File.write(@options[:logged_stdout_filespec], ol.join)
|
|
1299
|
+
def save_to_file(opts, required_lines)
|
|
1300
|
+
write_command_file(opts.merge(save_executed_script: true),
|
|
1301
|
+
required_lines)
|
|
1302
|
+
fout "File saved: #{@options[:saved_filespec]}"
|
|
1148
1303
|
end
|
|
1149
1304
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
|
1305
|
+
def saved_name_split(name)
|
|
1306
|
+
# rubocop:disable Layout/LineLength
|
|
1307
|
+
mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
|
|
1308
|
+
# rubocop:enable Layout/LineLength
|
|
1309
|
+
return unless mf
|
|
1310
|
+
|
|
1311
|
+
@options[:block_name] = mf[:block]
|
|
1312
|
+
@options[:filename] = mf[:file].gsub(FNR12, FNR11)
|
|
1173
1313
|
end
|
|
1174
1314
|
|
|
1175
1315
|
# Select and execute a code block from a Markdown document.
|
|
@@ -1183,62 +1323,37 @@ module MarkdownExec
|
|
|
1183
1323
|
def select_approve_and_execute_block(call_options, &options_block)
|
|
1184
1324
|
opts = optsmerge(call_options, options_block)
|
|
1185
1325
|
repeat_menu = true && !opts[:block_name].present?
|
|
1186
|
-
|
|
1187
1326
|
load_file = !LOAD_FILE
|
|
1327
|
+
default = 1
|
|
1328
|
+
|
|
1188
1329
|
loop do
|
|
1189
|
-
# load file
|
|
1190
|
-
#
|
|
1191
1330
|
loop do
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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))
|
|
1331
|
+
opts[:back] = false
|
|
1332
|
+
blocks_in_file, blocks_menu, mdoc = load_file_and_prepare_menu(opts)
|
|
1333
|
+
|
|
1200
1334
|
unless opts[:block_name].present?
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
|
1335
|
+
block_name, state = wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
|
1336
|
+
opts)
|
|
1337
|
+
case state
|
|
1338
|
+
when :exit
|
|
1210
1339
|
return nil
|
|
1211
|
-
when
|
|
1212
|
-
opts[:
|
|
1213
|
-
opts[:
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
|
1340
|
+
when :back
|
|
1341
|
+
opts[:block_name] = block_name[:option]
|
|
1342
|
+
opts[:back] = true
|
|
1343
|
+
when :continue
|
|
1344
|
+
opts[:block_name] = block_name
|
|
1225
1345
|
end
|
|
1226
1346
|
end
|
|
1227
|
-
break if load_file == LOAD_FILE
|
|
1228
|
-
|
|
1229
|
-
# later: load file
|
|
1230
|
-
|
|
1231
|
-
load_file, block_name = approve_and_execute_block(opts, mdoc)
|
|
1232
|
-
|
|
1233
|
-
opts[:block_name] = block_name
|
|
1234
|
-
if load_file == LOAD_FILE
|
|
1235
|
-
repeat_menu = true
|
|
1236
|
-
break
|
|
1237
|
-
end
|
|
1238
1347
|
|
|
1348
|
+
load_file, next_block_name = approve_and_execute_block(opts, mdoc)
|
|
1349
|
+
default = load_file == LOAD_FILE ? 1 : opts[:block_name]
|
|
1350
|
+
opts[:block_name] = next_block_name
|
|
1351
|
+
break if state == :continue && load_file == LOAD_FILE
|
|
1239
1352
|
break unless repeat_menu
|
|
1240
1353
|
end
|
|
1241
1354
|
break if load_file != LOAD_FILE
|
|
1355
|
+
|
|
1356
|
+
repeat_menu = next_block_name_from_command_line_arguments(opts)
|
|
1242
1357
|
end
|
|
1243
1358
|
rescue StandardError => err
|
|
1244
1359
|
warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
|
|
@@ -1247,25 +1362,53 @@ module MarkdownExec
|
|
|
1247
1362
|
raise ArgumentError, error
|
|
1248
1363
|
end
|
|
1249
1364
|
|
|
1250
|
-
def
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1365
|
+
def select_document_if_multiple(files = list_markdown_files_in_path)
|
|
1366
|
+
return files[0] if (count = files.count) == 1
|
|
1367
|
+
|
|
1368
|
+
return unless count >= 2
|
|
1369
|
+
|
|
1370
|
+
opts = options.dup
|
|
1371
|
+
select_option_or_exit opts[:prompt_select_md].to_s, files,
|
|
1372
|
+
opts.merge(per_page: opts[:select_page_height])
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
# Presents a TTY prompt to select an option or exit, returns selected option or nil
|
|
1376
|
+
def select_option_or_exit(prompt_text, items, opts = {})
|
|
1377
|
+
result = select_option_with_metadata(prompt_text, items, opts)
|
|
1378
|
+
return unless result.fetch(:option, nil)
|
|
1379
|
+
|
|
1380
|
+
result[:selected]
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
|
1384
|
+
def select_option_with_metadata(prompt_text, items, opts = {})
|
|
1385
|
+
selection = @prompt.select(prompt_text,
|
|
1386
|
+
prompt_menu_add_exit(
|
|
1387
|
+
prompt_menu_add_back(
|
|
1388
|
+
items,
|
|
1389
|
+
opts[:menu_option_back_name]
|
|
1390
|
+
),
|
|
1391
|
+
opts[:menu_option_exit_name]
|
|
1392
|
+
),
|
|
1393
|
+
opts.merge(filter: true))
|
|
1394
|
+
if selection == opts[:menu_option_back_name]
|
|
1395
|
+
{ option: selection, curr: @hs_curr, rest: @hs_rest, shell: BLOCK_TYPE_LINK }
|
|
1396
|
+
elsif selection == opts[:menu_option_exit_name]
|
|
1397
|
+
{ option: selection }
|
|
1398
|
+
else
|
|
1399
|
+
{ selected: selection }
|
|
1257
1400
|
end
|
|
1258
1401
|
end
|
|
1259
1402
|
|
|
1260
1403
|
def select_recent_output
|
|
1261
|
-
filename =
|
|
1404
|
+
filename = select_option_or_exit(
|
|
1262
1405
|
@options[:prompt_select_output].to_s,
|
|
1263
1406
|
list_recent_output(
|
|
1264
1407
|
@options[:saved_stdout_folder],
|
|
1265
1408
|
@options[:saved_stdout_glob],
|
|
1266
1409
|
@options[:list_count]
|
|
1267
1410
|
),
|
|
1268
|
-
{ per_page: @options[:select_page_height] }
|
|
1411
|
+
@options.merge({ per_page: @options[:select_page_height] })
|
|
1269
1412
|
)
|
|
1270
1413
|
return unless filename.present?
|
|
1271
1414
|
|
|
@@ -1273,14 +1416,14 @@ module MarkdownExec
|
|
|
1273
1416
|
end
|
|
1274
1417
|
|
|
1275
1418
|
def select_recent_script
|
|
1276
|
-
filename =
|
|
1419
|
+
filename = select_option_or_exit(
|
|
1277
1420
|
@options[:prompt_select_md].to_s,
|
|
1278
1421
|
list_recent_scripts(
|
|
1279
1422
|
@options[:saved_script_folder],
|
|
1280
1423
|
@options[:saved_script_glob],
|
|
1281
1424
|
@options[:list_count]
|
|
1282
1425
|
),
|
|
1283
|
-
{ per_page: @options[:select_page_height] }
|
|
1426
|
+
@options.merge({ per_page: @options[:select_page_height] })
|
|
1284
1427
|
)
|
|
1285
1428
|
return if filename.nil?
|
|
1286
1429
|
|
|
@@ -1293,27 +1436,37 @@ module MarkdownExec
|
|
|
1293
1436
|
})
|
|
1294
1437
|
end
|
|
1295
1438
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1439
|
+
# set the title of an FCB object based on its body if it is nil or empty
|
|
1440
|
+
def set_fcb_title(fcb)
|
|
1441
|
+
return unless fcb.title.nil? || fcb.title.empty?
|
|
1442
|
+
|
|
1443
|
+
fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
|
|
1301
1444
|
end
|
|
1302
1445
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1446
|
+
def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
|
1447
|
+
fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
|
|
1448
|
+
rest = fcb_title_groups.fetch(:rest, '')
|
|
1449
|
+
|
|
1450
|
+
fcb = FCB.new
|
|
1451
|
+
fcb.headings = headings
|
|
1452
|
+
fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
|
|
1453
|
+
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
|
1454
|
+
fcb.title = fcb_title_groups.fetch(:name, '')
|
|
1455
|
+
fcb.body = []
|
|
1456
|
+
fcb.reqs, fcb.wraps =
|
|
1457
|
+
ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
|
|
1458
|
+
req[1..-1]
|
|
1459
|
+
end) do |name|
|
|
1460
|
+
!name.match(Regexp.new(opts[:block_name_wrapper_match]))
|
|
1316
1461
|
end
|
|
1462
|
+
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
|
1463
|
+
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
|
1464
|
+
tn.named_captures.sym_keys
|
|
1465
|
+
end
|
|
1466
|
+
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
|
1467
|
+
tn.named_captures.sym_keys
|
|
1468
|
+
end
|
|
1469
|
+
fcb
|
|
1317
1470
|
end
|
|
1318
1471
|
|
|
1319
1472
|
def tab_completions(data = menu_for_optparse)
|
|
@@ -1322,6 +1475,77 @@ module MarkdownExec
|
|
|
1322
1475
|
end.compact
|
|
1323
1476
|
end
|
|
1324
1477
|
|
|
1478
|
+
def tty_prompt_without_disabled_symbol
|
|
1479
|
+
TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1482
|
+
##
|
|
1483
|
+
# Updates the hierarchy of document headings based on the given line and existing headings.
|
|
1484
|
+
# The function uses regular expressions specified in the `opts` to identify different levels of headings.
|
|
1485
|
+
#
|
|
1486
|
+
# @param line [String] The line of text to examine for heading content.
|
|
1487
|
+
# @param headings [Array<String>] The existing list of document headings.
|
|
1488
|
+
# @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
|
|
1489
|
+
#
|
|
1490
|
+
# @option opts [String] :heading1_match Regular expression for matching first-level headings.
|
|
1491
|
+
# @option opts [String] :heading2_match Regular expression for matching second-level headings.
|
|
1492
|
+
# @option opts [String] :heading3_match Regular expression for matching third-level headings.
|
|
1493
|
+
#
|
|
1494
|
+
# @return [Array<String>] Updated list of headings.
|
|
1495
|
+
def update_document_headings(line, headings, opts)
|
|
1496
|
+
if (lm = line.match(Regexp.new(opts[:heading3_match])))
|
|
1497
|
+
[headings[0], headings[1], lm[:name]]
|
|
1498
|
+
elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
|
|
1499
|
+
[headings[0], lm[:name]]
|
|
1500
|
+
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
|
1501
|
+
[lm[:name]]
|
|
1502
|
+
else
|
|
1503
|
+
headings
|
|
1504
|
+
end
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1507
|
+
##
|
|
1508
|
+
# Processes an individual line within a loop, updating headings and handling fenced code blocks.
|
|
1509
|
+
# This function is designed to be called within a loop that iterates through each line of a document.
|
|
1510
|
+
#
|
|
1511
|
+
# @param line [String] The current line being processed.
|
|
1512
|
+
# @param state [Hash] The current state of the parser, including flags and data related to the processing.
|
|
1513
|
+
# @param opts [Hash] A hash containing various options for line and block processing.
|
|
1514
|
+
# @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
|
|
1515
|
+
# @param block [Proc] An optional block for further processing or transformation of lines.
|
|
1516
|
+
#
|
|
1517
|
+
# @option state [Array<String>] :headings Current headings to be updated based on the line.
|
|
1518
|
+
# @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
|
|
1519
|
+
# @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
|
|
1520
|
+
# @option state [Object] :fcb An object representing the current fenced code block being processed.
|
|
1521
|
+
#
|
|
1522
|
+
# @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
|
|
1523
|
+
#
|
|
1524
|
+
# @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
|
|
1525
|
+
##
|
|
1526
|
+
def update_line_and_block_state(line, state, opts, selected_messages, &block)
|
|
1527
|
+
if opts[:menu_blocks_with_headings]
|
|
1528
|
+
state[:headings] =
|
|
1529
|
+
update_document_headings(line, state[:headings], opts)
|
|
1530
|
+
end
|
|
1531
|
+
|
|
1532
|
+
if line.match(state[:fenced_start_and_end_regex])
|
|
1533
|
+
if state[:in_fenced_block]
|
|
1534
|
+
process_fenced_block(state[:fcb], opts, selected_messages, &block)
|
|
1535
|
+
state[:in_fenced_block] = false
|
|
1536
|
+
else
|
|
1537
|
+
state[:fcb] =
|
|
1538
|
+
start_fenced_block(opts, line, state[:headings],
|
|
1539
|
+
state[:fenced_start_extended_regex])
|
|
1540
|
+
state[:in_fenced_block] = true
|
|
1541
|
+
end
|
|
1542
|
+
elsif state[:in_fenced_block] && state[:fcb].body
|
|
1543
|
+
state[:fcb].body += [line.chomp]
|
|
1544
|
+
else
|
|
1545
|
+
process_line(line, opts, selected_messages, &block)
|
|
1546
|
+
end
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1325
1549
|
# :reek:BooleanParameter
|
|
1326
1550
|
# :reek:ControlParameter
|
|
1327
1551
|
def update_options(opts = {}, over: true)
|
|
@@ -1333,6 +1557,29 @@ module MarkdownExec
|
|
|
1333
1557
|
@options
|
|
1334
1558
|
end
|
|
1335
1559
|
|
|
1560
|
+
## Handles the menu interaction and returns selected block name and option state
|
|
1561
|
+
#
|
|
1562
|
+
def wait_for_user_selection(blocks_in_file, blocks_menu, default, opts)
|
|
1563
|
+
pt = opts[:prompt_select_block].to_s
|
|
1564
|
+
bm = prepare_blocks_menu(blocks_menu, opts)
|
|
1565
|
+
return [nil, :exit] if bm.count.zero?
|
|
1566
|
+
|
|
1567
|
+
obj = select_option_with_metadata(pt, bm, opts.merge(
|
|
1568
|
+
default: default,
|
|
1569
|
+
per_page: opts[:select_page_height]
|
|
1570
|
+
))
|
|
1571
|
+
case obj.fetch(:option, nil)
|
|
1572
|
+
when opts[:menu_option_exit_name]
|
|
1573
|
+
[nil, :exit]
|
|
1574
|
+
when opts[:menu_option_back_name]
|
|
1575
|
+
[obj, :back]
|
|
1576
|
+
else
|
|
1577
|
+
label_block = blocks_in_file.find { |fcb| fcb.dname == obj[:selected] }
|
|
1578
|
+
[label_block.oname, :continue]
|
|
1579
|
+
end
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
# Handles the core logic for generating the command file's metadata and content.
|
|
1336
1583
|
def write_command_file(call_options, required_lines)
|
|
1337
1584
|
return unless call_options[:save_executed_script]
|
|
1338
1585
|
|
|
@@ -1348,22 +1595,51 @@ module MarkdownExec
|
|
|
1348
1595
|
@options[:saved_filespec] =
|
|
1349
1596
|
File.join opts[:saved_script_folder], opts[:saved_script_filename]
|
|
1350
1597
|
|
|
1351
|
-
dirname = File.dirname(@options[:saved_filespec])
|
|
1352
|
-
FileUtils.mkdir_p dirname
|
|
1353
1598
|
shebang = if @options[:shebang]&.present?
|
|
1354
1599
|
"#{@options[:shebang]} #{@options[:shell]}\n"
|
|
1355
1600
|
else
|
|
1356
1601
|
''
|
|
1357
1602
|
end
|
|
1358
1603
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
return if @options[:saved_script_chmod].zero?
|
|
1604
|
+
content = shebang +
|
|
1605
|
+
"# file_name: #{opts[:filename]}\n" \
|
|
1606
|
+
"# block_name: #{opts[:block_name]}\n" \
|
|
1607
|
+
"# time: #{time_now}\n" \
|
|
1608
|
+
"#{required_lines.flatten.join("\n")}\n"
|
|
1365
1609
|
|
|
1366
|
-
|
|
1610
|
+
create_and_write_file_with_permissions(@options[:saved_filespec], content,
|
|
1611
|
+
@options[:saved_script_chmod])
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
def write_execution_output_to_file
|
|
1615
|
+
FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
|
|
1616
|
+
|
|
1617
|
+
ol = ["-STDOUT-\n"]
|
|
1618
|
+
ol += @execute_files&.fetch(EF_STDOUT, [])
|
|
1619
|
+
ol += ["\n-STDERR-\n"]
|
|
1620
|
+
ol += @execute_files&.fetch(EF_STDERR, [])
|
|
1621
|
+
ol += ["\n-STDIN-\n"]
|
|
1622
|
+
ol += @execute_files&.fetch(EF_STDIN, [])
|
|
1623
|
+
ol += ["\n"]
|
|
1624
|
+
File.write(@options[:logged_stdout_filespec], ol.join)
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
# Writes required code blocks to a temporary file and sets an environment variable with its path.
|
|
1628
|
+
#
|
|
1629
|
+
# @param block_name [String] The name of the block to collect code for.
|
|
1630
|
+
# @param opts [Hash] Additional options for collecting code.
|
|
1631
|
+
# @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
|
|
1632
|
+
def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
|
|
1633
|
+
code_blocks = (read_required_blocks_from_temp_file +
|
|
1634
|
+
mdoc.collect_recursively_required_code(
|
|
1635
|
+
block_name,
|
|
1636
|
+
opts: opts
|
|
1637
|
+
)[:code]).join("\n")
|
|
1638
|
+
|
|
1639
|
+
Dir::Tmpname.create(self.class.to_s) do |path|
|
|
1640
|
+
File.write(path, code_blocks)
|
|
1641
|
+
ENV['MDE_LINK_REQUIRED_FILE'] = path
|
|
1642
|
+
end
|
|
1367
1643
|
end
|
|
1368
1644
|
end # class MarkParse
|
|
1369
1645
|
end # module MarkdownExec
|
|
@@ -1388,7 +1664,8 @@ if $PROGRAM_NAME == __FILE__
|
|
|
1388
1664
|
c.expects(:command_execute).with(
|
|
1389
1665
|
obj,
|
|
1390
1666
|
'',
|
|
1391
|
-
args: pigeon
|
|
1667
|
+
args: pigeon
|
|
1668
|
+
)
|
|
1392
1669
|
|
|
1393
1670
|
# Call method execute_approved_block
|
|
1394
1671
|
c.execute_approved_block(obj, [])
|
|
@@ -1415,7 +1692,8 @@ if $PROGRAM_NAME == __FILE__
|
|
|
1415
1692
|
}
|
|
1416
1693
|
]
|
|
1417
1694
|
|
|
1418
|
-
# iterate over the input and output data and
|
|
1695
|
+
# iterate over the input and output data and
|
|
1696
|
+
# assert that the method sets the title as expected
|
|
1419
1697
|
input_output_data.each do |data|
|
|
1420
1698
|
input = data[:input]
|
|
1421
1699
|
output = data[:output]
|
|
@@ -1424,13 +1702,5 @@ if $PROGRAM_NAME == __FILE__
|
|
|
1424
1702
|
end
|
|
1425
1703
|
end
|
|
1426
1704
|
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
1705
|
end
|
|
1436
1706
|
end
|