markdown_exec 1.8.8 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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