markdown_exec 1.5 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
data/lib/markdown_exec.rb CHANGED
@@ -18,8 +18,11 @@ require_relative 'cached_nested_file_reader'
18
18
  require_relative 'cli'
19
19
  require_relative 'colorize'
20
20
  require_relative 'env'
21
+ require_relative 'exceptions'
21
22
  require_relative 'fcb'
22
23
  require_relative 'filter'
24
+ require_relative 'fout'
25
+ require_relative 'hash_delegator'
23
26
  require_relative 'markdown_exec/version'
24
27
  require_relative 'mdoc'
25
28
  require_relative 'option_value'
@@ -42,79 +45,20 @@ MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
42
45
  #
43
46
  class FileMissingError < StandardError; end
44
47
 
45
- # hash with keys sorted by name
46
- # add Hash.sym_keys
47
- #
48
- class Hash
49
- unless defined?(sort_by_key)
50
- def sort_by_key
51
- keys.sort.to_h { |key| [key, self[key]] }
52
- end
53
- end
54
-
55
- unless defined?(sym_keys)
56
- def sym_keys
57
- transform_keys(&:to_sym)
58
- end
59
- end
60
- end
61
-
62
- class LoadFile
63
- Load = true
64
- Reuse = false
65
- end
66
-
67
- class MenuState
68
- BACK = :back
69
- CONTINUE = :continue
70
- EXIT = :exit
71
- end
72
-
73
- # integer value for comparison
74
- #
75
- def options_fetch_display_level(options)
76
- options.fetch(:display_level, 1)
77
- end
78
-
79
- # integer value for comparison
80
- #
81
- def options_fetch_display_level_xbase_prefix(options)
82
- options.fetch(:level_xbase_prefix, '')
48
+ def dp(str)
49
+ lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
83
50
  end
84
51
 
85
- # stdout manager
86
- #
87
- module FOUT
88
- # standard output; not for debug
89
- #
90
- def fout(str)
91
- puts str
92
- end
93
-
94
- def fout_list(str)
95
- puts str
96
- end
97
-
98
- def fout_section(name, data)
99
- puts "# #{name}"
100
- puts data.to_yaml
101
- end
102
-
103
- def approved_fout?(level)
104
- level <= options_fetch_display_level(@options)
105
- end
106
-
107
- # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
108
- #
109
- def lout(str, level: DISPLAY_LEVEL_BASE)
110
- return unless approved_fout? level
111
-
112
- fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
113
- end
52
+ def rbp
53
+ rpry
54
+ pp(caller.take(4).map.with_index { |line, ind| " - #{ind}: #{line}" })
55
+ binding.pry
114
56
  end
115
57
 
116
- def dp(str)
117
- lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
58
+ def bpp(*args)
59
+ pp '+ bpp()'
60
+ pp(*args.map.with_index { |line, ind| " - #{ind}: #{line}" })
61
+ rbp
118
62
  end
119
63
 
120
64
  def rpry
@@ -124,6 +68,13 @@ end
124
68
 
125
69
  public
126
70
 
71
+ # convert regex match groups to a hash with symbol keys
72
+ #
73
+ # :reek:UtilityFunction
74
+ def extract_named_captures_from_option(str, option)
75
+ str.match(Regexp.new(option))&.named_captures&.sym_keys
76
+ end
77
+
127
78
  # :reek:UtilityFunction
128
79
  def list_recent_output(saved_stdout_folder, saved_stdout_glob,
129
80
  list_count)
@@ -138,61 +89,10 @@ def list_recent_scripts(saved_script_folder, saved_script_glob,
138
89
  saved_script_glob, list_count)
139
90
  end
140
91
 
141
- # convert regex match groups to a hash with symbol keys
142
- #
143
- # :reek:UtilityFunction
144
- def extract_named_captures_from_option(str, option)
145
- str.match(Regexp.new(option))&.named_captures&.sym_keys
146
- end
147
-
148
- module ArrayUtil
149
- def self.partition_by_predicate(arr)
150
- true_list = []
151
- false_list = []
152
-
153
- arr.each do |element|
154
- if yield(element)
155
- true_list << element
156
- else
157
- false_list << element
158
- end
159
- end
160
-
161
- [true_list, false_list]
162
- end
163
- end
164
-
165
- module StringUtil
166
- # Splits the given string on the first occurrence of the specified character.
167
- # Returns an array containing the portion of the string before the character and the rest of the string.
168
- #
169
- # @param input_str [String] The string to be split.
170
- # @param split_char [String] The character on which to split the string.
171
- # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
172
- def self.partition_at_first(input_str, split_char)
173
- split_index = input_str.index(split_char)
174
-
175
- if split_index.nil?
176
- [input_str, '']
177
- else
178
- [input_str[0...split_index], input_str[(split_index + 1)..-1]]
179
- end
180
- end
181
- end
182
-
183
92
  # execute markdown documents
184
93
  #
185
94
  module MarkdownExec
186
- # :reek:IrresponsibleModule
187
- FNR11 = '/'
188
- FNR12 = ',~'
189
-
190
- SHELL_COLOR_OPTIONS = {
191
- BlockType::BASH => :menu_bash_color,
192
- BlockType::LINK => :menu_link_color,
193
- BlockType::OPTS => :menu_opts_color,
194
- BlockType::VARS => :menu_vars_color
195
- }.freeze
95
+ include Exceptions
196
96
 
197
97
  ##
198
98
  #
@@ -203,124 +103,32 @@ module MarkdownExec
203
103
  # :reek:TooManyInstanceVariables ### temp
204
104
  # :reek:TooManyMethods ### temp
205
105
  class MarkParse
206
- attr_reader :options
106
+ attr_reader :options, :prompt, :run_state
207
107
 
208
108
  include ArrayUtil
209
109
  include StringUtil
210
- include FOUT
211
110
 
212
111
  def initialize(options = {})
213
- @execute_aborted_at = nil
214
- @execute_completed_at = nil
215
- @execute_error = nil
216
- @execute_error_message = nil
217
- @execute_files = nil
218
- @execute_options = nil
219
- @execute_script_filespec = nil
220
- @execute_started_at = nil
221
112
  @option_parser = nil
222
- @options = options
223
- @prompt = tty_prompt_without_disabled_symbol
224
- end
225
-
226
- # Adds Back and Exit options to the CLI menu
227
- #
228
- # @param blocks_in_file [Array] The current blocks in the menu
229
- def add_menu_chrome_blocks!(blocks_in_file)
230
- return unless @options[:menu_link_format].present?
231
113
 
232
- if @options[:menu_with_back] && history_state_exist?
233
- append_chrome_block(blocks_in_file, MenuState::BACK)
234
- end
235
- if @options[:menu_with_exit]
236
- append_chrome_block(blocks_in_file, MenuState::EXIT)
237
- end
238
- append_divider(blocks_in_file, @options, :initial)
239
- append_divider(blocks_in_file, @options, :final)
114
+ @options = HashDelegator.new(options)
115
+ @fout = FOut.new(@delegate_object)
240
116
  end
241
117
 
242
- ##
243
- # Appends a summary of a block (FCB) to the blocks array.
244
- #
245
- def append_block_summary(blocks, fcb, opts)
246
- ## enhance fcb with block summary
247
- #
248
- blocks.push get_block_summary(opts, fcb)
249
- end
250
-
251
- # Appends a chrome block, which is a menu option for Back or Exit
252
- #
253
- # @param blocks_in_file [Array] The current blocks in the menu
254
- # @param type [Symbol] The type of chrome block to add (:back or :exit)
255
- def append_chrome_block(blocks_in_file, type)
256
- case type
257
- when MenuState::BACK
258
- state = history_state_partition(@options)
259
- @hs_curr = state[:unit]
260
- @hs_rest = state[:rest]
261
- option_name = @options[:menu_option_back_name]
262
- insert_at_top = @options[:menu_back_at_top]
263
- when MenuState::EXIT
264
- option_name = @options[:menu_option_exit_name]
265
- insert_at_top = @options[:menu_exit_at_top]
266
- end
118
+ private
267
119
 
268
- formatted_name = format(@options[:menu_link_format],
269
- safeval(option_name))
270
- chrome_block = FCB.new(
271
- chrome: true,
272
- dname: formatted_name.send(@options[:menu_link_color].to_sym),
273
- oname: formatted_name
120
+ def error_handler(name = '', opts = {})
121
+ Exceptions.error_handler(
122
+ "CachedNestedFileReader.#{name} -- #{$!}",
123
+ opts
274
124
  )
275
-
276
- if insert_at_top
277
- blocks_in_file.unshift(chrome_block)
278
- else
279
- blocks_in_file.push(chrome_block)
280
- end
281
125
  end
282
126
 
283
- # Appends a divider to the blocks array.
284
- # @param blocks [Array] The array of block elements.
285
- # @param opts [Hash] Options containing divider configuration.
286
- # @param position [Symbol] :initial or :final divider position.
287
- def append_divider(blocks, opts, position)
288
- divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider
289
- unless opts[:menu_divider_format].present? && opts[divider_key].present?
290
- return
291
- end
292
-
293
- oname = format(opts[:menu_divider_format],
294
- safeval(opts[divider_key]))
295
- divider = FCB.new(
296
- chrome: true,
297
- disabled: '',
298
- dname: oname.send(opts[:menu_divider_color].to_sym),
299
- oname: oname
127
+ def warn_format(name, message, opts = {})
128
+ Exceptions.warn_format(
129
+ "CachedNestedFileReader.#{name} -- #{message}",
130
+ opts
300
131
  )
301
-
302
- position == :initial ? blocks.unshift(divider) : blocks.push(divider)
303
- end
304
-
305
- # Execute a code block after approval and provide user interaction options.
306
- #
307
- # This method displays required code blocks, asks for user approval, and
308
- # executes the code block if approved. It also allows users to copy the
309
- # code to the clipboard or save it to a file.
310
- #
311
- # @param opts [Hash] Options hash containing configuration settings.
312
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
313
- #
314
- def approve_and_execute_block(selected, opts, mdoc)
315
- if selected.fetch(:shell, '') == BlockType::LINK
316
- handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
317
- elsif opts.fetch(:s_back, false)
318
- handle_back_link(opts)
319
- elsif selected[:shell] == BlockType::OPTS
320
- handle_shell_opts(opts, selected)
321
- else
322
- handle_remainder_blocks(mdoc, opts, selected)
323
- end
324
132
  end
325
133
 
326
134
  # return arguments before `--`
@@ -355,240 +163,34 @@ module MarkdownExec
355
163
  end.compact.to_h
356
164
  end
357
165
 
358
- # Finds the first hash-like element within an enumerable collection where the specified key
359
- # matches the given value. Returns a default value if no match is found.
360
- #
361
- # @param blocks [Enumerable] An enumerable collection of hash-like objects.
362
- # @param key [Object] The key to look up in each hash-like object.
363
- # @param value [Object] The value to compare against the value associated with the key.
364
- # @param default [Object] The default value to return if no match is found (optional).
365
- # @return [Object, nil] The found hash-like object, or the default value if no match is found.
366
- def block_find(blocks, key, value, default = nil)
367
- blocks.find { |item| item[key] == value } || default
368
- end
369
-
370
- def blocks_per_opts(blocks, opts)
371
- return blocks if opts[:struct]
372
-
373
- blocks.map do |block|
374
- block.fetch(:text, nil) || block.oname
375
- end.compact.reject(&:empty?)
376
- end
377
-
378
166
  def calculated_options
379
167
  {
380
168
  bash: true, # bash block parsing in get_block_summary()
381
- saved_script_filename: nil, # calculated
382
- struct: true # allow get_block_summary()
169
+ saved_script_filename: nil # calculated
383
170
  }
384
171
  end
385
172
 
386
- # Check whether the document exists and is readable
387
- def check_file_existence(filename)
388
- unless filename&.present?
389
- fout 'No blocks found.'
390
- return false
391
- end
392
-
393
- unless File.exist? filename
394
- fout 'Document is missing.'
395
- return false
396
- end
397
- true
398
- end
399
-
400
173
  def clear_required_file
401
174
  ENV['MDE_LINK_REQUIRED_FILE'] = ''
402
175
  end
403
176
 
404
- # Collect required code blocks based on the provided options.
405
- #
406
- # @param opts [Hash] Options hash containing configuration settings.
407
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
408
- # @return [Array<String>] Required code blocks as an array of lines.
409
- def collect_required_code_lines(mdoc, selected, opts: {})
410
- # Apply hash in opts block to environment variables
411
- if selected[:shell] == BlockType::VARS
412
- data = YAML.load(selected[:body].join("\n"))
413
- data.each_key do |key|
414
- ENV[key] = value = data[key].to_s
415
- next unless opts[:menu_vars_set_format].present?
416
-
417
- print format(
418
- opts[:menu_vars_set_format],
419
- { key: key,
420
- value: value }
421
- ).send(opts[:menu_vars_set_color].to_sym)
422
- end
423
- end
424
-
425
- required = mdoc.collect_recursively_required_code(opts[:block_name],
426
- opts: opts)
427
- read_required_blocks_from_temp_file + required[:code]
428
- end
429
-
430
- def cfile
431
- @cfile ||= CachedNestedFileReader.new(
432
- import_pattern: @options.fetch(:import_pattern)
433
- )
434
- end
435
-
436
- EF_STDOUT = :stdout
437
- EF_STDERR = :stderr
438
- EF_STDIN = :stdin
439
-
440
- # Existing command_execute method
441
- def command_execute(opts, command, args: [])
442
- @execute_files = Hash.new([])
443
- @execute_options = opts
444
- @execute_started_at = Time.now.utc
445
-
446
- Open3.popen3(opts[:shell], '-c', command, opts[:filename],
447
- *args) do |stdin, stdout, stderr, exec_thr|
448
- handle_stream(opts, stdout, EF_STDOUT) do |line|
449
- yield nil, line, nil, exec_thr if block_given?
450
- end
451
- handle_stream(opts, stderr, EF_STDERR) do |line|
452
- yield nil, nil, line, exec_thr if block_given?
453
- end
454
-
455
- in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
456
- stdin.puts(line)
457
- yield line, nil, nil, exec_thr if block_given?
458
- end
459
-
460
- exec_thr.join
461
- sleep 0.1
462
- in_thr.kill if in_thr&.alive?
463
- end
464
-
465
- @execute_completed_at = Time.now.utc
466
- rescue Errno::ENOENT => err
467
- #d 'command error ENOENT triggered by missing command in script'
468
- @execute_aborted_at = Time.now.utc
469
- @execute_error_message = err.message
470
- @execute_error = err
471
- @execute_files[EF_STDERR] += [@execute_error_message]
472
- fout "Error ENOENT: #{err.inspect}"
473
- rescue SignalException => err
474
- #d 'command SIGTERM triggered by user or system'
475
- @execute_aborted_at = Time.now.utc
476
- @execute_error_message = 'SIGTERM'
477
- @execute_error = err
478
- @execute_files[EF_STDERR] += [@execute_error_message]
479
- fout "Error ENOENT: #{err.inspect}"
480
- end
481
-
482
- def command_or_user_selected_block(blocks_in_file, blocks_menu, default,
483
- opts)
484
- if opts[:block_name].present?
485
- block = blocks_in_file.find do |item|
486
- item[:oname] == opts[:block_name]
487
- end
488
- else
489
- block, state = wait_for_user_selected_block(blocks_in_file, blocks_menu, default,
490
- opts)
491
- end
492
-
493
- [block, state]
494
- end
495
-
496
- def copy_to_clipboard(required_lines)
497
- text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
498
- Clipboard.copy(text)
499
- fout "Clipboard updated: #{required_lines.count} blocks," \
500
- " #{required_lines.flatten.count} lines," \
501
- " #{text.length} characters"
502
- end
503
-
504
- def count_blocks_in_filename
505
- fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
506
- cnt = 0
507
- cfile.readlines(@options[:filename]).each do |line|
508
- cnt += 1 if line.match(fenced_start_and_end_regex)
509
- end
510
- cnt / 2
511
- end
512
-
513
- ##
514
- # Creates and adds a formatted block to the blocks array based on the provided match and format options.
515
- # @param blocks [Array] The array of blocks to add the new block to.
516
- # @param fcb [FCB] The file control block containing the line to match against.
517
- # @param match_data [MatchData] The match data containing named captures for formatting.
518
- # @param format_option [String] The format string to be used for the new block.
519
- # @param color_method [Symbol] The color method to apply to the block's display name.
520
- def create_and_add_chrome_block(blocks, _fcb, match_data, format_option,
521
- color_method)
522
- oname = format(format_option,
523
- match_data.named_captures.transform_keys(&:to_sym))
524
- blocks.push FCB.new(
525
- chrome: true,
526
- disabled: '',
527
- dname: oname.send(color_method),
528
- oname: oname
529
- )
530
- end
531
-
532
- ##
533
- # Processes lines within the file and converts them into blocks if they match certain criteria.
534
- # @param blocks [Array] The array to append new blocks to.
535
- # @param fcb [FCB] The file control block being processed.
536
- # @param opts [Hash] Options containing configuration for line processing.
537
- # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
538
- def create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
539
- return unless use_chrome
540
-
541
- if opts[:menu_note_match].present? && (mbody = fcb.body[0].match opts[:menu_note_match])
542
- create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_note_format],
543
- opts[:menu_note_color].to_sym)
544
- elsif opts[:menu_divider_match].present? && (mbody = fcb.body[0].match opts[:menu_divider_match])
545
- create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_divider_format],
546
- opts[:menu_divider_color].to_sym)
547
- elsif opts[:menu_task_match].present? && (mbody = fcb.body[0].match opts[:menu_task_match])
548
- create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_task_format],
549
- opts[:menu_task_color].to_sym)
550
- end
551
- end
552
-
553
- def create_and_write_file_with_permissions(file_path, content,
554
- chmod_value)
555
- dirname = File.dirname(file_path)
556
- FileUtils.mkdir_p dirname
557
- File.write(file_path, content)
558
- return if chmod_value.zero?
559
-
560
- File.chmod chmod_value, file_path
561
- end
562
-
563
- # Deletes a required temporary file specified by an environment variable.
564
- # The function checks if the file exists before attempting to delete it.
565
- # Clears the environment variable after deletion.
566
- #
567
- def delete_required_temp_file
568
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
177
+ # # Deletes a required temporary file specified by an environment variable.
178
+ # # The function checks if the file exists before attempting to delete it.
179
+ # # Clears the environment variable after deletion.
180
+ # #
181
+ # def delete_required_temp_file
182
+ # temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
569
183
 
570
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
571
- return
572
- end
184
+ # return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
573
185
 
574
- FileUtils.rm_f(temp_blocks_file_path)
186
+ # FileUtils.rm_f(temp_blocks_file_path)
575
187
 
576
- clear_required_file
577
- end
188
+ # clear_required_file
189
+ # rescue StandardError
190
+ # error_handler('delete_required_temp_file')
191
+ # end
578
192
 
579
- # Derives a title from the body of an FCB object.
580
- # @param fcb [Object] The FCB object whose title is to be derived.
581
- # @return [String] The derived title.
582
- def derive_title_from_body(fcb)
583
- body_content = fcb&.body
584
- return '' unless body_content
585
-
586
- if body_content.count == 1
587
- body_content.first
588
- else
589
- format_multiline_body_as_title(body_content)
590
- end
591
- end
193
+ public
592
194
 
593
195
  ## Determines the correct filename to use for searching files
594
196
  #
@@ -597,8 +199,7 @@ module MarkdownExec
597
199
  if specified_filename&.present?
598
200
  return specified_filename if specified_filename.start_with?('/')
599
201
 
600
- File.join(specified_folder || default_folder,
601
- specified_filename)
202
+ File.join(specified_folder || default_folder, specified_filename)
602
203
  elsif specified_folder&.present?
603
204
  File.join(specified_folder,
604
205
  filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
@@ -607,31 +208,19 @@ module MarkdownExec
607
208
  end
608
209
  end
609
210
 
610
- # :reek:DuplicateMethodCall
611
- def display_required_code(opts, required_lines)
612
- frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
613
- fout frame
614
- required_lines.each { |cb| fout cb }
615
- fout frame
616
- end
211
+ private
617
212
 
618
- def execute_approved_block(opts, required_lines)
619
- write_command_file(opts, required_lines)
620
- command_execute(
621
- opts,
622
- required_lines.flatten.join("\n"),
623
- args: opts.fetch(:s_pass_args, [])
624
- )
625
- initialize_and_save_execution_output
626
- output_execution_summary
627
- output_execution_result
628
- end
213
+ # def error_handler(name = '', event = nil, backtrace = nil)
214
+ # warn(error = "\n * ERROR * #{name}; #{$!.inspect}")
215
+ # warn($@.take(4).map.with_index { |line, ind| " * #{ind}: #{line}" })
216
+ # binding.pry if $tap_enable
217
+ # raise ArgumentError, error
218
+ # end
629
219
 
630
220
  # Reports and executes block logic
631
221
  def execute_block_logic(files)
632
222
  @options[:filename] = select_document_if_multiple(files)
633
- select_approve_and_execute_block({ bash: true,
634
- struct: true })
223
+ @options.select_approve_and_execute_block
635
224
  end
636
225
 
637
226
  ## Executes the block specified in the options
@@ -640,12 +229,10 @@ module MarkdownExec
640
229
  finalize_cli_argument_processing(rest)
641
230
  @options[:s_cli_rest] = rest
642
231
  execute_code_block_based_on_options(@options)
643
- rescue FileMissingError => err
644
- puts "File missing: #{err}"
645
- rescue StandardError => err
646
- warn(error = "ERROR ** MarkParse.execute_block_with_error_handling(); #{err.inspect}")
647
- binding.pry if $tap_enable
648
- raise ArgumentError, error
232
+ rescue FileMissingError
233
+ warn "File missing: #{$!}"
234
+ rescue StandardError
235
+ error_handler('execute_block_with_error_handling')
649
236
  end
650
237
 
651
238
  # Main method to execute a block based on options and block_name
@@ -654,47 +241,40 @@ module MarkdownExec
654
241
  update_options(options, over: false)
655
242
 
656
243
  simple_commands = {
657
- doc_glob: -> { fout options[:md_filename_glob] },
658
- list_blocks: lambda do
659
- fout_list (files.map do |file|
660
- menu_with_block_labels(filename: file,
661
- struct: true)
662
- end).flatten(1)
663
- end,
664
- list_default_yaml: -> { fout_list list_default_yaml },
665
- list_docs: -> { fout_list files },
666
- list_default_env: -> { fout_list list_default_env },
244
+ doc_glob: -> { @fout.fout options[:md_filename_glob] },
245
+ list_default_yaml: -> { @fout.fout_list list_default_yaml },
246
+ list_docs: -> { @fout.fout_list files },
247
+ list_default_env: -> { @fout.fout_list list_default_env },
667
248
  list_recent_output: lambda {
668
- fout_list list_recent_output(
249
+ @fout.fout_list list_recent_output(
669
250
  @options[:saved_stdout_folder],
670
251
  @options[:saved_stdout_glob], @options[:list_count]
671
252
  )
672
253
  },
673
254
  list_recent_scripts: lambda {
674
- fout_list list_recent_scripts(
255
+ @fout.fout_list list_recent_scripts(
675
256
  options[:saved_script_folder],
676
257
  options[:saved_script_glob], options[:list_count]
677
258
  )
678
259
  },
679
- pwd: -> { fout File.expand_path('..', __dir__) },
260
+ pwd: -> { @fout.fout File.expand_path('..', __dir__) },
680
261
  run_last_script: -> { run_last_script },
681
262
  select_recent_output: -> { select_recent_output },
682
263
  select_recent_script: -> { select_recent_script },
683
- tab_completions: -> { fout tab_completions },
684
- menu_export: -> { fout menu_export }
264
+ tab_completions: -> { @fout.fout tab_completions },
265
+ menu_export: -> { @fout.fout menu_export }
685
266
  }
686
267
 
687
268
  return if execute_simple_commands(simple_commands)
688
269
 
689
- files = prepare_file_list(options)
270
+ files = opts_prepare_file_list(options)
690
271
  execute_block_logic(files)
691
272
  return unless @options[:output_saved_script_filename]
692
273
 
693
- fout "saved_filespec: #{@execute_script_filespec}"
694
- rescue StandardError => err
695
- warn(error = "ERROR ** MarkParse.execute_code_block_based_on_options(); #{err.inspect}")
696
- binding.pry if $tap_enable
697
- raise ArgumentError, error
274
+ @fout.fout "script_block_name: #{@options.run_state.script_block_name}"
275
+ @fout.fout "s_save_filespec: #{@options.run_state.saved_filespec}"
276
+ rescue StandardError
277
+ error_handler('execute_code_block_based_on_options')
698
278
  end
699
279
 
700
280
  # Executes command based on the provided option keys
@@ -727,176 +307,22 @@ module MarkdownExec
727
307
  #
728
308
  block_name = rest.shift
729
309
  @options[:block_name] = block_name if block_name.present?
730
- end
731
-
732
- # Formats multiline body content as a title string.
733
- # @param body_lines [Array<String>] The lines of body content.
734
- # @return [String] Formatted title.
735
- def format_multiline_body_as_title(body_lines)
736
- body_lines.map.with_index do |line, index|
737
- index.zero? ? line : " #{line}"
738
- end.join("\n") << "\n"
739
- end
740
-
741
- ## summarize blocks
742
- #
743
- def get_block_summary(call_options, fcb)
744
- opts = optsmerge call_options
745
- # return fcb.body unless opts[:struct]
746
- return fcb unless opts[:bash]
747
-
748
- fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
749
- titlexcall = if fcb.call
750
- fcb.title.sub("%#{fcb.call}", '')
751
- else
752
- fcb.title
753
- end
754
- bm = extract_named_captures_from_option(titlexcall,
755
- opts[:block_name_match])
756
- fcb.stdin = extract_named_captures_from_option(titlexcall,
757
- opts[:block_stdin_scan])
758
- fcb.stdout = extract_named_captures_from_option(titlexcall,
759
- opts[:block_stdout_scan])
760
-
761
- shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
762
- fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
763
- fcb.dname = if shell_color_option && opts[shell_color_option].present?
764
- fcb.oname.send(opts[shell_color_option].to_sym)
765
- else
766
- fcb.oname
767
- end
768
- fcb
769
- end
770
-
771
- # Handles the link-back operation.
772
- #
773
- # @param opts [Hash] Configuration options hash.
774
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
775
- def handle_back_link(opts)
776
- history_state_pop(opts)
777
- [LoadFile::Load, '']
778
- end
779
-
780
- # Handles the execution and display of remainder blocks from a selected menu item.
781
- #
782
- # @param mdoc [Object] Document object containing code blocks.
783
- # @param opts [Hash] Configuration options hash.
784
- # @param selected [Hash] Selected item from the menu.
785
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
786
- # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
787
- def handle_remainder_blocks(mdoc, opts, selected)
788
- required_lines = collect_required_code_lines(mdoc, selected,
789
- opts: opts)
790
- if opts[:output_script] || opts[:user_must_approve]
791
- display_required_code(opts, required_lines)
792
- end
793
- allow = if opts[:user_must_approve]
794
- prompt_for_user_approval(opts,
795
- required_lines)
796
- else
797
- true
798
- end
799
- opts[:s_ir_approve] = allow
800
- if opts[:s_ir_approve]
801
- execute_approved_block(opts,
802
- required_lines)
803
- end
804
-
805
- [LoadFile::Reuse, '']
806
- end
807
-
808
- # Handles the link-shell operation.
809
- #
810
- # @param opts [Hash] Configuration options hash.
811
- # @param body [Array<String>] The body content.
812
- # @param mdoc [Object] Document object containing code blocks.
813
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
814
- def handle_shell_link(opts, body, mdoc)
815
- data = body.present? ? YAML.load(body.join("\n")) : {}
816
- data_file = data.fetch('file', nil)
817
- return [LoadFile::Reuse, ''] unless data_file
818
-
819
- history_state_push(mdoc, data_file, opts)
820
-
821
- data.fetch('vars', []).each do |var|
822
- ENV[var[0]] = var[1].to_s
823
- end
824
-
825
- [LoadFile::Load, data.fetch('block', '')]
826
- end
827
-
828
- # Handles options for the shell.
829
- #
830
- # @param opts [Hash] Configuration options hash.
831
- # @param selected [Hash] Selected item from the menu.
832
- # @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
833
- def handle_shell_opts(opts, selected, tgt2 = nil)
834
- data = YAML.load(selected[:body].join("\n"))
835
- data.each_key do |key|
836
- opts[key.to_sym] = value = data[key]
837
- tgt2[key.to_sym] = value if tgt2
838
- next unless opts[:menu_opts_set_format].present?
839
-
840
- print format(
841
- opts[:menu_opts_set_format],
842
- { key: key,
843
- value: value }
844
- ).send(opts[:menu_opts_set_color].to_sym)
845
- end
846
- [LoadFile::Reuse, '']
847
- end
848
-
849
- # Handles reading and processing lines from a given IO stream
850
- #
851
- # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
852
- # @param file_type [Symbol] The type of file to which the stream corresponds.
853
- def handle_stream(opts, stream, file_type, swap: false)
854
- Thread.new do
855
- until (line = stream.gets).nil?
856
- @execute_files[file_type] =
857
- @execute_files[file_type] + [line.strip]
858
- print line if opts[:output_stdout]
859
- yield line if block_given?
860
- end
861
- rescue IOError
862
- #d 'stdout IOError, thread killed, do nothing'
863
- end
864
- end
865
-
866
- def history_state_exist?
867
- history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
868
- history.present? ? history : nil
869
- end
870
-
871
- def history_state_partition(opts)
872
- unit, rest = StringUtil.partition_at_first(
873
- ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
874
- opts[:history_document_separator]
875
- )
876
- { unit: unit, rest: rest }.tap_inspect
877
- end
878
-
879
- def history_state_pop(opts)
880
- state = history_state_partition(opts)
881
- opts[:filename] = state[:unit]
882
- ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
883
- delete_required_temp_file
884
- end
885
-
886
- def history_state_push(mdoc, data_file, opts)
887
- [data_file, opts[:block_name]].tap_inspect 'filename, blockname'
888
- new_history = opts[:filename] +
889
- opts[:history_document_separator] +
890
- ENV.fetch(MDE_HISTORY_ENV_NAME, '')
891
- opts[:filename] = data_file
892
- write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
893
- ENV[MDE_HISTORY_ENV_NAME] = new_history
310
+ rescue FileMissingError
311
+ warn_format('finalize_cli_argument_processing',
312
+ "File missing -- #{$!}", { abort: true })
313
+ # @options[:block_name] = ''
314
+ # @options[:filename] = ''
315
+ # exit 1
316
+ rescue StandardError
317
+ error_handler('finalize_cli_argument_processing')
894
318
  end
895
319
 
896
320
  ## Sets up the options and returns the parsed arguments
897
321
  #
898
322
  def initialize_and_parse_cli_options
899
- @options = base_options
323
+ # @options = base_options
324
+ @options = HashDelegator.new(base_options)
325
+
900
326
  read_configuration_file!(@options,
901
327
  ".#{MarkdownExec::APP_NAME.downcase}.yml")
902
328
 
@@ -909,7 +335,7 @@ module MarkdownExec
909
335
  ].join("\n")
910
336
 
911
337
  menu_iter do |item|
912
- menu_option_append opts, @options, item
338
+ opts_menu_option_append opts, @options, item
913
339
  end
914
340
  end
915
341
  @option_parser.load
@@ -917,55 +343,11 @@ module MarkdownExec
917
343
 
918
344
  rest = @option_parser.parse!(arguments_for_mde)
919
345
  @options[:s_pass_args] = ARGV[rest.count + 1..]
346
+ @options.merge(@options.run_state.to_h)
920
347
 
921
348
  rest
922
349
  end
923
350
 
924
- def initialize_and_save_execution_output
925
- return unless @options[:save_execution_output]
926
-
927
- @options[:logged_stdout_filename] =
928
- SavedAsset.stdout_name(blockname: @options[:block_name],
929
- filename: File.basename(@options[:filename],
930
- '.*'),
931
- prefix: @options[:logged_stdout_filename_prefix],
932
- time: Time.now.utc)
933
-
934
- @logged_stdout_filespec =
935
- @options[:logged_stdout_filespec] =
936
- File.join @options[:saved_stdout_folder],
937
- @options[:logged_stdout_filename]
938
- @logged_stdout_filespec = @options[:logged_stdout_filespec]
939
- write_execution_output_to_file
940
- end
941
-
942
- # Initializes variables for regex and other states
943
- def initialize_state(opts)
944
- {
945
- fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
946
- fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
947
- fcb: FCB.new,
948
- in_fenced_block: false,
949
- headings: []
950
- }
951
- end
952
-
953
- # Main function to iterate through blocks in file
954
- def iter_blocks_in_file(opts = {}, &block)
955
- return unless check_file_existence(opts[:filename])
956
-
957
- state = initialize_state(opts)
958
-
959
- selected_messages = yield :filter
960
-
961
- cfile.readlines(opts[:filename]).each do |line|
962
- next unless line
963
-
964
- update_line_and_block_state(line, state, opts, selected_messages,
965
- &block)
966
- end
967
- end
968
-
969
351
  ##
970
352
  # Returns a lambda expression based on the given procname.
971
353
  # @param procname [String] The name of the process to generate a lambda for.
@@ -981,7 +363,7 @@ module MarkdownExec
981
363
  ->(_) { exit }
982
364
  when 'help'
983
365
  lambda { |_|
984
- fout menu_help
366
+ @fout.fout menu_help
985
367
  exit
986
368
  }
987
369
  when 'path'
@@ -989,7 +371,7 @@ module MarkdownExec
989
371
  when 'show_config'
990
372
  lambda { |_|
991
373
  finalize_cli_argument_processing(options)
992
- fout options.sort_by_key.to_yaml
374
+ @fout.fout options.sort_by_key.to_yaml
993
375
  }
994
376
  when 'val_as_bool'
995
377
  lambda { |value|
@@ -1001,7 +383,7 @@ module MarkdownExec
1001
383
  ->(value) { value.to_s }
1002
384
  when 'version'
1003
385
  lambda { |_|
1004
- fout MarkdownExec::VERSION
386
+ @fout.fout MarkdownExec::VERSION
1005
387
  exit
1006
388
  }
1007
389
  else
@@ -1031,16 +413,7 @@ module MarkdownExec
1031
413
  end.compact.sort
1032
414
  end
1033
415
 
1034
- def list_files_per_options(options)
1035
- list_files_specified(
1036
- determine_filename(
1037
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
1038
- specified_folder: options[:path],
1039
- default_filename: 'README.md',
1040
- default_folder: '.'
1041
- )
1042
- )
1043
- end
416
+ public
1044
417
 
1045
418
  ## Searches for files based on the specified or default filenames and folders
1046
419
  #
@@ -1057,105 +430,7 @@ module MarkdownExec
1057
430
  @options[:md_filename_glob]))
1058
431
  end
1059
432
 
1060
- ## output type (body string or full object) per option struct and bash
1061
- #
1062
- def list_named_blocks_in_file(call_options = {}, &options_block)
1063
- opts = optsmerge call_options, options_block
1064
- blocks_per_opts(
1065
- menu_from_file(opts.merge(struct: true)).select do |fcb|
1066
- Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
1067
- end, opts
1068
- )
1069
- end
1070
-
1071
- # return true if values were modified
1072
- # execute block once per filename
1073
- #
1074
- def load_auto_blocks(opts, blocks_in_file)
1075
- return unless opts[:document_load_opts_block_name].present?
1076
- return if opts[:s_most_recent_filename] == opts[:filename]
1077
-
1078
- block = block_find(blocks_in_file, :oname,
1079
- opts[:document_load_opts_block_name])
1080
- return unless block
1081
-
1082
- handle_shell_opts(opts, block, @options)
1083
- opts[:s_most_recent_filename] = opts[:filename]
1084
- true
1085
- end
1086
-
1087
- def mdoc_and_menu_from_file(opts)
1088
- menu_blocks = menu_from_file(opts.merge(struct: true))
1089
- mdoc = MDoc.new(menu_blocks) do |nopts|
1090
- opts.merge!(nopts)
1091
- end
1092
- [menu_blocks, mdoc]
1093
- end
1094
-
1095
- ## Handles the file loading and returns the blocks in the file and MDoc instance
1096
- #
1097
- def mdoc_menu_and_selected_from_file(opts)
1098
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1099
- if load_auto_blocks(opts, blocks_in_file)
1100
- # recreate menu with new options
1101
- #
1102
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1103
- end
1104
-
1105
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1106
- add_menu_chrome_blocks!(blocks_menu)
1107
- [blocks_in_file, blocks_menu, mdoc]
1108
- end
1109
-
1110
- def menu_chrome_colored_option(opts,
1111
- option_symbol = :menu_option_back_name)
1112
- if opts[:menu_chrome_color]
1113
- menu_chrome_formatted_option(opts,
1114
- option_symbol).send(opts[:menu_chrome_color].to_sym)
1115
- else
1116
- menu_chrome_formatted_option(opts, option_symbol)
1117
- end
1118
- end
1119
-
1120
- def menu_chrome_formatted_option(opts,
1121
- option_symbol = :menu_option_back_name)
1122
- val1 = safeval(opts.fetch(option_symbol, ''))
1123
- val1 unless opts[:menu_chrome_format]
1124
-
1125
- format(opts[:menu_chrome_format], val1)
1126
- end
1127
-
1128
- def menu_export(data = menu_for_optparse)
1129
- data.map do |item|
1130
- item.delete(:procname)
1131
- item
1132
- end.to_yaml
1133
- end
1134
-
1135
- def menu_for_blocks(menu_options)
1136
- options = calculated_options.merge menu_options
1137
- menu = []
1138
- iter_blocks_in_file(options) do |btype, fcb|
1139
- case btype
1140
- when :filter
1141
- %i[blocks line]
1142
- when :line
1143
- if options[:menu_divider_match] &&
1144
- (mbody = fcb.body[0].match(options[:menu_divider_match]))
1145
- menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name],
1146
- disabled: '' })
1147
- end
1148
- if options[:menu_note_match] &&
1149
- (mbody = fcb.body[0].match(options[:menu_note_match]))
1150
- menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name],
1151
- disabled: '' })
1152
- end
1153
- when :blocks
1154
- menu += [fcb.oname]
1155
- end
1156
- end
1157
- menu
1158
- end
433
+ private
1159
434
 
1160
435
  ##
1161
436
  # Generates a menu suitable for OptionParser from the menu items defined in YAML format.
@@ -1169,37 +444,6 @@ module MarkdownExec
1169
444
  end
1170
445
  end
1171
446
 
1172
- ##
1173
- # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
1174
- # The list can be customized via call_options and options_block.
1175
- #
1176
- # @param call_options [Hash] Options passed as an argument.
1177
- # @param options_block [Proc] Block for dynamic option manipulation.
1178
- # @return [Array<FCB>] An array of FCB objects representing the blocks.
1179
- #
1180
- def menu_from_file(call_options = {},
1181
- &options_block)
1182
- opts = optsmerge(call_options, options_block)
1183
- use_chrome = !opts[:no_chrome]
1184
-
1185
- blocks = []
1186
- iter_blocks_in_file(opts) do |btype, fcb|
1187
- case btype
1188
- when :blocks
1189
- append_block_summary(blocks, fcb, opts)
1190
- when :filter # what btypes are responded to?
1191
- %i[blocks line]
1192
- when :line
1193
- create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
1194
- end
1195
- end
1196
- blocks
1197
- rescue StandardError => err
1198
- warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
1199
- warn(caller[0..4])
1200
- raise StandardError, error
1201
- end
1202
-
1203
447
  def menu_help
1204
448
  @option_parser.help
1205
449
  end
@@ -1208,10 +452,26 @@ module MarkdownExec
1208
452
  data.map(&block)
1209
453
  end
1210
454
 
1211
- def menu_option_append(opts, options, item)
1212
- unless item[:long_name].present? || item[:short_name].present?
1213
- return
1214
- end
455
+ def opts_list_files(options)
456
+ list_files_specified(
457
+ determine_filename(
458
+ specified_filename: options[:filename]&.present? ? options[:filename] : nil,
459
+ specified_folder: options[:path],
460
+ default_filename: 'README.md',
461
+ default_folder: '.'
462
+ )
463
+ )
464
+ end
465
+
466
+ def menu_export(data = menu_for_optparse)
467
+ data.map do |item|
468
+ item.delete(:procname)
469
+ item
470
+ end.to_yaml
471
+ end
472
+
473
+ def opts_menu_option_append(opts, options, item)
474
+ return unless item[:long_name].present? || item[:short_name].present?
1215
475
 
1216
476
  opts.on(*[
1217
477
  # - long name
@@ -1239,188 +499,9 @@ module MarkdownExec
1239
499
  ].compact)
1240
500
  end
1241
501
 
1242
- def menu_with_block_labels(call_options = {})
1243
- opts = options.merge(call_options)
1244
- menu_from_file(opts).map do |fcb|
1245
- BlockLabel.make(
1246
- filename: opts[:filename],
1247
- headings: fcb.fetch(:headings, []),
1248
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1249
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1250
- title: fcb[:title],
1251
- text: fcb[:text],
1252
- body: fcb[:body]
1253
- )
1254
- end.compact
1255
- end
1256
-
1257
- def next_block_name_from_command_line_arguments(opts)
1258
- if opts[:s_cli_rest].present?
1259
- opts[:block_name] = opts[:s_cli_rest].pop
1260
- false # repeat_menu
1261
- else
1262
- true # repeat_menu
1263
- end
1264
- end
1265
-
1266
- # :reek:ControlParameter
1267
- def optsmerge(call_options = {}, options_block = nil)
1268
- class_call_options = @options.merge(call_options || {})
1269
- if options_block
1270
- options_block.call class_call_options
1271
- else
1272
- class_call_options
1273
- end
1274
- end
1275
-
1276
- def output_execution_result
1277
- oq = [['Block', @options[:block_name], DISPLAY_LEVEL_ADMIN],
1278
- ['Command',
1279
- [MarkdownExec::BIN_NAME,
1280
- @options[:filename],
1281
- @options[:block_name]].join(' '),
1282
- DISPLAY_LEVEL_ADMIN]]
1283
-
1284
- [['Script', :saved_filespec],
1285
- ['StdOut', :logged_stdout_filespec]].each do |label, name|
1286
- if @options[name]
1287
- oq << [label, @options[name],
1288
- DISPLAY_LEVEL_ADMIN]
1289
- end
1290
- end
1291
-
1292
- oq.map do |label, value, level|
1293
- lout ["#{label}:".yellow, value.to_s].join(' '), level: level
1294
- end
1295
- end
1296
-
1297
- def output_execution_summary
1298
- return unless @options[:output_execution_summary]
1299
-
1300
- fout_section 'summary', {
1301
- execute_aborted_at: @execute_aborted_at,
1302
- execute_completed_at: @execute_completed_at,
1303
- execute_error: @execute_error,
1304
- execute_error_message: @execute_error_message,
1305
- execute_files: @execute_files,
1306
- execute_options: @execute_options,
1307
- execute_started_at: @execute_started_at,
1308
- execute_script_filespec: @execute_script_filespec
1309
- }
1310
- end
1311
-
1312
- # Prepare the blocks menu by adding labels and other necessary details.
1313
- #
1314
- # @param blocks_in_file [Array<Hash>] The list of blocks from the file.
1315
- # @param opts [Hash] The options hash.
1316
- # @return [Array<Hash>] The updated blocks menu.
1317
- def prepare_blocks_menu(blocks_in_file, opts)
1318
- # next if fcb.fetch(:disabled, false)
1319
- # next unless fcb.fetch(:name, '').present?
1320
- replace_consecutive_blanks(blocks_in_file).map do |fcb|
1321
- next if Filter.prepared_not_in_menu?(opts, fcb)
1322
-
1323
- fcb.merge!(
1324
- name: fcb.dname,
1325
- label: BlockLabel.make(
1326
- body: fcb[:body],
1327
- filename: opts[:filename],
1328
- headings: fcb.fetch(:headings, []),
1329
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1330
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1331
- text: fcb[:text],
1332
- title: fcb[:title]
1333
- )
1334
- )
1335
- fcb.to_h
1336
- end.compact
1337
- end
1338
-
1339
502
  # Prepares and fetches file listings
1340
- def prepare_file_list(options)
1341
- list_files_per_options(options)
1342
- end
1343
-
1344
- def process_fenced_block(fcb, opts, selected_messages, &block)
1345
- fcb.oname = fcb.dname = fcb.title || ''
1346
- return unless fcb.body
1347
-
1348
- update_title_from_body(fcb)
1349
-
1350
- if block &&
1351
- selected_messages.include?(:blocks) &&
1352
- Filter.fcb_select?(opts, fcb)
1353
- block.call :blocks, fcb
1354
- end
1355
- end
1356
-
1357
- def process_line(line, _opts, selected_messages, &block)
1358
- return unless block && selected_messages.include?(:line)
1359
-
1360
- # dp 'text outside of fcb'
1361
- fcb = FCB.new
1362
- fcb.body = [line]
1363
- block.call(:line, fcb)
1364
- end
1365
-
1366
- class MenuOptions
1367
- YES = 1
1368
- NO = 2
1369
- SCRIPT_TO_CLIPBOARD = 3
1370
- SAVE_SCRIPT = 4
1371
- end
1372
-
1373
- ##
1374
- # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1375
- # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
1376
- #
1377
- # @param opts [Hash] A hash containing various options for the menu.
1378
- # @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
1379
- #
1380
- # @option opts [String] :prompt_approve_block Prompt text for the approval menu.
1381
- # @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
1382
- # @option opts [String] :prompt_no Text for the 'No' choice in the menu.
1383
- # @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
1384
- # @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
1385
- #
1386
- # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
1387
- ##
1388
- def prompt_for_user_approval(opts, required_lines)
1389
- # Present a selection menu for user approval.
1390
-
1391
- sel = @prompt.select(opts[:prompt_approve_block],
1392
- filter: true) do |menu|
1393
- menu.default MenuOptions::YES
1394
- menu.choice opts[:prompt_yes], MenuOptions::YES
1395
- menu.choice opts[:prompt_no], MenuOptions::NO
1396
- menu.choice opts[:prompt_script_to_clipboard],
1397
- MenuOptions::SCRIPT_TO_CLIPBOARD
1398
- menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
1399
- end
1400
-
1401
- if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1402
- copy_to_clipboard(required_lines)
1403
- elsif sel == MenuOptions::SAVE_SCRIPT
1404
- save_to_file(opts, required_lines)
1405
- end
1406
-
1407
- sel == MenuOptions::YES
1408
- rescue TTY::Reader::InputInterrupt
1409
- exit 1
1410
- end
1411
-
1412
- def prompt_select_continue(opts)
1413
- sel = @prompt.select(
1414
- opts[:prompt_after_bash_exec],
1415
- filter: true,
1416
- quiet: true
1417
- ) do |menu|
1418
- menu.choice opts[:prompt_yes]
1419
- menu.choice opts[:prompt_exit]
1420
- end
1421
- sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1422
- rescue TTY::Reader::InputInterrupt
1423
- exit 1
503
+ def opts_prepare_file_list(options)
504
+ opts_list_files(options)
1424
505
  end
1425
506
 
1426
507
  # :reek:UtilityFunction ### temp
@@ -1431,53 +512,18 @@ module MarkdownExec
1431
512
  .transform_keys(&:to_sym))
1432
513
  end
1433
514
 
1434
- # Reads required code blocks from a temporary file specified by an environment variable.
1435
- #
1436
- # @return [Array<String>] An array containing the lines read from the temporary file.
1437
- # @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
1438
- def read_required_blocks_from_temp_file
1439
- temp_blocks = []
1440
-
1441
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
1442
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
1443
- return temp_blocks
1444
- end
1445
-
1446
- if File.exist?(temp_blocks_file_path)
1447
- temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
1448
- end
1449
-
1450
- temp_blocks
1451
- end
1452
-
1453
- # Replace duplicate blanks (where :oname is not present) with a single blank line.
1454
- #
1455
- # @param [Array<Hash>] lines Array of hashes to process.
1456
- # @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
1457
- def replace_consecutive_blanks(lines)
1458
- lines.chunk_while do |i, j|
1459
- i[:oname].to_s.empty? && j[:oname].to_s.empty?
1460
- end.map do |chunk|
1461
- if chunk.any? do |line|
1462
- line[:oname].to_s.strip.empty?
1463
- end
1464
- chunk.first
1465
- else
1466
- chunk
1467
- end
1468
- end.flatten
1469
- end
515
+ public
1470
516
 
1471
517
  def run
1472
518
  clear_required_file
1473
519
  execute_block_with_error_handling(initialize_and_parse_cli_options)
1474
- delete_required_temp_file
1475
- rescue StandardError => err
1476
- warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
1477
- binding.pry if $tap_enable
1478
- raise ArgumentError, error
520
+ @options.delete_required_temp_file
521
+ rescue StandardError
522
+ error_handler('run')
1479
523
  end
1480
524
 
525
+ private
526
+
1481
527
  def run_last_script
1482
528
  filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
1483
529
  @options[:saved_script_glob])
@@ -1485,89 +531,20 @@ module MarkdownExec
1485
531
 
1486
532
  saved_name_split filename
1487
533
  @options[:save_executed_script] = false
1488
- select_approve_and_execute_block
1489
- end
1490
-
1491
- def safeval(str)
1492
- eval(str)
534
+ @options.select_approve_and_execute_block
1493
535
  rescue StandardError
1494
- warn $!
1495
- binding.pry if $tap_enable
1496
- raise StandardError, $!
1497
- end
1498
-
1499
- def save_to_file(opts, required_lines)
1500
- write_command_file(opts.merge(save_executed_script: true),
1501
- required_lines)
1502
- fout "File saved: #{@options[:saved_filespec]}"
536
+ error_handler('run_last_script')
1503
537
  end
1504
538
 
1505
539
  def saved_name_split(name)
1506
540
  # rubocop:disable Layout/LineLength
1507
- mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
541
+ mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match(name)
1508
542
  # rubocop:enable Layout/LineLength
1509
543
  return unless mf
1510
544
 
1511
545
  @options[:block_name] = mf[:block]
1512
- @options[:filename] = mf[:file].gsub(FNR12, FNR11)
1513
- end
1514
-
1515
- # Select and execute a code block from a Markdown document.
1516
- #
1517
- # This method allows the user to interactively select a code block from a
1518
- # Markdown document, obtain approval, and execute the chosen block of code.
1519
- #
1520
- # @param call_options [Hash] Initial options for the method.
1521
- # @param options_block [Block] Block of options to be merged with call_options.
1522
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1523
- def select_approve_and_execute_block(call_options = {},
1524
- &options_block)
1525
- base_opts = optsmerge(call_options, options_block)
1526
- repeat_menu = true && !base_opts[:block_name].present?
1527
- load_file = LoadFile::Reuse
1528
- default = nil
1529
- block = nil
1530
-
1531
- loop do
1532
- loop do
1533
- opts = base_opts.dup
1534
- opts[:s_back] = false
1535
- blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
1536
- block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
1537
- default, opts)
1538
- return if state == MenuState::EXIT
1539
-
1540
- load_file, next_block_name = approve_and_execute_block(block, opts,
1541
- mdoc)
1542
- default = load_file == LoadFile::Load ? nil : opts[:block_name]
1543
- base_opts[:block_name] = opts[:block_name] = next_block_name
1544
- base_opts[:filename] = opts[:filename]
1545
-
1546
- # user prompt to exit if the menu will be displayed again
1547
- #
1548
- if repeat_menu &&
1549
- block[:shell] == BlockType::BASH &&
1550
- opts[:pause_after_bash_exec] &&
1551
- prompt_select_continue(opts) == MenuState::EXIT
1552
- return
1553
- end
1554
-
1555
- # exit current document/menu if loading next document or single block_name was specified
1556
- #
1557
- if state == MenuState::CONTINUE && load_file == LoadFile::Load
1558
- break
1559
- end
1560
- break unless repeat_menu
1561
- end
1562
- break if load_file == LoadFile::Reuse
1563
-
1564
- repeat_menu = next_block_name_from_command_line_arguments(base_opts)
1565
- end
1566
- rescue StandardError => err
1567
- warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
1568
- warn err.backtrace
1569
- binding.pry if $tap_enable
1570
- raise ArgumentError, error
546
+ @options[:filename] = mf[:file].gsub(@options[:saved_filename_pattern],
547
+ @options[:saved_filename_replacement])
1571
548
  end
1572
549
 
1573
550
  def select_document_if_multiple(files = list_markdown_files_in_path)
@@ -1576,44 +553,24 @@ module MarkdownExec
1576
553
  return unless count >= 2
1577
554
 
1578
555
  opts = options.dup
1579
- select_option_or_exit opts[:prompt_select_md].to_s, files,
1580
- opts.merge(per_page: opts[:select_page_height])
556
+ select_option_or_exit(HashDelegator.new(@options).string_send_color(opts[:prompt_select_md].to_s, :prompt_color_after_script_execution),
557
+ files,
558
+ opts.merge(per_page: opts[:select_page_height]))
1581
559
  end
1582
560
 
1583
561
  # Presents a TTY prompt to select an option or exit, returns selected option or nil
1584
- def select_option_or_exit(prompt_text, items, opts = {})
1585
- result = select_option_with_metadata(prompt_text, items, opts)
562
+ def select_option_or_exit(prompt_text, strings, opts = {})
563
+ result = @options.select_option_with_metadata(prompt_text, strings,
564
+ opts)
1586
565
  return unless result.fetch(:option, nil)
1587
566
 
1588
567
  result[:selected]
1589
568
  end
1590
569
 
1591
- # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1592
- def select_option_with_metadata(prompt_text, items, opts = {})
1593
- selection = @prompt.select(prompt_text,
1594
- items,
1595
- opts.merge(filter: true))
1596
-
1597
- items.find { |item| item[:dname] == selection }
1598
- .merge(
1599
- if selection == menu_chrome_colored_option(opts,
1600
- :menu_option_back_name)
1601
- { option: selection, curr: @hs_curr, rest: @hs_rest,
1602
- shell: BlockType::LINK }
1603
- elsif selection == menu_chrome_colored_option(opts,
1604
- :menu_option_exit_name)
1605
- { option: selection }
1606
- else
1607
- { selected: selection }
1608
- end
1609
- )
1610
- rescue TTY::Reader::InputInterrupt
1611
- exit 1
1612
- end
1613
-
1614
570
  def select_recent_output
1615
571
  filename = select_option_or_exit(
1616
- @options[:prompt_select_output].to_s,
572
+ HashDelegator.new(@options).string_send_color(@options[:prompt_select_output].to_s,
573
+ :prompt_color_after_script_execution),
1617
574
  list_recent_output(
1618
575
  @options[:saved_stdout_folder],
1619
576
  @options[:saved_stdout_glob],
@@ -1628,7 +585,8 @@ module MarkdownExec
1628
585
 
1629
586
  def select_recent_script
1630
587
  filename = select_option_or_exit(
1631
- @options[:prompt_select_md].to_s,
588
+ HashDelegator.new(@options).string_send_color(@options[:prompt_select_md].to_s,
589
+ :prompt_color_after_script_execution),
1632
590
  list_recent_scripts(
1633
591
  @options[:saved_script_folder],
1634
592
  @options[:saved_script_glob],
@@ -1640,37 +598,10 @@ module MarkdownExec
1640
598
 
1641
599
  saved_name_split(filename)
1642
600
 
1643
- select_approve_and_execute_block({ bash: true,
1644
- save_executed_script: false,
1645
- struct: true })
601
+ @options.select_approve_and_execute_block ### ({ save_executed_script: false })
1646
602
  end
1647
603
 
1648
- def start_fenced_block(opts, line, headings,
1649
- fenced_start_extended_regex)
1650
- fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1651
- rest = fcb_title_groups.fetch(:rest, '')
1652
-
1653
- fcb = FCB.new
1654
- fcb.headings = headings
1655
- fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
1656
- fcb.shell = fcb_title_groups.fetch(:shell, '')
1657
- fcb.title = fcb_title_groups.fetch(:name, '')
1658
- fcb.body = []
1659
- fcb.reqs, fcb.wraps =
1660
- ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
1661
- req[1..-1]
1662
- end) do |name|
1663
- !name.match(Regexp.new(opts[:block_name_wrapper_match]))
1664
- end
1665
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
1666
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
1667
- tn.named_captures.sym_keys
1668
- end
1669
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
1670
- tn.named_captures.sym_keys
1671
- end
1672
- fcb
1673
- end
604
+ public
1674
605
 
1675
606
  def tab_completions(data = menu_for_optparse)
1676
607
  data.map do |item|
@@ -1678,83 +609,6 @@ module MarkdownExec
1678
609
  end.compact
1679
610
  end
1680
611
 
1681
- def tty_prompt_without_disabled_symbol
1682
- TTY::Prompt.new(interrupt: lambda {
1683
- puts;
1684
- raise TTY::Reader::InputInterrupt
1685
- },
1686
- symbols: { cross: ' ' })
1687
- end
1688
-
1689
- ##
1690
- # Updates the hierarchy of document headings based on the given line and existing headings.
1691
- # The function uses regular expressions specified in the `opts` to identify different levels of headings.
1692
- #
1693
- # @param line [String] The line of text to examine for heading content.
1694
- # @param headings [Array<String>] The existing list of document headings.
1695
- # @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
1696
- #
1697
- # @option opts [String] :heading1_match Regular expression for matching first-level headings.
1698
- # @option opts [String] :heading2_match Regular expression for matching second-level headings.
1699
- # @option opts [String] :heading3_match Regular expression for matching third-level headings.
1700
- #
1701
- # @return [Array<String>] Updated list of headings.
1702
- def update_document_headings(line, headings, opts)
1703
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
1704
- [headings[0], headings[1], lm[:name]]
1705
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
1706
- [headings[0], lm[:name]]
1707
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
1708
- [lm[:name]]
1709
- else
1710
- headings
1711
- end
1712
- end
1713
-
1714
- ##
1715
- # Processes an individual line within a loop, updating headings and handling fenced code blocks.
1716
- # This function is designed to be called within a loop that iterates through each line of a document.
1717
- #
1718
- # @param line [String] The current line being processed.
1719
- # @param state [Hash] The current state of the parser, including flags and data related to the processing.
1720
- # @param opts [Hash] A hash containing various options for line and block processing.
1721
- # @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
1722
- # @param block [Proc] An optional block for further processing or transformation of lines.
1723
- #
1724
- # @option state [Array<String>] :headings Current headings to be updated based on the line.
1725
- # @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
1726
- # @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
1727
- # @option state [Object] :fcb An object representing the current fenced code block being processed.
1728
- #
1729
- # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
1730
- #
1731
- # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1732
- ##
1733
- def update_line_and_block_state(line, state, opts, selected_messages,
1734
- &block)
1735
- if opts[:menu_blocks_with_headings]
1736
- state[:headings] =
1737
- update_document_headings(line, state[:headings], opts)
1738
- end
1739
-
1740
- if line.match(state[:fenced_start_and_end_regex])
1741
- if state[:in_fenced_block]
1742
- process_fenced_block(state[:fcb], opts, selected_messages,
1743
- &block)
1744
- state[:in_fenced_block] = false
1745
- else
1746
- state[:fcb] =
1747
- start_fenced_block(opts, line, state[:headings],
1748
- state[:fenced_start_extended_regex])
1749
- state[:in_fenced_block] = true
1750
- end
1751
- elsif state[:in_fenced_block] && state[:fcb].body
1752
- state[:fcb].body += [line.chomp]
1753
- else
1754
- process_line(line, opts, selected_messages, &block)
1755
- end
1756
- end
1757
-
1758
612
  # :reek:BooleanParameter
1759
613
  # :reek:ControlParameter
1760
614
  def update_options(opts = {}, over: true)
@@ -1765,125 +619,6 @@ module MarkdownExec
1765
619
  end
1766
620
  @options
1767
621
  end
1768
-
1769
- # Updates the title of an FCB object from its body content if the title is nil or empty.
1770
- def update_title_from_body(fcb)
1771
- return unless fcb.title.nil? || fcb.title.empty?
1772
-
1773
- fcb.title = derive_title_from_body(fcb)
1774
- end
1775
-
1776
- def wait_for_user_selected_block(blocks_in_file, blocks_menu,
1777
- default, opts)
1778
- block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
1779
- default, opts)
1780
- case state
1781
- when MenuState::BACK
1782
- opts[:block_name] = block[:dname]
1783
- opts[:s_back] = true
1784
- when MenuState::CONTINUE
1785
- opts[:block_name] = block[:dname]
1786
- end
1787
-
1788
- [block, state]
1789
- end
1790
-
1791
- ## Handles the menu interaction and returns selected block and option state
1792
- #
1793
- def wait_for_user_selection(blocks_in_file, blocks_menu, default,
1794
- opts)
1795
- pt = opts[:prompt_select_block].to_s
1796
- bm = prepare_blocks_menu(blocks_menu, opts)
1797
- return [nil, MenuState::EXIT] if bm.count.zero?
1798
-
1799
- o2 = if default
1800
- opts.merge(default: default)
1801
- else
1802
- opts
1803
- end
1804
-
1805
- obj = select_option_with_metadata(pt, bm, o2.merge(
1806
- per_page: opts[:select_page_height]
1807
- ))
1808
-
1809
- case obj.fetch(:oname, nil)
1810
- when menu_chrome_formatted_option(opts, :menu_option_exit_name)
1811
- [nil, MenuState::EXIT]
1812
- when menu_chrome_formatted_option(opts, :menu_option_back_name)
1813
- [obj, MenuState::BACK]
1814
- else
1815
- [obj, MenuState::CONTINUE]
1816
- end
1817
- rescue StandardError => err
1818
- warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
1819
- warn caller.take(3)
1820
- binding.pry if $tap_enable
1821
- raise ArgumentError, error
1822
- end
1823
-
1824
- # Handles the core logic for generating the command file's metadata and content.
1825
- def write_command_file(call_options, required_lines)
1826
- return unless call_options[:save_executed_script]
1827
-
1828
- time_now = Time.now.utc
1829
- opts = optsmerge call_options
1830
- opts[:saved_script_filename] =
1831
- SavedAsset.script_name(blockname: opts[:block_name],
1832
- filename: opts[:filename],
1833
- prefix: opts[:saved_script_filename_prefix],
1834
- time: time_now)
1835
-
1836
- @execute_script_filespec =
1837
- @options[:saved_filespec] =
1838
- File.join opts[:saved_script_folder],
1839
- opts[:saved_script_filename]
1840
-
1841
- shebang = if @options[:shebang]&.present?
1842
- "#{@options[:shebang]} #{@options[:shell]}\n"
1843
- else
1844
- ''
1845
- end
1846
-
1847
- content = shebang +
1848
- "# file_name: #{opts[:filename]}\n" \
1849
- "# block_name: #{opts[:block_name]}\n" \
1850
- "# time: #{time_now}\n" \
1851
- "#{required_lines.flatten.join("\n")}\n"
1852
-
1853
- create_and_write_file_with_permissions(@options[:saved_filespec], content,
1854
- @options[:saved_script_chmod])
1855
- end
1856
-
1857
- def write_execution_output_to_file
1858
- FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
1859
-
1860
- ol = ["-STDOUT-\n"]
1861
- ol += @execute_files&.fetch(EF_STDOUT, [])
1862
- ol += ["\n-STDERR-\n"]
1863
- ol += @execute_files&.fetch(EF_STDERR, [])
1864
- ol += ["\n-STDIN-\n"]
1865
- ol += @execute_files&.fetch(EF_STDIN, [])
1866
- ol += ["\n"]
1867
- File.write(@options[:logged_stdout_filespec], ol.join)
1868
- end
1869
-
1870
- # Writes required code blocks to a temporary file and sets an environment variable with its path.
1871
- #
1872
- # @param block_name [String] The name of the block to collect code for.
1873
- # @param opts [Hash] Additional options for collecting code.
1874
- # @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
1875
- def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
1876
- code_blocks = (read_required_blocks_from_temp_file +
1877
- mdoc.collect_recursively_required_code(
1878
- block_name,
1879
- opts: opts
1880
- )[:code]).join("\n")
1881
-
1882
- Dir::Tmpname.create(self.class.to_s) do |path|
1883
- File.write(path, code_blocks)
1884
- ENV['MDE_LINK_REQUIRED_FILE'] = path
1885
- end
1886
- end
1887
622
  end # class MarkParse
1888
623
  end # module MarkdownExec
1889
624
 
@@ -1894,60 +629,6 @@ if $PROGRAM_NAME == __FILE__
1894
629
  require 'minitest/autorun'
1895
630
 
1896
631
  module MarkdownExec
1897
- class TestMarkParse < Minitest::Test
1898
- require 'mocha/minitest'
1899
-
1900
- def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1901
- pigeon = 'E'
1902
- obj = { s_pass_args: pigeon }
1903
-
1904
- c = MarkdownExec::MarkParse.new
1905
-
1906
- # Expect that method command_execute is called with argument args having value pigeon
1907
- c.expects(:command_execute).with(
1908
- obj,
1909
- '',
1910
- args: pigeon
1911
- )
1912
-
1913
- # Call method execute_approved_block
1914
- c.execute_approved_block(obj, [])
1915
- end
1916
-
1917
- def setup
1918
- @mark_parse = MarkdownExec::MarkParse.new
1919
- end
1920
-
1921
- def test_set_fcb_title
1922
- # sample input and output data for testing update_title_from_body method
1923
- input_output_data = [
1924
- {
1925
- input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1926
- output: "puts 'Hello, world!'"
1927
- },
1928
- {
1929
- input: FCB.new(title: '',
1930
- body: ['def add(x, y)',
1931
- ' x + y', 'end']),
1932
- output: "def add(x, y)\n x + y\n end\n"
1933
- },
1934
- {
1935
- input: FCB.new(title: 'foo', body: %w[bar baz]),
1936
- output: 'foo' # expect the title to remain unchanged
1937
- }
1938
- ]
1939
-
1940
- # iterate over the input and output data and
1941
- # assert that the method sets the title as expected
1942
- input_output_data.each do |data|
1943
- input = data[:input]
1944
- output = data[:output]
1945
- @mark_parse.update_title_from_body(input)
1946
- assert_equal output, input.title
1947
- end
1948
- end
1949
- end
1950
-
1951
632
  def test_select_block
1952
633
  blocks = [block1, block2]
1953
634
  menu = [m1, m2]