markdown_exec 1.8.8 → 2.0.0

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.
@@ -11,6 +11,7 @@ require 'optparse'
11
11
  require 'set'
12
12
  require 'shellwords'
13
13
  require 'tmpdir'
14
+ # require 'tty-file'
14
15
  require 'tty-prompt'
15
16
  require 'yaml'
16
17
 
@@ -40,10 +41,6 @@ class String
40
41
  end
41
42
 
42
43
  module HashDelegatorSelf
43
- # def add_back_option(menu_blocks)
44
- # append_chrome_block(menu_blocks, MenuState::BACK)
45
- # end
46
-
47
44
  # Applies an ANSI color method to a string using a specified color key.
48
45
  # The method retrieves the color method from the provided hash. If the color key
49
46
  # is not present in the hash, it uses a default color method.
@@ -185,16 +182,20 @@ module HashDelegatorSelf
185
182
  fcb.oname = fcb.dname = fcb.title || ''
186
183
  end
187
184
 
185
+ def join_code_lines(lines)
186
+ ((lines || []) + ['']).join("\n")
187
+ end
188
+
188
189
  def merge_lists(*args)
189
190
  # Filters out nil values, flattens the arrays, and ensures an empty list is returned if no valid lists are provided
190
191
  merged = args.compact.flatten
191
192
  merged.empty? ? [] : merged
192
193
  end
193
194
 
194
- def next_link_state(block_name_from_cli, was_using_cli, block_state)
195
+ def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, block_name: nil)
195
196
  # &bsp 'next_link_state', block_name_from_cli, was_using_cli, block_state
196
197
  # Set block_name based on block_name_from_cli
197
- block_name = block_name_from_cli ? @cli_block_name : nil
198
+ block_name = @cli_block_name if block_name_from_cli
198
199
  # &bsp 'block_name:', block_name
199
200
 
200
201
  # Determine the state of breaker based on was_using_cli and the block type
@@ -210,6 +211,8 @@ module HashDelegatorSelf
210
211
 
211
212
  def parse_yaml_data_from_body(body)
212
213
  body.any? ? YAML.load(body.join("\n")) : {}
214
+ rescue StandardError
215
+ error_handler('parse_yaml_data_from_body', { abort: true })
213
216
  end
214
217
 
215
218
  # Reads required code blocks from a temporary file specified by an environment variable.
@@ -264,21 +267,15 @@ module HashDelegatorSelf
264
267
  # @param fcb [Object] The fcb object whose attributes are to be updated.
265
268
  # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable.
266
269
  # @param block [Block] An optional block to yield to if conditions are met.
267
- def update_menu_attrib_yield_selected(fcb, selected_messages, configuration = {}, &block)
270
+ def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, &block)
268
271
  initialize_fcb_names(fcb)
269
272
  return unless fcb.body
270
273
 
271
274
  default_block_title_from_body(fcb)
272
- MarkdownExec::Filter.yield_to_block_if_applicable(fcb, selected_messages, configuration,
275
+ MarkdownExec::Filter.yield_to_block_if_applicable(fcb, messages, configuration,
273
276
  &block)
274
277
  end
275
278
 
276
- # Writes the provided code blocks to a file.
277
- # @param code_blocks [String] Code blocks to write into the file.
278
- def write_code_to_file(content, path)
279
- File.write(path, content)
280
- end
281
-
282
279
  def write_execution_output_to_file(files, filespec)
283
280
  FileUtils.mkdir_p File.dirname(filespec)
284
281
 
@@ -304,7 +301,6 @@ module HashDelegatorSelf
304
301
  block.call(:line, MarkdownExec::FCB.new(body: [line]))
305
302
  end
306
303
  end
307
- ### require_relative 'hash_delegator_self'
308
304
 
309
305
  # This module provides methods for compacting and converting data structures.
310
306
  module CompactionHelpers
@@ -408,40 +404,37 @@ module MarkdownExec
408
404
  # along with initial and final dividers, based on the delegate object's configuration.
409
405
  #
410
406
  # @param menu_blocks [Array] The array of menu block elements to be modified.
411
- def add_menu_chrome_blocks!(menu_blocks, link_state)
407
+ def add_menu_chrome_blocks!(menu_blocks:, link_state:)
412
408
  return unless @delegate_object[:menu_link_format].present?
413
409
 
414
- if @delegate_object[:menu_with_inherited_lines]
415
- add_inherited_lines(menu_blocks,
416
- link_state)
417
- end
410
+ add_inherited_lines(menu_blocks: menu_blocks, link_state: link_state) if @delegate_object[:menu_with_inherited_lines]
418
411
 
419
412
  # back before exit
420
- add_back_option(menu_blocks) if should_add_back_option?
413
+ add_back_option(menu_blocks: menu_blocks) if should_add_back_option?
421
414
 
422
415
  # exit after other options
423
- add_exit_option(menu_blocks) if @delegate_object[:menu_with_exit]
416
+ add_exit_option(menu_blocks: menu_blocks) if @delegate_object[:menu_with_exit]
424
417
 
425
- add_dividers(menu_blocks)
418
+ add_dividers(menu_blocks: menu_blocks)
426
419
  end
427
420
 
428
421
  private
429
422
 
430
- def add_back_option(menu_blocks)
431
- append_chrome_block(menu_blocks, MenuState::BACK)
423
+ def add_back_option(menu_blocks:)
424
+ append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::BACK)
432
425
  end
433
426
 
434
- def add_dividers(menu_blocks)
435
- append_divider(menu_blocks, :initial)
436
- append_divider(menu_blocks, :final)
427
+ def add_dividers(menu_blocks:)
428
+ append_divider(menu_blocks: menu_blocks, position: :initial)
429
+ append_divider(menu_blocks: menu_blocks, position: :final)
437
430
  end
438
431
 
439
- def add_exit_option(menu_blocks)
440
- append_chrome_block(menu_blocks, MenuState::EXIT)
432
+ def add_exit_option(menu_blocks:)
433
+ append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::EXIT)
441
434
  end
442
435
 
443
- def add_inherited_lines(menu_blocks, link_state)
444
- append_inherited_lines(menu_blocks, link_state)
436
+ def add_inherited_lines(menu_blocks:, link_state:)
437
+ append_inherited_lines(menu_blocks: menu_blocks, link_state: link_state)
445
438
  end
446
439
 
447
440
  public
@@ -450,8 +443,8 @@ module MarkdownExec
450
443
  #
451
444
  # @param all_blocks [Array] The current blocks in the menu
452
445
  # @param type [Symbol] The type of chrome block to add (:back or :exit)
453
- def append_chrome_block(menu_blocks, type)
454
- case type
446
+ def append_chrome_block(menu_blocks:, menu_state:)
447
+ case menu_state
455
448
  when MenuState::BACK
456
449
  history_state_partition
457
450
  option_name = @delegate_object[:menu_option_back_name]
@@ -483,7 +476,7 @@ module MarkdownExec
483
476
  #
484
477
  # @param menu_blocks [Array] The array of menu block elements.
485
478
  # @param position [Symbol] The position to insert the divider (:initial or :final).
486
- def append_inherited_lines(menu_blocks, link_state, position: top)
479
+ def append_inherited_lines(menu_blocks:, link_state:, position: top)
487
480
  return unless link_state.inherited_lines.present?
488
481
 
489
482
  insert_at_top = @delegate_object[:menu_inherited_lines_at_top]
@@ -516,7 +509,7 @@ module MarkdownExec
516
509
  #
517
510
  # @param menu_blocks [Array] The array of menu block elements.
518
511
  # @param position [Symbol] The position to insert the divider (:initial or :final).
519
- def append_divider(menu_blocks, position)
512
+ def append_divider(menu_blocks:, position:)
520
513
  return unless divider_formatting_present?(position)
521
514
 
522
515
  divider = create_divider(position)
@@ -538,6 +531,15 @@ module MarkdownExec
538
531
  end
539
532
  end
540
533
 
534
+ def assign_key_value_in_bash(key, value)
535
+ if value =~ /["$\\`]/
536
+ # requiring ShellWords to write into Bash scripts
537
+ "#{key}=#{Shellwords.escape(value)}"
538
+ else
539
+ "#{key}=\"#{value}\""
540
+ end
541
+ end
542
+
541
543
  # private
542
544
 
543
545
  # Iterates through nested files to collect various types of blocks, including dividers, tasks, and others.
@@ -599,11 +601,9 @@ module MarkdownExec
599
601
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
600
602
  # @param selected [Hash] The selected block.
601
603
  # @return [Array<String>] Required code blocks as an array of lines.
602
- def collect_required_code_lines(mdoc, selected, link_state = LinkState.new, block_source:)
603
- set_environment_variables_for_block(selected) if selected[:shell] == BlockType::VARS
604
-
604
+ def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new)
605
605
  required = mdoc.collect_recursively_required_code(
606
- selected[:nickname] || selected[:oname],
606
+ anyname: selected[:nickname] || selected[:oname],
607
607
  label_format_above: @delegate_object[:shell_code_label_format_above],
608
608
  label_format_below: @delegate_object[:shell_code_label_format_below],
609
609
  block_source: block_source
@@ -619,12 +619,14 @@ module MarkdownExec
619
619
  runtime_exception(:runtime_exception_error_level,
620
620
  'unmet_dependencies, flag: runtime_exception_error_level',
621
621
  required[:unmet_dependencies])
622
- elsif true
622
+ else
623
623
  warn format_and_highlight_dependencies(dependencies,
624
624
  highlight: [@delegate_object[:block_name]])
625
625
  end
626
626
 
627
- HashDelegator.code_merge(link_state&.inherited_lines, required[:code])
627
+ code_lines = selected[:shell] == BlockType::VARS ? set_environment_variables_for_block(selected) : []
628
+
629
+ HashDelegator.code_merge(link_state&.inherited_lines, required[:code] + code_lines)
628
630
  end
629
631
 
630
632
  def command_execute(command, args: [])
@@ -663,14 +665,14 @@ module MarkdownExec
663
665
  '-c', command,
664
666
  @delegate_object[:filename],
665
667
  *args) do |stdin, stdout, stderr, exec_thr|
666
- handle_stream(stdout, ExecutionStreams::StdOut) do |line|
668
+ handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
667
669
  yield nil, line, nil, exec_thr if block_given?
668
670
  end
669
- handle_stream(stderr, ExecutionStreams::StdErr) do |line|
671
+ handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
670
672
  yield nil, nil, line, exec_thr if block_given?
671
673
  end
672
674
 
673
- in_thr = handle_stream($stdin, ExecutionStreams::StdIn) do |line|
675
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
674
676
  stdin.puts(line)
675
677
  yield line, nil, nil, exec_thr if block_given?
676
678
  end
@@ -699,7 +701,7 @@ module MarkdownExec
699
701
  @fout.fout "Error ENOENT: #{err.inspect}"
700
702
  end
701
703
 
702
- def load_cli_or_user_selected_block(all_blocks, menu_blocks, default)
704
+ def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
703
705
  if @delegate_object[:block_name].present?
704
706
  block = all_blocks.find do |item|
705
707
  item[:oname] == @delegate_object[:block_name]
@@ -723,18 +725,18 @@ module MarkdownExec
723
725
  # @param mdoc [Object] The markdown document object containing code blocks.
724
726
  # @param selected [Hash] The selected item from the menu to be executed.
725
727
  # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one.
726
- def compile_execute_and_trigger_reuse(mdoc, selected, link_state = nil, block_source:)
727
- required_lines = collect_required_code_lines(mdoc, selected, link_state,
728
+ def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:, link_state: nil)
729
+ required_lines = collect_required_code_lines(mdoc: mdoc, selected: selected, link_state: link_state,
728
730
  block_source: block_source)
729
731
  output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
730
- display_required_code(required_lines) if output_or_approval
732
+ display_required_code(required_lines: required_lines) if output_or_approval
731
733
  allow_execution = if @delegate_object[:user_must_approve]
732
- prompt_for_user_approval(required_lines, selected)
734
+ prompt_for_user_approval(required_lines: required_lines, selected: selected)
733
735
  else
734
736
  true
735
737
  end
736
738
 
737
- execute_required_lines(required_lines, selected) if allow_execution
739
+ execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
738
740
 
739
741
  link_state.block_name = nil
740
742
  LoadFileLinkState.new(LoadFile::Reuse, link_state)
@@ -766,8 +768,8 @@ module MarkdownExec
766
768
  # @param match_data [MatchData] The match data containing named captures for formatting.
767
769
  # @param format_option [String] The format string to be used for the new block.
768
770
  # @param color_method [Symbol] The color method to apply to the block's display name.
769
- def create_and_add_chrome_block(blocks, match_data, format_option,
770
- color_method)
771
+ def create_and_add_chrome_block(blocks:, match_data:, format_option:,
772
+ color_method:)
771
773
  oname = format(format_option,
772
774
  match_data.named_captures.transform_keys(&:to_sym))
773
775
  blocks.push FCB.new(
@@ -800,8 +802,12 @@ module MarkdownExec
800
802
  next
801
803
  end
802
804
 
803
- create_and_add_chrome_block(blocks, mbody, @delegate_object[criteria[:format]],
804
- @delegate_object[criteria[:color]].to_sym)
805
+ create_and_add_chrome_block(
806
+ blocks: blocks,
807
+ match_data: mbody,
808
+ format_option: @delegate_object[criteria[:format]],
809
+ color_method: @delegate_object[criteria[:color]].to_sym
810
+ )
805
811
  break
806
812
  end
807
813
  end
@@ -829,9 +835,7 @@ module MarkdownExec
829
835
  return true if @run_state.block_name_from_cli
830
836
 
831
837
  # return false if @prior_execution_block == @delegate_object[:block_name]
832
- if @prior_execution_block == @delegate_object[:block_name]
833
- return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat
834
- end
838
+ return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat if @prior_execution_block == @delegate_object[:block_name]
835
839
 
836
840
  @prior_execution_block = @delegate_object[:block_name]
837
841
  @allowed_execution_block = nil
@@ -865,7 +869,7 @@ module MarkdownExec
865
869
  # It wraps the code lines between a formatted header and tail.
866
870
  #
867
871
  # @param required_lines [Array<String>] The lines of code to be displayed.
868
- def display_required_code(required_lines)
872
+ def display_required_code(required_lines:)
869
873
  output_color_formatted(:script_preview_head,
870
874
  :script_preview_frame_color)
871
875
  required_lines.each { |cb| @fout.fout cb }
@@ -892,10 +896,10 @@ module MarkdownExec
892
896
  #
893
897
  # @param required_lines [Array<String>] The lines of code to be executed.
894
898
  # @param selected [FCB] The selected functional code block object.
895
- def execute_required_lines(required_lines = [], selected = FCB.new)
896
- write_command_file(required_lines, selected) if @delegate_object[:save_executed_script]
899
+ def execute_required_lines(required_lines: [], selected: FCB.new)
900
+ write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script]
897
901
  calc_logged_stdout_filename
898
- format_and_execute_command(required_lines)
902
+ format_and_execute_command(code_lines: required_lines)
899
903
  post_execution_process
900
904
  end
901
905
 
@@ -908,12 +912,14 @@ module MarkdownExec
908
912
  # @param opts [Hash] Options hash containing configuration settings.
909
913
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
910
914
  #
911
- def execute_shell_type(selected, mdoc, link_state = LinkState.new,
912
- block_source:)
915
+ def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new)
913
916
  if selected.fetch(:shell, '') == BlockType::LINK
914
917
  debounce_reset
915
- push_link_history_and_trigger_load(selected.fetch(:body, ''), mdoc, selected,
916
- link_state)
918
+ push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''),
919
+ mdoc: mdoc,
920
+ selected: selected,
921
+ link_state: link_state,
922
+ block_source: block_source)
917
923
 
918
924
  elsif @menu_user_clicked_back_link
919
925
  debounce_reset
@@ -921,13 +927,50 @@ module MarkdownExec
921
927
 
922
928
  elsif selected[:shell] == BlockType::OPTS
923
929
  debounce_reset
924
- options_state = read_show_options_and_trigger_reuse(selected, link_state)
930
+ block_names = []
931
+ code_lines = []
932
+ dependencies = {}
933
+ options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state)
934
+
935
+ ## apply options to current state
936
+ #
925
937
  @menu_base_options.merge!(options_state.options)
926
938
  @delegate_object.merge!(options_state.options)
927
- options_state.load_file_link_state
939
+
940
+ ### options_state.load_file_link_state
941
+ link_state = LinkState.new
942
+ link_history_push_and_next(
943
+ curr_block_name: selected[:oname],
944
+ curr_document_filename: @delegate_object[:filename],
945
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
946
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
947
+ inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
948
+ next_block_name: '',
949
+ next_document_filename: @delegate_object[:filename],
950
+ next_load_file: LoadFile::Reuse
951
+ )
952
+
953
+
954
+ elsif selected[:shell] == BlockType::VARS
955
+ debounce_reset
956
+ block_names = []
957
+ code_lines = set_environment_variables_for_block(selected)
958
+ dependencies = {}
959
+ link_history_push_and_next(
960
+ curr_block_name: selected[:oname],
961
+ curr_document_filename: @delegate_object[:filename],
962
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
963
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
964
+ inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
965
+ next_block_name: '',
966
+ next_document_filename: @delegate_object[:filename],
967
+ next_load_file: LoadFile::Reuse
968
+ )
928
969
 
929
970
  elsif debounce_allows
930
- compile_execute_and_trigger_reuse(mdoc, selected, link_state,
971
+ compile_execute_and_trigger_reuse(mdoc: mdoc,
972
+ selected: selected,
973
+ link_state: link_state,
931
974
  block_source: block_source)
932
975
  else
933
976
  LoadFileLinkState.new(LoadFile::Reuse, link_state)
@@ -948,8 +991,8 @@ module MarkdownExec
948
991
  string_send_color(data_string, color_sym)
949
992
  end
950
993
 
951
- def format_and_execute_command(lines)
952
- formatted_command = lines.flatten.join("\n")
994
+ def format_and_execute_command(code_lines:)
995
+ formatted_command = code_lines.flatten.join("\n")
953
996
  @fout.fout fetch_color(data_sym: :script_execution_head,
954
997
  color_sym: :script_execution_frame_color)
955
998
  command_execute(formatted_command, args: @pass_args)
@@ -1032,7 +1075,7 @@ module MarkdownExec
1032
1075
  @menu_user_clicked_back_link = block_state.state == MenuState::BACK
1033
1076
  end
1034
1077
 
1035
- def handle_stream(stream, file_type, swap: false)
1078
+ def handle_stream(stream:, file_type:, swap: false)
1036
1079
  @process_mutex.synchronize do
1037
1080
  Thread.new do
1038
1081
  stream.each_line do |line|
@@ -1086,10 +1129,10 @@ module MarkdownExec
1086
1129
  end
1087
1130
  end
1088
1131
 
1089
- def link_block_data_eval(link_state, code_lines, selected, link_block_data)
1132
+ def link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source:)
1090
1133
  all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines)
1091
1134
 
1092
- if link_block_data.fetch(LinkDataKeys::Exec, false)
1135
+ if link_block_data.fetch(LinkKeys::Exec, false)
1093
1136
  @run_state.files = Hash.new([])
1094
1137
  output_lines = []
1095
1138
 
@@ -1097,14 +1140,14 @@ module MarkdownExec
1097
1140
  @delegate_object[:shell],
1098
1141
  '-c', all_code.join("\n")
1099
1142
  ) do |stdin, stdout, stderr, _exec_thr|
1100
- handle_stream(stdout, ExecutionStreams::StdOut) do |line|
1143
+ handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
1101
1144
  output_lines.push(line)
1102
1145
  end
1103
- handle_stream(stderr, ExecutionStreams::StdErr) do |line|
1146
+ handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
1104
1147
  output_lines.push(line)
1105
1148
  end
1106
1149
 
1107
- in_thr = handle_stream($stdin, ExecutionStreams::StdIn) do |line|
1150
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
1108
1151
  stdin.puts(line)
1109
1152
  end
1110
1153
 
@@ -1127,13 +1170,10 @@ module MarkdownExec
1127
1170
  output_lines = `#{all_code.join("\n")}`.split("\n")
1128
1171
  end
1129
1172
 
1130
- unless output_lines
1131
- HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true })
1132
- end
1173
+ HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true }) unless output_lines
1133
1174
 
1134
1175
  label_format_above = @delegate_object[:shell_code_label_format_above]
1135
1176
  label_format_below = @delegate_object[:shell_code_label_format_below]
1136
- block_source = { document_filename: link_state&.document_filename }
1137
1177
 
1138
1178
  [label_format_above && format(label_format_above,
1139
1179
  block_source.merge({ block_name: selected[:oname] }))] +
@@ -1172,20 +1212,126 @@ module MarkdownExec
1172
1212
  )
1173
1213
  end
1174
1214
 
1215
+ # format + glob + select for file in load block
1216
+ # name has references to ENV vars and doc and batch vars incl. timestamp
1217
+ def load_filespec_from_expression(expression)
1218
+ # Process expression with embedded formatting
1219
+ expanded_expression = formatted_expression(expression)
1220
+
1221
+ # Handle wildcards or direct file specification
1222
+ if contains_wildcards?(expanded_expression)
1223
+ load_filespec_wildcard_expansion(expanded_expression)
1224
+ else
1225
+ expanded_expression
1226
+ end
1227
+ end
1228
+
1229
+ def save_filespec_from_expression(expression)
1230
+ # Process expression with embedded formatting
1231
+ formatted = formatted_expression(expression)
1232
+
1233
+ # Handle wildcards or direct file specification
1234
+ if contains_wildcards?(formatted)
1235
+ save_filespec_wildcard_expansion(formatted)
1236
+ else
1237
+ formatted
1238
+ end
1239
+ end
1240
+
1241
+ # private
1242
+
1243
+ # Expand expression if it contains format specifiers
1244
+ def formatted_expression(expr)
1245
+ expr.include?('%{') ? format_expression(expr) : expr
1246
+ end
1247
+
1248
+ # Format expression using environment variables and run state
1249
+ def format_expression(expr)
1250
+ data = link_load_format_data
1251
+ ENV.each { |key, value| data[key] = value }
1252
+ format(expr, data)
1253
+ end
1254
+
1255
+ # Check if the expression contains wildcard characters
1256
+ def contains_wildcards?(expr)
1257
+ expr.match(%r{\*|\?|\[})
1258
+ end
1259
+
1260
+ # Handle expression with wildcard characters
1261
+ def load_filespec_wildcard_expansion(expr)
1262
+ files = find_files(expr)
1263
+ case files.count
1264
+ when 0
1265
+ HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
1266
+ when 1
1267
+ files.first
1268
+ else
1269
+ prompt_select_code_filename(files)
1270
+ end
1271
+ end
1272
+
1273
+ # Handle expression with wildcard characters
1274
+ # allow user to select or enter
1275
+ def puts_gets_oprompt_(filespec)
1276
+ puts format(@delegate_object[:prompt_show_expr_format],
1277
+ { expr: filespec })
1278
+ puts @delegate_object[:prompt_enter_filespec]
1279
+ gets.chomp
1280
+ end
1281
+
1282
+ # prompt user to enter a path (i.e. containing a path separator)
1283
+ # or name to substitute into the wildcard expression
1284
+ def prompt_for_filespec_with_wildcard(filespec)
1285
+ puts format(@delegate_object[:prompt_show_expr_format],
1286
+ { expr: filespec })
1287
+ puts @delegate_object[:prompt_enter_filespec]
1288
+ resolve_path_or_substitute(gets.chomp, filespec)
1289
+ end
1290
+
1291
+ # Handle expression with wildcard characters
1292
+ # allow user to select or enter
1293
+ def save_filespec_wildcard_expansion(filespec)
1294
+ files = find_files(filespec)
1295
+ case files.count
1296
+ when 0
1297
+ prompt_for_filespec_with_wildcard(filespec)
1298
+ else
1299
+ ## user selects from existing files or other
1300
+ # input into path with wildcard for easy entry
1301
+ #
1302
+ name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
1303
+ if name == @delegate_object[:prompt_filespec_other]
1304
+ prompt_for_filespec_with_wildcard(filespec)
1305
+ else
1306
+ name
1307
+ end
1308
+ end
1309
+ end
1310
+
1311
+ def link_load_format_data
1312
+ {
1313
+ batch_index: @run_state.batch_index,
1314
+ batch_random: @run_state.batch_random,
1315
+ block_name: @delegate_object[:block_name],
1316
+ document_filename: File.basename(@delegate_object[:filename]),
1317
+ document_filespec: @delegate_object[:filename],
1318
+ home: Dir.pwd,
1319
+ started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format])
1320
+ }
1321
+ end
1322
+
1175
1323
  # Loads auto blocks based on delegate object settings and updates if new filename is detected.
1176
1324
  # Executes a specified block once per filename.
1177
1325
  # @param all_blocks [Array] Array of all block elements.
1178
1326
  # @return [Boolean, nil] True if values were modified, nil otherwise.
1179
1327
  def load_auto_blocks(all_blocks)
1180
1328
  block_name = @delegate_object[:document_load_opts_block_name]
1181
- unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1182
- return
1183
- end
1329
+ return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1184
1330
 
1185
1331
  block = HashDelegator.block_find(all_blocks, :oname, block_name)
1186
1332
  return unless block
1187
1333
 
1188
- options_state = read_show_options_and_trigger_reuse(block)
1334
+ options_state = read_show_options_and_trigger_reuse(selected: block)
1189
1335
  @menu_base_options.merge!(options_state.options)
1190
1336
  @delegate_object.merge!(options_state.options)
1191
1337
 
@@ -1211,7 +1357,7 @@ module MarkdownExec
1211
1357
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_blocks(all_blocks)
1212
1358
 
1213
1359
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1214
- add_menu_chrome_blocks!(menu_blocks, link_state)
1360
+ add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
1215
1361
  ### compress empty lines
1216
1362
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true
1217
1363
  [all_blocks, menu_blocks, mdoc]
@@ -1253,13 +1399,6 @@ module MarkdownExec
1253
1399
  end
1254
1400
  end
1255
1401
 
1256
- def shift_cli_argument
1257
- return true unless @menu_base_options[:input_cli_rest].present?
1258
-
1259
- @cli_block_name = @menu_base_options[:input_cli_rest].shift
1260
- false
1261
- end
1262
-
1263
1402
  def output_color_formatted(data_sym, color_sym)
1264
1403
  formatted_string = string_send_color(@delegate_object[data_sym],
1265
1404
  color_sym)
@@ -1338,7 +1477,7 @@ module MarkdownExec
1338
1477
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1339
1478
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1340
1479
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1341
- next_block_name: '', # not link_block_data['block'] || ''
1480
+ next_block_name: '', # not link_block_data[LinkKeys::Block] || ''
1342
1481
  next_document_filename: @delegate_object[:filename], # not next_document_filename
1343
1482
  next_load_file: LoadFile::Reuse # not next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1344
1483
  )
@@ -1482,7 +1621,7 @@ module MarkdownExec
1482
1621
  #
1483
1622
  # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
1484
1623
  ##
1485
- def prompt_for_user_approval(required_lines, selected)
1624
+ def prompt_for_user_approval(required_lines:, selected:)
1486
1625
  # Present a selection menu for user approval.
1487
1626
  sel = @prompt.select(
1488
1627
  string_send_color(@delegate_object[:prompt_approve_block],
@@ -1502,7 +1641,7 @@ module MarkdownExec
1502
1641
  if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1503
1642
  copy_to_clipboard(required_lines)
1504
1643
  elsif sel == MenuOptions::SAVE_SCRIPT
1505
- save_to_file(required_lines, selected)
1644
+ save_to_file(required_lines: required_lines, selected: selected)
1506
1645
  end
1507
1646
 
1508
1647
  sel == MenuOptions::YES
@@ -1527,6 +1666,21 @@ module MarkdownExec
1527
1666
 
1528
1667
  # public
1529
1668
 
1669
+ def prompt_select_code_filename(filenames)
1670
+ @prompt.select(
1671
+ string_send_color(@delegate_object[:prompt_select_code_file],
1672
+ :prompt_color_after_script_execution),
1673
+ filter: true,
1674
+ quiet: true
1675
+ ) do |menu|
1676
+ filenames.each do |filename|
1677
+ menu.choice filename
1678
+ end
1679
+ end
1680
+ rescue TTY::Reader::InputInterrupt
1681
+ exit 1
1682
+ end
1683
+
1530
1684
  # Handles the processing of a link block in Markdown Execution.
1531
1685
  # It loads YAML data from the link_block_body content, pushes the state to history,
1532
1686
  # sets environment variables, and decides on the next block to load.
@@ -1535,25 +1689,18 @@ module MarkdownExec
1535
1689
  # @param mdoc [Object] Markdown document object.
1536
1690
  # @param selected [FCB] Selected code block.
1537
1691
  # @return [LoadFileLinkState] Object indicating the next action for file loading.
1538
- def push_link_history_and_trigger_load(link_block_body, mdoc, selected,
1539
- link_state = LinkState.new)
1692
+ def push_link_history_and_trigger_load(link_block_body: [], mdoc: nil, selected: FCB.new,
1693
+ link_state: LinkState.new, block_source: {})
1540
1694
  link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body)
1541
1695
 
1542
- # load key and values from link block into current environment
1543
- #
1544
- (link_block_data['vars'] || []).each do |(key, value)|
1545
- ENV[key] = value.to_s
1546
- ### add to inherited_lines
1547
- end
1548
-
1549
1696
  ## collect blocks specified by block
1550
1697
  #
1551
1698
  if mdoc
1552
1699
  code_info = mdoc.collect_recursively_required_code(
1553
- selected[:oname],
1700
+ anyname: selected[:oname],
1554
1701
  label_format_above: @delegate_object[:shell_code_label_format_above],
1555
1702
  label_format_below: @delegate_object[:shell_code_label_format_below],
1556
- block_source: { document_filename: link_state.document_filename }
1703
+ block_source: block_source
1557
1704
  )
1558
1705
  code_lines = code_info[:code]
1559
1706
  block_names = code_info[:block_names]
@@ -1563,22 +1710,34 @@ module MarkdownExec
1563
1710
  code_lines = []
1564
1711
  dependencies = {}
1565
1712
  end
1566
- next_document_filename = link_block_data['file'] || @delegate_object[:filename]
1567
1713
 
1568
- ## append blocks loaded per LinkDataKeys::Load
1714
+ # load key and values from link block into current environment
1715
+ #
1716
+ if link_block_data[LinkKeys::Vars]
1717
+ code_lines.push "# #{selected[:oname]}"
1718
+ (link_block_data[LinkKeys::Vars] || []).each do |(key, value)|
1719
+ ENV[key] = value.to_s
1720
+ code_lines.push(assign_key_value_in_bash(key, value))
1721
+ end
1722
+ end
1723
+
1724
+ ## append blocks loaded, apply LinkKeys::Eval
1569
1725
  #
1570
- if (load_filespec = link_block_data.fetch(LinkDataKeys::Load, '')).present?
1571
- code_lines += File.readlines(load_filespec, chomp: true)
1726
+ if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
1727
+ load_filespec = load_filespec_from_expression(load_expr)
1728
+ code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
1572
1729
  end
1573
1730
 
1574
1731
  # if an eval link block, evaluate code_lines and return its standard output
1575
1732
  #
1576
- if link_block_data.fetch(LinkDataKeys::Eval,
1577
- false) || link_block_data.fetch(LinkDataKeys::Exec, false)
1578
- code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data)
1733
+ if link_block_data.fetch(LinkKeys::Eval,
1734
+ false) || link_block_data.fetch(LinkKeys::Exec, false)
1735
+ code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
1579
1736
  end
1580
1737
 
1581
- if link_block_data[LinkDataKeys::Return]
1738
+ next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
1739
+
1740
+ if link_block_data[LinkKeys::Return]
1582
1741
  pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
1583
1742
  dependencies, selected)
1584
1743
 
@@ -1589,13 +1748,26 @@ module MarkdownExec
1589
1748
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1590
1749
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1591
1750
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1592
- next_block_name: link_block_data['block'] || '',
1751
+ next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
1752
+ nil) || link_block_data[LinkKeys::Block] || '',
1593
1753
  next_document_filename: next_document_filename,
1594
1754
  next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1595
1755
  )
1596
1756
  end
1597
1757
  end
1598
1758
 
1759
+ # Determines if a given path is absolute or substitutes a placeholder in an expression with the path.
1760
+ # @param path [String] The input path to check or fill in.
1761
+ # @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
1762
+ # @return [String] The absolute path or the expression with the wildcard replaced by the path.
1763
+ def resolve_path_or_substitute(path, expression)
1764
+ if path.include?('/')
1765
+ path
1766
+ else
1767
+ expression.gsub('*', path)
1768
+ end
1769
+ end
1770
+
1599
1771
  def runtime_exception(exception_sym, name, items)
1600
1772
  if @delegate_object[exception_sym] != 0
1601
1773
  data = { name: name, detail: items.join(', ') }
@@ -1615,11 +1787,23 @@ module MarkdownExec
1615
1787
  exit @delegate_object[exception_sym]
1616
1788
  end
1617
1789
 
1618
- def save_to_file(required_lines, selected)
1619
- write_command_file(required_lines, selected)
1790
+ def save_to_file(required_lines:, selected:)
1791
+ write_command_file(required_lines: required_lines, selected: selected)
1620
1792
  @fout.fout "File saved: #{@run_state.saved_filespec}"
1621
1793
  end
1622
1794
 
1795
+ def block_state_for_name_from_cli(block_name)
1796
+ SelectedBlockMenuState.new(
1797
+ @dml_blocks_in_file.find do |item|
1798
+ item[:oname] == block_name
1799
+ end&.merge(
1800
+ block_name_from_cli: true,
1801
+ block_name_from_ui: false
1802
+ ),
1803
+ MenuState::CONTINUE
1804
+ )
1805
+ end
1806
+
1623
1807
  # Select and execute a code block from a Markdown document.
1624
1808
  #
1625
1809
  # This method allows the user to interactively select a code block from a
@@ -1628,52 +1812,109 @@ module MarkdownExec
1628
1812
  # @return [Nil] Returns nil if no code block is selected or an error occurs.
1629
1813
  def document_menu_loop
1630
1814
  @menu_base_options = @delegate_object
1631
- link_state = LinkState.new(
1815
+ @dml_link_state = LinkState.new(
1632
1816
  block_name: @delegate_object[:block_name],
1633
1817
  document_filename: @delegate_object[:filename]
1634
1818
  )
1635
- @run_state.block_name_from_cli = link_state.block_name.present?
1636
- @cli_block_name = link_state.block_name
1637
- now_using_cli = @run_state.block_name_from_cli
1638
- menu_default_dname = nil
1819
+ @run_state.block_name_from_cli = @dml_link_state.block_name.present?
1820
+ @cli_block_name = @dml_link_state.block_name
1821
+ @dml_now_using_cli = @run_state.block_name_from_cli
1822
+ @dml_menu_default_dname = nil
1823
+ @dml_block_state = SelectedBlockMenuState.new
1639
1824
 
1640
1825
  @run_state.batch_random = Random.new.rand
1641
1826
  @run_state.batch_index = 0
1642
1827
 
1643
- loop do
1644
- @run_state.batch_index += 1
1645
- @run_state.in_own_window = false
1828
+ InputSequencer.new(
1829
+ @delegate_object[:filename],
1830
+ @delegate_object[:input_cli_rest]
1831
+ ).run do |msg, data|
1832
+ case msg
1833
+ when :parse_document # once for each menu
1834
+ # puts "@ - parse document #{data}"
1835
+ ii_parse_document(data)
1836
+
1837
+ when :display_menu
1838
+ # warn "@ - display menu:"
1839
+ # ii_display_menu
1840
+ @dml_block_state = SelectedBlockMenuState.new
1841
+ @delegate_object[:block_name] = nil
1842
+
1843
+ when :user_choice
1844
+ # puts "? - Select a block to execute (or type #{$texit} to exit):"
1845
+ break if ii_user_choice == :break # into @dml_block_state
1846
+ break if @dml_block_state.block.nil? # no block matched
1847
+
1848
+ # puts "! - Executing block: #{data}"
1849
+ @dml_block_state.block[:oname]
1850
+
1851
+ when :execute_block
1852
+ block_name = data
1853
+ if block_name == '* Back' ####
1854
+ debounce_reset
1855
+ @menu_user_clicked_back_link = true
1856
+ load_file_link_state = pop_link_history_and_trigger_load
1857
+ @dml_link_state = load_file_link_state.link_state
1858
+
1859
+ InputSequencer.merge_link_state(
1860
+ @dml_link_state,
1861
+ InputSequencer.next_link_state(
1862
+ block_name: @dml_link_state.block_name,
1863
+ document_filename: @dml_link_state.document_filename,
1864
+ prior_block_was_link: true
1865
+ )
1866
+ )
1646
1867
 
1647
- # &bsp 'loop', block_name_from_cli, @cli_block_name
1648
- @run_state.block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc = \
1649
- set_delobj_menu_loop_vars(@run_state.block_name_from_cli, now_using_cli, link_state)
1868
+ else
1869
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1870
+ if @dml_block_state.block[:shell] == BlockType::OPTS
1871
+ debounce_reset
1872
+ link_state = LinkState.new
1873
+ options_state = read_show_options_and_trigger_reuse(
1874
+ selected: @dml_block_state.block,
1875
+ link_state: link_state
1876
+ )
1877
+
1878
+ @menu_base_options.merge!(options_state.options)
1879
+ @delegate_object.merge!(options_state.options)
1880
+ options_state.load_file_link_state.link_state
1881
+ else
1882
+ ii_execute_block(block_name)
1650
1883
 
1651
- # cli or user selection
1652
- #
1653
- block_state = load_cli_or_user_selected_block(blocks_in_file, menu_blocks,
1654
- menu_default_dname)
1655
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1656
- if !block_state
1657
- HashDelegator.error_handler('block_state missing', { abort: true })
1658
- elsif block_state.state == MenuState::EXIT
1659
- # &bsp 'load_cli_or_user_selected_block -> break'
1660
- break
1661
- end
1884
+ if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1885
+ selected: @dml_block_state.block)
1886
+ return :break
1887
+ end
1662
1888
 
1663
- dump_and_warn_block_state(block_state.block)
1664
- link_state, menu_default_dname = exec_bash_next_state(block_state.block, mdoc,
1665
- link_state)
1666
- if prompt_user_exit(@run_state.block_name_from_cli, block_state.block)
1667
- # &bsp 'prompt_user_exit -> break'
1668
- break
1669
- end
1889
+ ## order of block name processing: link block, cli, from user
1890
+ #
1891
+ @cli_block_name = block_name
1892
+ @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1893
+ HashDelegator.next_link_state(
1894
+ block_name_from_cli: !@dml_link_state.block_name,
1895
+ was_using_cli: @dml_now_using_cli,
1896
+ block_state: @dml_block_state,
1897
+ block_name: @dml_link_state.block_name
1898
+ )
1899
+
1900
+ if !@dml_block_state.block[:block_name_from_ui] && cli_break
1901
+ # &bsp '!block_name_from_ui + cli_break -> break'
1902
+ return :break
1903
+ end
1670
1904
 
1671
- link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1672
- HashDelegator.next_link_state(!shift_cli_argument, now_using_cli, block_state)
1905
+ InputSequencer.next_link_state(
1906
+ block_name: @dml_link_state.block_name,
1907
+ prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
1908
+ )
1909
+ end
1910
+ end
1673
1911
 
1674
- if !block_state.block[:block_name_from_ui] && cli_break
1675
- # &bsp '!block_name_from_ui + cli_break -> break'
1676
- break
1912
+ when :exit?
1913
+ data == $texit
1914
+ when :stay?
1915
+ data == $stay
1916
+ else
1917
+ raise "Invalid message: #{msg}"
1677
1918
  end
1678
1919
  end
1679
1920
  rescue StandardError
@@ -1681,23 +1922,66 @@ module MarkdownExec
1681
1922
  { abort: true })
1682
1923
  end
1683
1924
 
1684
- def exec_bash_next_state(block_state_block, mdoc, link_state)
1925
+ def ii_parse_document(_document_filename)
1926
+ @run_state.batch_index += 1
1927
+ @run_state.in_own_window = false
1928
+
1929
+ # &bsp 'loop', block_name_from_cli, @cli_block_name
1930
+ @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
1931
+ set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
1932
+ now_using_cli: @dml_now_using_cli,
1933
+ link_state: @dml_link_state)
1934
+ end
1935
+
1936
+ def ii_user_choice
1937
+ @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1938
+ menu_blocks: @dml_menu_blocks,
1939
+ default: @dml_menu_default_dname)
1940
+ # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1941
+ if !@dml_block_state
1942
+ HashDelegator.error_handler('block_state missing', { abort: true })
1943
+ elsif @dml_block_state.state == MenuState::EXIT
1944
+ # &bsp 'load_cli_or_user_selected_block -> break'
1945
+ :break
1946
+ end
1947
+ end
1948
+
1949
+ def ii_execute_block(block_name)
1950
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1951
+
1952
+ dump_and_warn_block_state(selected: @dml_block_state.block)
1953
+ @dml_link_state, @dml_menu_default_dname = \
1954
+ exec_bash_next_state(
1955
+ selected: @dml_block_state.block,
1956
+ mdoc: @dml_mdoc,
1957
+ link_state: @dml_link_state,
1958
+ block_source: {
1959
+ document_filename: @delegate_object[:filename],
1960
+ time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1961
+ }
1962
+ )
1963
+ end
1964
+
1965
+ def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
1685
1966
  lfls = execute_shell_type(
1686
- block_state_block,
1687
- mdoc,
1688
- link_state,
1689
- block_source: { document_filename: @delegate_object[:filename] }
1967
+ selected: selected,
1968
+ mdoc: mdoc,
1969
+ link_state: link_state,
1970
+ block_source: block_source
1690
1971
  )
1691
1972
 
1692
1973
  # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1693
1974
  [lfls.link_state,
1694
- lfls.load_file == LoadFile::Load ? nil : block_state_block[:dname]]
1975
+ lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
1695
1976
  end
1696
1977
 
1697
- def set_delobj_menu_loop_vars(block_name_from_cli, now_using_cli, link_state)
1978
+ def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
1698
1979
  block_name_from_cli, now_using_cli = \
1699
- manage_cli_selection_state(block_name_from_cli, now_using_cli, link_state)
1700
- set_delob_filename_block_name(link_state, block_name_from_cli)
1980
+ manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
1981
+ now_using_cli: now_using_cli,
1982
+ link_state: link_state)
1983
+ set_delob_filename_block_name(link_state: link_state,
1984
+ block_name_from_cli: block_name_from_cli)
1701
1985
 
1702
1986
  # update @delegate_object and @menu_base_options in auto_load
1703
1987
  #
@@ -1709,14 +1993,14 @@ module MarkdownExec
1709
1993
 
1710
1994
  # user prompt to exit if the menu will be displayed again
1711
1995
  #
1712
- def prompt_user_exit(block_name_from_cli, block_state_block)
1996
+ def prompt_user_exit(block_name_from_cli:, selected:)
1713
1997
  !block_name_from_cli &&
1714
- block_state_block[:shell] == BlockType::BASH &&
1998
+ selected[:shell] == BlockType::BASH &&
1715
1999
  @delegate_object[:pause_after_script_execution] &&
1716
2000
  prompt_select_continue == MenuState::EXIT
1717
2001
  end
1718
2002
 
1719
- def manage_cli_selection_state(block_name_from_cli, now_using_cli, link_state)
2003
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
1720
2004
  if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
1721
2005
  # &bsp 'pause cli control, allow user to select block'
1722
2006
  block_name_from_cli = false
@@ -1739,7 +2023,7 @@ module MarkdownExec
1739
2023
  #
1740
2024
  # @param link_state [LinkState] The current link state object.
1741
2025
  # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
1742
- def set_delob_filename_block_name(link_state, block_name_from_cli)
2026
+ def set_delob_filename_block_name(link_state:, block_name_from_cli:)
1743
2027
  @delegate_object[:filename] = link_state.document_filename
1744
2028
  link_state.block_name = @delegate_object[:block_name] =
1745
2029
  block_name_from_cli ? @cli_block_name : link_state.block_name
@@ -1752,9 +2036,7 @@ module MarkdownExec
1752
2036
  # @param menu_blocks [Hash] Hash of menu blocks.
1753
2037
  # @param link_state [LinkState] Current state of the link.
1754
2038
  def dump_delobj(blocks_in_file, menu_blocks, link_state)
1755
- if @delegate_object[:dump_delegate_object]
1756
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object')
1757
- end
2039
+ warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
1758
2040
 
1759
2041
  if @delegate_object[:dump_blocks_in_file]
1760
2042
  warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
@@ -1766,20 +2048,22 @@ module MarkdownExec
1766
2048
  label: 'menu_blocks')
1767
2049
  end
1768
2050
 
2051
+ warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
2052
+ warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
1769
2053
  return unless @delegate_object[:dump_inherited_lines]
1770
2054
 
1771
2055
  warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
1772
2056
  end
1773
2057
 
1774
- def dump_and_warn_block_state(block_state_block)
1775
- if block_state_block.nil?
2058
+ def dump_and_warn_block_state(selected:)
2059
+ if selected.nil?
1776
2060
  Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
1777
2061
  { abort: true })
1778
2062
  end
1779
2063
 
1780
2064
  return unless @delegate_object[:dump_selected_block]
1781
2065
 
1782
- warn block_state_block.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
2066
+ warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
1783
2067
  end
1784
2068
 
1785
2069
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
@@ -1814,14 +2098,20 @@ module MarkdownExec
1814
2098
  end
1815
2099
 
1816
2100
  def set_environment_variables_for_block(selected)
2101
+ code_lines = []
1817
2102
  YAML.load(selected[:body].join("\n"))&.each do |key, value|
1818
2103
  ENV[key] = value.to_s
2104
+
2105
+ require 'shellwords'
2106
+ code_lines.push "#{key}=\"#{Shellwords.escape(value)}\""
2107
+
1819
2108
  next unless @delegate_object[:menu_vars_set_format].present?
1820
2109
 
1821
2110
  formatted_string = format(@delegate_object[:menu_vars_set_format],
1822
2111
  { key: key, value: value })
1823
2112
  print string_send_color(formatted_string, :menu_vars_set_color)
1824
2113
  end
2114
+ code_lines
1825
2115
  end
1826
2116
 
1827
2117
  def should_add_back_option?
@@ -1908,8 +2198,12 @@ module MarkdownExec
1908
2198
  if state[:in_fenced_block]
1909
2199
  ## end of code block
1910
2200
  #
1911
- HashDelegator.update_menu_attrib_yield_selected(state[:fcb], selected_messages, @delegate_object,
1912
- &block)
2201
+ HashDelegator.update_menu_attrib_yield_selected(
2202
+ fcb: state[:fcb],
2203
+ messages: selected_messages,
2204
+ configuration: @delegate_object,
2205
+ &block
2206
+ )
1913
2207
  state[:in_fenced_block] = false
1914
2208
  else
1915
2209
  ## start of code block
@@ -1942,7 +2236,7 @@ module MarkdownExec
1942
2236
  # @param selected [Hash] Selected item from the menu containing a YAML body.
1943
2237
  # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
1944
2238
  # @return [LoadFileLinkState] An instance indicating the next action for loading files.
1945
- def read_show_options_and_trigger_reuse(selected, link_state = LinkState.new)
2239
+ def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
1946
2240
  obj = {}
1947
2241
  data = YAML.load(selected[:body].join("\n"))
1948
2242
  (data || []).each do |key, value|
@@ -1996,7 +2290,7 @@ module MarkdownExec
1996
2290
  end
1997
2291
 
1998
2292
  # Handles the core logic for generating the command file's metadata and content.
1999
- def write_command_file(required_lines, selected)
2293
+ def write_command_file(required_lines:, selected:)
2000
2294
  return unless @delegate_object[:save_executed_script]
2001
2295
 
2002
2296
  time_now = Time.now.utc
@@ -2032,25 +2326,16 @@ module MarkdownExec
2032
2326
  HashDelegator.error_handler('write_command_file')
2033
2327
  end
2034
2328
 
2035
- # Writes required code blocks to a temporary file and sets an environment variable with its path.
2036
- #
2037
- # @param mdoc [Object] The Markdown document object.
2038
- # @param block_name [String] The name of the block to collect code for.
2039
- def write_required_blocks_to_file(mdoc, block_name, temp_file_path, import_filename: nil)
2040
- c1 = if mdoc
2041
- mdoc.collect_recursively_required_code(
2042
- block_name,
2043
- label_format_above: @delegate_object[:shell_code_label_format_above],
2044
- label_format_below: @delegate_object[:shell_code_label_format_below]
2045
- )[:code]
2046
- else
2047
- []
2048
- end
2049
-
2050
- code_blocks = (HashDelegator.read_required_blocks_from_temp_file(import_filename) +
2051
- c1).join("\n")
2052
-
2053
- HashDelegator.write_code_to_file(code_blocks, temp_file_path)
2329
+ def write_inherited_lines_to_file(link_state, link_block_data)
2330
+ save_expr = link_block_data.fetch(LinkKeys::Save, '')
2331
+ if save_expr.present?
2332
+ save_filespec = save_filespec_from_expression(save_expr)
2333
+ File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
2334
+ # TTY::File.create_file save_filespec, HahDelegator.join_code_lines(link_state&.inherited_lines), force: true
2335
+ @delegate_object[:filename]
2336
+ else
2337
+ link_block_data[LinkKeys::File] || @delegate_object[:filename]
2338
+ end
2054
2339
  end
2055
2340
  end
2056
2341
  end
@@ -2063,6 +2348,11 @@ Bundler.require(:default)
2063
2348
  require 'minitest/autorun'
2064
2349
  require 'mocha/minitest'
2065
2350
 
2351
+ ####
2352
+ require_relative 'dev/instance_method_wrapper'
2353
+ # MarkdownExec::HashDelegator.prepend(InstanceMethodWrapper)
2354
+ # MarkdownExec::HashDelegator.singleton_class.prepend(ClassMethodWrapper)
2355
+
2066
2356
  module MarkdownExec
2067
2357
  class TestHashDelegator < Minitest::Test
2068
2358
  def setup
@@ -2094,14 +2384,14 @@ module MarkdownExec
2094
2384
  # Test case for empty body
2095
2385
  def test_push_link_history_and_trigger_load_with_empty_body
2096
2386
  assert_equal LoadFile::Reuse,
2097
- @hd.push_link_history_and_trigger_load([], nil, FCB.new).load_file
2387
+ @hd.push_link_history_and_trigger_load.load_file
2098
2388
  end
2099
2389
 
2100
2390
  # Test case for non-empty body without 'file' key
2101
2391
  def test_push_link_history_and_trigger_load_without_file_key
2102
2392
  body = ["vars:\n KEY: VALUE"]
2103
2393
  assert_equal LoadFile::Reuse,
2104
- @hd.push_link_history_and_trigger_load(body, nil, FCB.new).load_file
2394
+ @hd.push_link_history_and_trigger_load(link_block_body: body).load_file
2105
2395
  end
2106
2396
 
2107
2397
  # Test case for non-empty body with 'file' key
@@ -2111,10 +2401,12 @@ module MarkdownExec
2111
2401
  LinkState.new(block_name: 'sample_block',
2112
2402
  document_filename: 'sample_file',
2113
2403
  inherited_dependencies: {},
2114
- inherited_lines: []))
2404
+ inherited_lines: ['# ', 'KEY="VALUE"']))
2115
2405
  assert_equal expected_result,
2116
- @hd.push_link_history_and_trigger_load(body, nil, FCB.new(block_name: 'sample_block',
2117
- filename: 'sample_file'))
2406
+ @hd.push_link_history_and_trigger_load(
2407
+ link_block_body: body,
2408
+ selected: FCB.new(block_name: 'sample_block', filename: 'sample_file')
2409
+ )
2118
2410
  end
2119
2411
 
2120
2412
  def test_indent_all_lines_with_indent
@@ -2192,7 +2484,7 @@ module MarkdownExec
2192
2484
 
2193
2485
  def test_append_divider_initial
2194
2486
  menu_blocks = []
2195
- @hd.append_divider(menu_blocks, :initial)
2487
+ @hd.append_divider(menu_blocks: menu_blocks, position: :initial)
2196
2488
 
2197
2489
  assert_equal 1, menu_blocks.size
2198
2490
  assert_equal 'Formatted Divider', menu_blocks.first.dname
@@ -2200,7 +2492,7 @@ module MarkdownExec
2200
2492
 
2201
2493
  def test_append_divider_final
2202
2494
  menu_blocks = []
2203
- @hd.append_divider(menu_blocks, :final)
2495
+ @hd.append_divider(menu_blocks: menu_blocks, position: :final)
2204
2496
 
2205
2497
  assert_equal 1, menu_blocks.size
2206
2498
  assert_equal 'Formatted Divider', menu_blocks.last.dname
@@ -2209,7 +2501,7 @@ module MarkdownExec
2209
2501
  def test_append_divider_without_format
2210
2502
  @hd.instance_variable_set(:@delegate_object, {})
2211
2503
  menu_blocks = []
2212
- @hd.append_divider(menu_blocks, :initial)
2504
+ @hd.append_divider(menu_blocks: menu_blocks, position: :initial)
2213
2505
 
2214
2506
  assert_empty menu_blocks
2215
2507
  end
@@ -2280,9 +2572,9 @@ module MarkdownExec
2280
2572
  def test_collect_required_code_lines_with_vars
2281
2573
  YAML.stubs(:load).returns({ 'key' => 'value' })
2282
2574
  @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] })
2283
- result = @hd.collect_required_code_lines(@mdoc, @selected, block_source: {})
2575
+ result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, block_source: {})
2284
2576
 
2285
- assert_equal ['code line'], result
2577
+ assert_equal ['code line', 'key="value"'], result
2286
2578
  end
2287
2579
  end
2288
2580
 
@@ -2299,7 +2591,7 @@ module MarkdownExec
2299
2591
  @hd.instance_variable_set(:@delegate_object,
2300
2592
  { block_name: 'block1' })
2301
2593
 
2302
- result = @hd.load_cli_or_user_selected_block(all_blocks, [], nil)
2594
+ result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks)
2303
2595
 
2304
2596
  assert_equal all_blocks.first.merge(block_name_from_ui: false), result.block
2305
2597
  assert_nil result.state
@@ -2310,7 +2602,7 @@ module MarkdownExec
2310
2602
  :some_state)
2311
2603
  @hd.stubs(:wait_for_user_selected_block).returns(block_state)
2312
2604
 
2313
- result = @hd.load_cli_or_user_selected_block([], [], nil)
2605
+ result = @hd.load_cli_or_user_selected_block
2314
2606
 
2315
2607
  assert_equal block_state.block.merge(block_name_from_ui: true), result.block
2316
2608
  assert_equal :some_state, result.state
@@ -2437,7 +2729,7 @@ module MarkdownExec
2437
2729
  @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_tail).returns('Footer')
2438
2730
  @hd.instance_variable_get(:@fout).expects(:fout).times(4)
2439
2731
 
2440
- @hd.display_required_code(required_lines)
2732
+ @hd.display_required_code(required_lines: required_lines)
2441
2733
 
2442
2734
  # Verifying that fout is called for each line and for header & footer
2443
2735
  assert true # Placeholder for actual test assertions
@@ -2647,7 +2939,7 @@ module MarkdownExec
2647
2939
  stream = StringIO.new("line 1\nline 2\n")
2648
2940
  file_type = :stdout
2649
2941
 
2650
- Thread.new { @hd.handle_stream(stream, file_type) }
2942
+ Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
2651
2943
 
2652
2944
  @hd.wait_for_stream_processing
2653
2945
 
@@ -2660,7 +2952,7 @@ module MarkdownExec
2660
2952
  file_type = :stdout
2661
2953
  stream.stubs(:each_line).raises(IOError)
2662
2954
 
2663
- Thread.new { @hd.handle_stream(stream, file_type) }
2955
+ Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
2664
2956
 
2665
2957
  @hd.wait_for_stream_processing
2666
2958
 
@@ -2824,13 +3116,13 @@ module MarkdownExec
2824
3116
  HashDelegator.expects(:default_block_title_from_body).with(@fcb)
2825
3117
  Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], {})
2826
3118
 
2827
- HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message])
3119
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
2828
3120
  end
2829
3121
 
2830
3122
  def test_update_menu_attrib_yield_selected_without_body
2831
3123
  @fcb.stubs(:body).returns(nil)
2832
3124
  HashDelegator.expects(:initialize_fcb_names).with(@fcb)
2833
- HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message])
3125
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
2834
3126
  end
2835
3127
  end
2836
3128
 
@@ -2901,4 +3193,30 @@ module MarkdownExec
2901
3193
  refute block_called
2902
3194
  end
2903
3195
  end
3196
+
3197
+ def test_resolves_absolute_path
3198
+ absolute_path = '/usr/local/bin'
3199
+ assert_equal '/usr/local/bin', resolve_path_or_substitute(absolute_path, 'prefix/*/suffix')
3200
+ end
3201
+
3202
+ def test_substitutes_wildcard_with_path
3203
+ path = 'bin'
3204
+ expression = 'prefix/*/suffix'
3205
+ expected_result = 'prefix/bin/suffix'
3206
+ assert_equal expected_result, resolve_path_or_substitute(path, expression)
3207
+ end
3208
+
3209
+ def test_handles_path_with_no_separator_as_is
3210
+ path = 'bin'
3211
+ expression = 'prefix*suffix'
3212
+ expected_result = 'prefixbinsuffix'
3213
+ assert_equal expected_result, resolve_path_or_substitute(path, expression)
3214
+ end
3215
+
3216
+ def test_returns_expression_unchanged_for_empty_path
3217
+ path = ''
3218
+ expression = 'prefix/*/suffix'
3219
+ expected_result = 'prefix/*/suffix'
3220
+ assert_equal expected_result, resolve_path_or_substitute(path, expression)
3221
+ end
2904
3222
  end # module MarkdownExec