markdown_exec 1.8.9 → 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.
@@ -186,7 +183,7 @@ module HashDelegatorSelf
186
183
  end
187
184
 
188
185
  def join_code_lines(lines)
189
- ((lines || [])+ ['']).join("\n")
186
+ ((lines || []) + ['']).join("\n")
190
187
  end
191
188
 
192
189
  def merge_lists(*args)
@@ -195,10 +192,10 @@ module HashDelegatorSelf
195
192
  merged.empty? ? [] : merged
196
193
  end
197
194
 
198
- def next_link_state(block_name_from_cli, was_using_cli, block_state, block_name: nil)
195
+ def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, block_name: nil)
199
196
  # &bsp 'next_link_state', block_name_from_cli, was_using_cli, block_state
200
197
  # Set block_name based on block_name_from_cli
201
- block_name = block_name_from_cli ? @cli_block_name : block_name
198
+ block_name = @cli_block_name if block_name_from_cli
202
199
  # &bsp 'block_name:', block_name
203
200
 
204
201
  # Determine the state of breaker based on was_using_cli and the block type
@@ -214,6 +211,8 @@ module HashDelegatorSelf
214
211
 
215
212
  def parse_yaml_data_from_body(body)
216
213
  body.any? ? YAML.load(body.join("\n")) : {}
214
+ rescue StandardError
215
+ error_handler('parse_yaml_data_from_body', { abort: true })
217
216
  end
218
217
 
219
218
  # Reads required code blocks from a temporary file specified by an environment variable.
@@ -268,21 +267,15 @@ module HashDelegatorSelf
268
267
  # @param fcb [Object] The fcb object whose attributes are to be updated.
269
268
  # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable.
270
269
  # @param block [Block] An optional block to yield to if conditions are met.
271
- def update_menu_attrib_yield_selected(fcb, selected_messages, configuration = {}, &block)
270
+ def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, &block)
272
271
  initialize_fcb_names(fcb)
273
272
  return unless fcb.body
274
273
 
275
274
  default_block_title_from_body(fcb)
276
- MarkdownExec::Filter.yield_to_block_if_applicable(fcb, selected_messages, configuration,
275
+ MarkdownExec::Filter.yield_to_block_if_applicable(fcb, messages, configuration,
277
276
  &block)
278
277
  end
279
278
 
280
- # Writes the provided code blocks to a file.
281
- # @param code_blocks [String] Code blocks to write into the file.
282
- def write_code_to_file(content, path)
283
- File.write(path, content)
284
- end
285
-
286
279
  def write_execution_output_to_file(files, filespec)
287
280
  FileUtils.mkdir_p File.dirname(filespec)
288
281
 
@@ -308,7 +301,6 @@ module HashDelegatorSelf
308
301
  block.call(:line, MarkdownExec::FCB.new(body: [line]))
309
302
  end
310
303
  end
311
- ### require_relative 'hash_delegator_self'
312
304
 
313
305
  # This module provides methods for compacting and converting data structures.
314
306
  module CompactionHelpers
@@ -412,40 +404,37 @@ module MarkdownExec
412
404
  # along with initial and final dividers, based on the delegate object's configuration.
413
405
  #
414
406
  # @param menu_blocks [Array] The array of menu block elements to be modified.
415
- def add_menu_chrome_blocks!(menu_blocks, link_state)
407
+ def add_menu_chrome_blocks!(menu_blocks:, link_state:)
416
408
  return unless @delegate_object[:menu_link_format].present?
417
409
 
418
- if @delegate_object[:menu_with_inherited_lines]
419
- add_inherited_lines(menu_blocks,
420
- link_state)
421
- end
410
+ add_inherited_lines(menu_blocks: menu_blocks, link_state: link_state) if @delegate_object[:menu_with_inherited_lines]
422
411
 
423
412
  # back before exit
424
- add_back_option(menu_blocks) if should_add_back_option?
413
+ add_back_option(menu_blocks: menu_blocks) if should_add_back_option?
425
414
 
426
415
  # exit after other options
427
- 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]
428
417
 
429
- add_dividers(menu_blocks)
418
+ add_dividers(menu_blocks: menu_blocks)
430
419
  end
431
420
 
432
421
  private
433
422
 
434
- def add_back_option(menu_blocks)
435
- 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)
436
425
  end
437
426
 
438
- def add_dividers(menu_blocks)
439
- append_divider(menu_blocks, :initial)
440
- 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)
441
430
  end
442
431
 
443
- def add_exit_option(menu_blocks)
444
- 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)
445
434
  end
446
435
 
447
- def add_inherited_lines(menu_blocks, link_state)
448
- 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)
449
438
  end
450
439
 
451
440
  public
@@ -454,8 +443,8 @@ module MarkdownExec
454
443
  #
455
444
  # @param all_blocks [Array] The current blocks in the menu
456
445
  # @param type [Symbol] The type of chrome block to add (:back or :exit)
457
- def append_chrome_block(menu_blocks, type)
458
- case type
446
+ def append_chrome_block(menu_blocks:, menu_state:)
447
+ case menu_state
459
448
  when MenuState::BACK
460
449
  history_state_partition
461
450
  option_name = @delegate_object[:menu_option_back_name]
@@ -487,7 +476,7 @@ module MarkdownExec
487
476
  #
488
477
  # @param menu_blocks [Array] The array of menu block elements.
489
478
  # @param position [Symbol] The position to insert the divider (:initial or :final).
490
- def append_inherited_lines(menu_blocks, link_state, position: top)
479
+ def append_inherited_lines(menu_blocks:, link_state:, position: top)
491
480
  return unless link_state.inherited_lines.present?
492
481
 
493
482
  insert_at_top = @delegate_object[:menu_inherited_lines_at_top]
@@ -520,7 +509,7 @@ module MarkdownExec
520
509
  #
521
510
  # @param menu_blocks [Array] The array of menu block elements.
522
511
  # @param position [Symbol] The position to insert the divider (:initial or :final).
523
- def append_divider(menu_blocks, position)
512
+ def append_divider(menu_blocks:, position:)
524
513
  return unless divider_formatting_present?(position)
525
514
 
526
515
  divider = create_divider(position)
@@ -542,6 +531,15 @@ module MarkdownExec
542
531
  end
543
532
  end
544
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
+
545
543
  # private
546
544
 
547
545
  # Iterates through nested files to collect various types of blocks, including dividers, tasks, and others.
@@ -603,9 +601,9 @@ module MarkdownExec
603
601
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
604
602
  # @param selected [Hash] The selected block.
605
603
  # @return [Array<String>] Required code blocks as an array of lines.
606
- def collect_required_code_lines(mdoc, selected, link_state = LinkState.new, block_source:)
604
+ def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new)
607
605
  required = mdoc.collect_recursively_required_code(
608
- selected[:nickname] || selected[:oname],
606
+ anyname: selected[:nickname] || selected[:oname],
609
607
  label_format_above: @delegate_object[:shell_code_label_format_above],
610
608
  label_format_below: @delegate_object[:shell_code_label_format_below],
611
609
  block_source: block_source
@@ -621,7 +619,7 @@ module MarkdownExec
621
619
  runtime_exception(:runtime_exception_error_level,
622
620
  'unmet_dependencies, flag: runtime_exception_error_level',
623
621
  required[:unmet_dependencies])
624
- elsif true
622
+ else
625
623
  warn format_and_highlight_dependencies(dependencies,
626
624
  highlight: [@delegate_object[:block_name]])
627
625
  end
@@ -667,14 +665,14 @@ module MarkdownExec
667
665
  '-c', command,
668
666
  @delegate_object[:filename],
669
667
  *args) do |stdin, stdout, stderr, exec_thr|
670
- handle_stream(stdout, ExecutionStreams::StdOut) do |line|
668
+ handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
671
669
  yield nil, line, nil, exec_thr if block_given?
672
670
  end
673
- handle_stream(stderr, ExecutionStreams::StdErr) do |line|
671
+ handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
674
672
  yield nil, nil, line, exec_thr if block_given?
675
673
  end
676
674
 
677
- in_thr = handle_stream($stdin, ExecutionStreams::StdIn) do |line|
675
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
678
676
  stdin.puts(line)
679
677
  yield line, nil, nil, exec_thr if block_given?
680
678
  end
@@ -703,7 +701,7 @@ module MarkdownExec
703
701
  @fout.fout "Error ENOENT: #{err.inspect}"
704
702
  end
705
703
 
706
- 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)
707
705
  if @delegate_object[:block_name].present?
708
706
  block = all_blocks.find do |item|
709
707
  item[:oname] == @delegate_object[:block_name]
@@ -727,18 +725,18 @@ module MarkdownExec
727
725
  # @param mdoc [Object] The markdown document object containing code blocks.
728
726
  # @param selected [Hash] The selected item from the menu to be executed.
729
727
  # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one.
730
- def compile_execute_and_trigger_reuse(mdoc, selected, link_state = nil, block_source:)
731
- 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,
732
730
  block_source: block_source)
733
731
  output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
734
- display_required_code(required_lines) if output_or_approval
732
+ display_required_code(required_lines: required_lines) if output_or_approval
735
733
  allow_execution = if @delegate_object[:user_must_approve]
736
- prompt_for_user_approval(required_lines, selected)
734
+ prompt_for_user_approval(required_lines: required_lines, selected: selected)
737
735
  else
738
736
  true
739
737
  end
740
738
 
741
- execute_required_lines(required_lines, selected) if allow_execution
739
+ execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
742
740
 
743
741
  link_state.block_name = nil
744
742
  LoadFileLinkState.new(LoadFile::Reuse, link_state)
@@ -770,8 +768,8 @@ module MarkdownExec
770
768
  # @param match_data [MatchData] The match data containing named captures for formatting.
771
769
  # @param format_option [String] The format string to be used for the new block.
772
770
  # @param color_method [Symbol] The color method to apply to the block's display name.
773
- def create_and_add_chrome_block(blocks, match_data, format_option,
774
- color_method)
771
+ def create_and_add_chrome_block(blocks:, match_data:, format_option:,
772
+ color_method:)
775
773
  oname = format(format_option,
776
774
  match_data.named_captures.transform_keys(&:to_sym))
777
775
  blocks.push FCB.new(
@@ -804,8 +802,12 @@ module MarkdownExec
804
802
  next
805
803
  end
806
804
 
807
- create_and_add_chrome_block(blocks, mbody, @delegate_object[criteria[:format]],
808
- @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
+ )
809
811
  break
810
812
  end
811
813
  end
@@ -833,9 +835,7 @@ module MarkdownExec
833
835
  return true if @run_state.block_name_from_cli
834
836
 
835
837
  # return false if @prior_execution_block == @delegate_object[:block_name]
836
- if @prior_execution_block == @delegate_object[:block_name]
837
- return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat
838
- end
838
+ return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat if @prior_execution_block == @delegate_object[:block_name]
839
839
 
840
840
  @prior_execution_block = @delegate_object[:block_name]
841
841
  @allowed_execution_block = nil
@@ -869,7 +869,7 @@ module MarkdownExec
869
869
  # It wraps the code lines between a formatted header and tail.
870
870
  #
871
871
  # @param required_lines [Array<String>] The lines of code to be displayed.
872
- def display_required_code(required_lines)
872
+ def display_required_code(required_lines:)
873
873
  output_color_formatted(:script_preview_head,
874
874
  :script_preview_frame_color)
875
875
  required_lines.each { |cb| @fout.fout cb }
@@ -896,10 +896,10 @@ module MarkdownExec
896
896
  #
897
897
  # @param required_lines [Array<String>] The lines of code to be executed.
898
898
  # @param selected [FCB] The selected functional code block object.
899
- def execute_required_lines(required_lines = [], selected = FCB.new)
900
- 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]
901
901
  calc_logged_stdout_filename
902
- format_and_execute_command(required_lines)
902
+ format_and_execute_command(code_lines: required_lines)
903
903
  post_execution_process
904
904
  end
905
905
 
@@ -912,12 +912,14 @@ module MarkdownExec
912
912
  # @param opts [Hash] Options hash containing configuration settings.
913
913
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
914
914
  #
915
- def execute_shell_type(selected, mdoc, link_state = LinkState.new,
916
- block_source:)
915
+ def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new)
917
916
  if selected.fetch(:shell, '') == BlockType::LINK
918
917
  debounce_reset
919
- push_link_history_and_trigger_load(selected.fetch(:body, ''), mdoc, selected,
920
- 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)
921
923
 
922
924
  elsif @menu_user_clicked_back_link
923
925
  debounce_reset
@@ -925,10 +927,29 @@ module MarkdownExec
925
927
 
926
928
  elsif selected[:shell] == BlockType::OPTS
927
929
  debounce_reset
928
- 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
+ #
929
937
  @menu_base_options.merge!(options_state.options)
930
938
  @delegate_object.merge!(options_state.options)
931
- 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
+
932
953
 
933
954
  elsif selected[:shell] == BlockType::VARS
934
955
  debounce_reset
@@ -947,7 +968,9 @@ module MarkdownExec
947
968
  )
948
969
 
949
970
  elsif debounce_allows
950
- 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,
951
974
  block_source: block_source)
952
975
  else
953
976
  LoadFileLinkState.new(LoadFile::Reuse, link_state)
@@ -968,8 +991,8 @@ module MarkdownExec
968
991
  string_send_color(data_string, color_sym)
969
992
  end
970
993
 
971
- def format_and_execute_command(lines)
972
- formatted_command = lines.flatten.join("\n")
994
+ def format_and_execute_command(code_lines:)
995
+ formatted_command = code_lines.flatten.join("\n")
973
996
  @fout.fout fetch_color(data_sym: :script_execution_head,
974
997
  color_sym: :script_execution_frame_color)
975
998
  command_execute(formatted_command, args: @pass_args)
@@ -1052,7 +1075,7 @@ module MarkdownExec
1052
1075
  @menu_user_clicked_back_link = block_state.state == MenuState::BACK
1053
1076
  end
1054
1077
 
1055
- def handle_stream(stream, file_type, swap: false)
1078
+ def handle_stream(stream:, file_type:, swap: false)
1056
1079
  @process_mutex.synchronize do
1057
1080
  Thread.new do
1058
1081
  stream.each_line do |line|
@@ -1106,7 +1129,7 @@ module MarkdownExec
1106
1129
  end
1107
1130
  end
1108
1131
 
1109
- 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:)
1110
1133
  all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines)
1111
1134
 
1112
1135
  if link_block_data.fetch(LinkKeys::Exec, false)
@@ -1117,14 +1140,14 @@ module MarkdownExec
1117
1140
  @delegate_object[:shell],
1118
1141
  '-c', all_code.join("\n")
1119
1142
  ) do |stdin, stdout, stderr, _exec_thr|
1120
- handle_stream(stdout, ExecutionStreams::StdOut) do |line|
1143
+ handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
1121
1144
  output_lines.push(line)
1122
1145
  end
1123
- handle_stream(stderr, ExecutionStreams::StdErr) do |line|
1146
+ handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
1124
1147
  output_lines.push(line)
1125
1148
  end
1126
1149
 
1127
- in_thr = handle_stream($stdin, ExecutionStreams::StdIn) do |line|
1150
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
1128
1151
  stdin.puts(line)
1129
1152
  end
1130
1153
 
@@ -1147,13 +1170,10 @@ module MarkdownExec
1147
1170
  output_lines = `#{all_code.join("\n")}`.split("\n")
1148
1171
  end
1149
1172
 
1150
- unless output_lines
1151
- HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true })
1152
- end
1173
+ HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true }) unless output_lines
1153
1174
 
1154
1175
  label_format_above = @delegate_object[:shell_code_label_format_above]
1155
1176
  label_format_below = @delegate_object[:shell_code_label_format_below]
1156
- block_source = { document_filename: link_state&.document_filename }
1157
1177
 
1158
1178
  [label_format_above && format(label_format_above,
1159
1179
  block_source.merge({ block_name: selected[:oname] }))] +
@@ -1192,20 +1212,126 @@ module MarkdownExec
1192
1212
  )
1193
1213
  end
1194
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
+
1195
1323
  # Loads auto blocks based on delegate object settings and updates if new filename is detected.
1196
1324
  # Executes a specified block once per filename.
1197
1325
  # @param all_blocks [Array] Array of all block elements.
1198
1326
  # @return [Boolean, nil] True if values were modified, nil otherwise.
1199
1327
  def load_auto_blocks(all_blocks)
1200
1328
  block_name = @delegate_object[:document_load_opts_block_name]
1201
- unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1202
- return
1203
- end
1329
+ return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1204
1330
 
1205
1331
  block = HashDelegator.block_find(all_blocks, :oname, block_name)
1206
1332
  return unless block
1207
1333
 
1208
- options_state = read_show_options_and_trigger_reuse(block)
1334
+ options_state = read_show_options_and_trigger_reuse(selected: block)
1209
1335
  @menu_base_options.merge!(options_state.options)
1210
1336
  @delegate_object.merge!(options_state.options)
1211
1337
 
@@ -1231,7 +1357,7 @@ module MarkdownExec
1231
1357
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_blocks(all_blocks)
1232
1358
 
1233
1359
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1234
- add_menu_chrome_blocks!(menu_blocks, link_state)
1360
+ add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
1235
1361
  ### compress empty lines
1236
1362
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true
1237
1363
  [all_blocks, menu_blocks, mdoc]
@@ -1273,13 +1399,6 @@ module MarkdownExec
1273
1399
  end
1274
1400
  end
1275
1401
 
1276
- def shift_cli_argument
1277
- return true unless @menu_base_options[:input_cli_rest].present?
1278
-
1279
- @cli_block_name = @menu_base_options[:input_cli_rest].shift
1280
- false
1281
- end
1282
-
1283
1402
  def output_color_formatted(data_sym, color_sym)
1284
1403
  formatted_string = string_send_color(@delegate_object[data_sym],
1285
1404
  color_sym)
@@ -1502,7 +1621,7 @@ module MarkdownExec
1502
1621
  #
1503
1622
  # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
1504
1623
  ##
1505
- def prompt_for_user_approval(required_lines, selected)
1624
+ def prompt_for_user_approval(required_lines:, selected:)
1506
1625
  # Present a selection menu for user approval.
1507
1626
  sel = @prompt.select(
1508
1627
  string_send_color(@delegate_object[:prompt_approve_block],
@@ -1522,7 +1641,7 @@ module MarkdownExec
1522
1641
  if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1523
1642
  copy_to_clipboard(required_lines)
1524
1643
  elsif sel == MenuOptions::SAVE_SCRIPT
1525
- save_to_file(required_lines, selected)
1644
+ save_to_file(required_lines: required_lines, selected: selected)
1526
1645
  end
1527
1646
 
1528
1647
  sel == MenuOptions::YES
@@ -1547,6 +1666,21 @@ module MarkdownExec
1547
1666
 
1548
1667
  # public
1549
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
+
1550
1684
  # Handles the processing of a link block in Markdown Execution.
1551
1685
  # It loads YAML data from the link_block_body content, pushes the state to history,
1552
1686
  # sets environment variables, and decides on the next block to load.
@@ -1555,18 +1689,18 @@ module MarkdownExec
1555
1689
  # @param mdoc [Object] Markdown document object.
1556
1690
  # @param selected [FCB] Selected code block.
1557
1691
  # @return [LoadFileLinkState] Object indicating the next action for file loading.
1558
- def push_link_history_and_trigger_load(link_block_body, mdoc, selected,
1559
- 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: {})
1560
1694
  link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body)
1561
1695
 
1562
1696
  ## collect blocks specified by block
1563
1697
  #
1564
1698
  if mdoc
1565
1699
  code_info = mdoc.collect_recursively_required_code(
1566
- selected[:oname],
1700
+ anyname: selected[:oname],
1567
1701
  label_format_above: @delegate_object[:shell_code_label_format_above],
1568
1702
  label_format_below: @delegate_object[:shell_code_label_format_below],
1569
- block_source: { document_filename: link_state.document_filename }
1703
+ block_source: block_source
1570
1704
  )
1571
1705
  code_lines = code_info[:code]
1572
1706
  block_names = code_info[:block_names]
@@ -1576,7 +1710,6 @@ module MarkdownExec
1576
1710
  code_lines = []
1577
1711
  dependencies = {}
1578
1712
  end
1579
- next_document_filename = link_block_data[LinkKeys::File] || @delegate_object[:filename]
1580
1713
 
1581
1714
  # load key and values from link block into current environment
1582
1715
  #
@@ -1584,30 +1717,25 @@ module MarkdownExec
1584
1717
  code_lines.push "# #{selected[:oname]}"
1585
1718
  (link_block_data[LinkKeys::Vars] || []).each do |(key, value)|
1586
1719
  ENV[key] = value.to_s
1587
- require 'shellwords'
1588
- code_lines.push "#{key}=\"#{Shellwords.escape(value)}\""
1720
+ code_lines.push(assign_key_value_in_bash(key, value))
1589
1721
  end
1590
1722
  end
1591
1723
 
1592
1724
  ## append blocks loaded, apply LinkKeys::Eval
1593
1725
  #
1594
- if (load_filespec = link_block_data.fetch(LinkKeys::Load, '')).present?
1595
- 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
1596
1729
  end
1597
1730
 
1598
1731
  # if an eval link block, evaluate code_lines and return its standard output
1599
1732
  #
1600
1733
  if link_block_data.fetch(LinkKeys::Eval,
1601
1734
  false) || link_block_data.fetch(LinkKeys::Exec, false)
1602
- code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data)
1735
+ code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
1603
1736
  end
1604
1737
 
1605
- ## write variables
1606
- #
1607
- if (save_filespec = link_block_data.fetch(LinkKeys::Save, '')).present?
1608
- File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
1609
- next_document_filename = @delegate_object[:filename]
1610
- end
1738
+ next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
1611
1739
 
1612
1740
  if link_block_data[LinkKeys::Return]
1613
1741
  pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
@@ -1620,13 +1748,26 @@ module MarkdownExec
1620
1748
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1621
1749
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1622
1750
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1623
- next_block_name: link_block_data.fetch(LinkKeys::NextBlock, nil) || link_block_data[LinkKeys::Block] || '',
1751
+ next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
1752
+ nil) || link_block_data[LinkKeys::Block] || '',
1624
1753
  next_document_filename: next_document_filename,
1625
1754
  next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1626
1755
  )
1627
1756
  end
1628
1757
  end
1629
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
+
1630
1771
  def runtime_exception(exception_sym, name, items)
1631
1772
  if @delegate_object[exception_sym] != 0
1632
1773
  data = { name: name, detail: items.join(', ') }
@@ -1646,11 +1787,23 @@ module MarkdownExec
1646
1787
  exit @delegate_object[exception_sym]
1647
1788
  end
1648
1789
 
1649
- def save_to_file(required_lines, selected)
1650
- write_command_file(required_lines, selected)
1790
+ def save_to_file(required_lines:, selected:)
1791
+ write_command_file(required_lines: required_lines, selected: selected)
1651
1792
  @fout.fout "File saved: #{@run_state.saved_filespec}"
1652
1793
  end
1653
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
+
1654
1807
  # Select and execute a code block from a Markdown document.
1655
1808
  #
1656
1809
  # This method allows the user to interactively select a code block from a
@@ -1659,57 +1812,109 @@ module MarkdownExec
1659
1812
  # @return [Nil] Returns nil if no code block is selected or an error occurs.
1660
1813
  def document_menu_loop
1661
1814
  @menu_base_options = @delegate_object
1662
- link_state = LinkState.new(
1815
+ @dml_link_state = LinkState.new(
1663
1816
  block_name: @delegate_object[:block_name],
1664
1817
  document_filename: @delegate_object[:filename]
1665
1818
  )
1666
- @run_state.block_name_from_cli = link_state.block_name.present?
1667
- @cli_block_name = link_state.block_name
1668
- now_using_cli = @run_state.block_name_from_cli
1669
- 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
1670
1824
 
1671
1825
  @run_state.batch_random = Random.new.rand
1672
1826
  @run_state.batch_index = 0
1673
1827
 
1674
- loop do
1675
- @run_state.batch_index += 1
1676
- @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
+ )
1677
1867
 
1678
- # &bsp 'loop', block_name_from_cli, @cli_block_name
1679
- @run_state.block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc = \
1680
- 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)
1681
1883
 
1682
- # cli or user selection
1683
- #
1684
- block_state = load_cli_or_user_selected_block(blocks_in_file, menu_blocks,
1685
- menu_default_dname)
1686
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1687
- if !block_state
1688
- HashDelegator.error_handler('block_state missing', { abort: true })
1689
- elsif block_state.state == MenuState::EXIT
1690
- # &bsp 'load_cli_or_user_selected_block -> break'
1691
- break
1692
- 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
1693
1888
 
1694
- dump_and_warn_block_state(block_state.block)
1695
- link_state, menu_default_dname = exec_bash_next_state(block_state.block, mdoc,
1696
- link_state)
1697
- if prompt_user_exit(@run_state.block_name_from_cli, block_state.block)
1698
- # &bsp 'prompt_user_exit -> break'
1699
- break
1700
- 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
1701
1904
 
1702
- ## order of block name processing
1703
- # from link block
1704
- # from cli
1705
- # from user
1706
- #
1707
- link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1708
- HashDelegator.next_link_state(!link_state.block_name && !shift_cli_argument, now_using_cli, block_state, block_name: link_state.block_name)
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
1709
1911
 
1710
- if !block_state.block[:block_name_from_ui] && cli_break
1711
- # &bsp '!block_name_from_ui + cli_break -> break'
1712
- break
1912
+ when :exit?
1913
+ data == $texit
1914
+ when :stay?
1915
+ data == $stay
1916
+ else
1917
+ raise "Invalid message: #{msg}"
1713
1918
  end
1714
1919
  end
1715
1920
  rescue StandardError
@@ -1717,23 +1922,66 @@ module MarkdownExec
1717
1922
  { abort: true })
1718
1923
  end
1719
1924
 
1720
- 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: {})
1721
1966
  lfls = execute_shell_type(
1722
- block_state_block,
1723
- mdoc,
1724
- link_state,
1725
- block_source: { document_filename: @delegate_object[:filename] }
1967
+ selected: selected,
1968
+ mdoc: mdoc,
1969
+ link_state: link_state,
1970
+ block_source: block_source
1726
1971
  )
1727
1972
 
1728
1973
  # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1729
1974
  [lfls.link_state,
1730
- lfls.load_file == LoadFile::Load ? nil : block_state_block[:dname]]
1975
+ lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
1731
1976
  end
1732
1977
 
1733
- 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:)
1734
1979
  block_name_from_cli, now_using_cli = \
1735
- manage_cli_selection_state(block_name_from_cli, now_using_cli, link_state)
1736
- 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)
1737
1985
 
1738
1986
  # update @delegate_object and @menu_base_options in auto_load
1739
1987
  #
@@ -1745,14 +1993,14 @@ module MarkdownExec
1745
1993
 
1746
1994
  # user prompt to exit if the menu will be displayed again
1747
1995
  #
1748
- def prompt_user_exit(block_name_from_cli, block_state_block)
1996
+ def prompt_user_exit(block_name_from_cli:, selected:)
1749
1997
  !block_name_from_cli &&
1750
- block_state_block[:shell] == BlockType::BASH &&
1998
+ selected[:shell] == BlockType::BASH &&
1751
1999
  @delegate_object[:pause_after_script_execution] &&
1752
2000
  prompt_select_continue == MenuState::EXIT
1753
2001
  end
1754
2002
 
1755
- 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:)
1756
2004
  if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
1757
2005
  # &bsp 'pause cli control, allow user to select block'
1758
2006
  block_name_from_cli = false
@@ -1775,7 +2023,7 @@ module MarkdownExec
1775
2023
  #
1776
2024
  # @param link_state [LinkState] The current link state object.
1777
2025
  # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
1778
- 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:)
1779
2027
  @delegate_object[:filename] = link_state.document_filename
1780
2028
  link_state.block_name = @delegate_object[:block_name] =
1781
2029
  block_name_from_cli ? @cli_block_name : link_state.block_name
@@ -1788,9 +2036,7 @@ module MarkdownExec
1788
2036
  # @param menu_blocks [Hash] Hash of menu blocks.
1789
2037
  # @param link_state [LinkState] Current state of the link.
1790
2038
  def dump_delobj(blocks_in_file, menu_blocks, link_state)
1791
- if @delegate_object[:dump_delegate_object]
1792
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object')
1793
- end
2039
+ warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
1794
2040
 
1795
2041
  if @delegate_object[:dump_blocks_in_file]
1796
2042
  warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
@@ -1802,20 +2048,22 @@ module MarkdownExec
1802
2048
  label: 'menu_blocks')
1803
2049
  end
1804
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]
1805
2053
  return unless @delegate_object[:dump_inherited_lines]
1806
2054
 
1807
2055
  warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
1808
2056
  end
1809
2057
 
1810
- def dump_and_warn_block_state(block_state_block)
1811
- if block_state_block.nil?
2058
+ def dump_and_warn_block_state(selected:)
2059
+ if selected.nil?
1812
2060
  Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
1813
2061
  { abort: true })
1814
2062
  end
1815
2063
 
1816
2064
  return unless @delegate_object[:dump_selected_block]
1817
2065
 
1818
- warn block_state_block.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
2066
+ warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
1819
2067
  end
1820
2068
 
1821
2069
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
@@ -1950,8 +2198,12 @@ module MarkdownExec
1950
2198
  if state[:in_fenced_block]
1951
2199
  ## end of code block
1952
2200
  #
1953
- HashDelegator.update_menu_attrib_yield_selected(state[:fcb], selected_messages, @delegate_object,
1954
- &block)
2201
+ HashDelegator.update_menu_attrib_yield_selected(
2202
+ fcb: state[:fcb],
2203
+ messages: selected_messages,
2204
+ configuration: @delegate_object,
2205
+ &block
2206
+ )
1955
2207
  state[:in_fenced_block] = false
1956
2208
  else
1957
2209
  ## start of code block
@@ -1984,7 +2236,7 @@ module MarkdownExec
1984
2236
  # @param selected [Hash] Selected item from the menu containing a YAML body.
1985
2237
  # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
1986
2238
  # @return [LoadFileLinkState] An instance indicating the next action for loading files.
1987
- 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)
1988
2240
  obj = {}
1989
2241
  data = YAML.load(selected[:body].join("\n"))
1990
2242
  (data || []).each do |key, value|
@@ -2038,7 +2290,7 @@ module MarkdownExec
2038
2290
  end
2039
2291
 
2040
2292
  # Handles the core logic for generating the command file's metadata and content.
2041
- def write_command_file(required_lines, selected)
2293
+ def write_command_file(required_lines:, selected:)
2042
2294
  return unless @delegate_object[:save_executed_script]
2043
2295
 
2044
2296
  time_now = Time.now.utc
@@ -2074,25 +2326,16 @@ module MarkdownExec
2074
2326
  HashDelegator.error_handler('write_command_file')
2075
2327
  end
2076
2328
 
2077
- # Writes required code blocks to a temporary file and sets an environment variable with its path.
2078
- #
2079
- # @param mdoc [Object] The Markdown document object.
2080
- # @param block_name [String] The name of the block to collect code for.
2081
- def write_required_blocks_to_file(mdoc, block_name, temp_file_path, import_filename: nil)
2082
- c1 = if mdoc
2083
- mdoc.collect_recursively_required_code(
2084
- block_name,
2085
- label_format_above: @delegate_object[:shell_code_label_format_above],
2086
- label_format_below: @delegate_object[:shell_code_label_format_below]
2087
- )[:code]
2088
- else
2089
- []
2090
- end
2091
-
2092
- code_blocks = (HashDelegator.read_required_blocks_from_temp_file(import_filename) +
2093
- c1).join("\n")
2094
-
2095
- 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
2096
2339
  end
2097
2340
  end
2098
2341
  end
@@ -2105,6 +2348,11 @@ Bundler.require(:default)
2105
2348
  require 'minitest/autorun'
2106
2349
  require 'mocha/minitest'
2107
2350
 
2351
+ ####
2352
+ require_relative 'dev/instance_method_wrapper'
2353
+ # MarkdownExec::HashDelegator.prepend(InstanceMethodWrapper)
2354
+ # MarkdownExec::HashDelegator.singleton_class.prepend(ClassMethodWrapper)
2355
+
2108
2356
  module MarkdownExec
2109
2357
  class TestHashDelegator < Minitest::Test
2110
2358
  def setup
@@ -2136,14 +2384,14 @@ module MarkdownExec
2136
2384
  # Test case for empty body
2137
2385
  def test_push_link_history_and_trigger_load_with_empty_body
2138
2386
  assert_equal LoadFile::Reuse,
2139
- @hd.push_link_history_and_trigger_load([], nil, FCB.new).load_file
2387
+ @hd.push_link_history_and_trigger_load.load_file
2140
2388
  end
2141
2389
 
2142
2390
  # Test case for non-empty body without 'file' key
2143
2391
  def test_push_link_history_and_trigger_load_without_file_key
2144
2392
  body = ["vars:\n KEY: VALUE"]
2145
2393
  assert_equal LoadFile::Reuse,
2146
- @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
2147
2395
  end
2148
2396
 
2149
2397
  # Test case for non-empty body with 'file' key
@@ -2155,8 +2403,10 @@ module MarkdownExec
2155
2403
  inherited_dependencies: {},
2156
2404
  inherited_lines: ['# ', 'KEY="VALUE"']))
2157
2405
  assert_equal expected_result,
2158
- @hd.push_link_history_and_trigger_load(body, nil, FCB.new(block_name: 'sample_block',
2159
- 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
+ )
2160
2410
  end
2161
2411
 
2162
2412
  def test_indent_all_lines_with_indent
@@ -2234,7 +2484,7 @@ module MarkdownExec
2234
2484
 
2235
2485
  def test_append_divider_initial
2236
2486
  menu_blocks = []
2237
- @hd.append_divider(menu_blocks, :initial)
2487
+ @hd.append_divider(menu_blocks: menu_blocks, position: :initial)
2238
2488
 
2239
2489
  assert_equal 1, menu_blocks.size
2240
2490
  assert_equal 'Formatted Divider', menu_blocks.first.dname
@@ -2242,7 +2492,7 @@ module MarkdownExec
2242
2492
 
2243
2493
  def test_append_divider_final
2244
2494
  menu_blocks = []
2245
- @hd.append_divider(menu_blocks, :final)
2495
+ @hd.append_divider(menu_blocks: menu_blocks, position: :final)
2246
2496
 
2247
2497
  assert_equal 1, menu_blocks.size
2248
2498
  assert_equal 'Formatted Divider', menu_blocks.last.dname
@@ -2251,7 +2501,7 @@ module MarkdownExec
2251
2501
  def test_append_divider_without_format
2252
2502
  @hd.instance_variable_set(:@delegate_object, {})
2253
2503
  menu_blocks = []
2254
- @hd.append_divider(menu_blocks, :initial)
2504
+ @hd.append_divider(menu_blocks: menu_blocks, position: :initial)
2255
2505
 
2256
2506
  assert_empty menu_blocks
2257
2507
  end
@@ -2322,7 +2572,7 @@ module MarkdownExec
2322
2572
  def test_collect_required_code_lines_with_vars
2323
2573
  YAML.stubs(:load).returns({ 'key' => 'value' })
2324
2574
  @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] })
2325
- result = @hd.collect_required_code_lines(@mdoc, @selected, block_source: {})
2575
+ result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, block_source: {})
2326
2576
 
2327
2577
  assert_equal ['code line', 'key="value"'], result
2328
2578
  end
@@ -2341,7 +2591,7 @@ module MarkdownExec
2341
2591
  @hd.instance_variable_set(:@delegate_object,
2342
2592
  { block_name: 'block1' })
2343
2593
 
2344
- result = @hd.load_cli_or_user_selected_block(all_blocks, [], nil)
2594
+ result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks)
2345
2595
 
2346
2596
  assert_equal all_blocks.first.merge(block_name_from_ui: false), result.block
2347
2597
  assert_nil result.state
@@ -2352,7 +2602,7 @@ module MarkdownExec
2352
2602
  :some_state)
2353
2603
  @hd.stubs(:wait_for_user_selected_block).returns(block_state)
2354
2604
 
2355
- result = @hd.load_cli_or_user_selected_block([], [], nil)
2605
+ result = @hd.load_cli_or_user_selected_block
2356
2606
 
2357
2607
  assert_equal block_state.block.merge(block_name_from_ui: true), result.block
2358
2608
  assert_equal :some_state, result.state
@@ -2479,7 +2729,7 @@ module MarkdownExec
2479
2729
  @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_tail).returns('Footer')
2480
2730
  @hd.instance_variable_get(:@fout).expects(:fout).times(4)
2481
2731
 
2482
- @hd.display_required_code(required_lines)
2732
+ @hd.display_required_code(required_lines: required_lines)
2483
2733
 
2484
2734
  # Verifying that fout is called for each line and for header & footer
2485
2735
  assert true # Placeholder for actual test assertions
@@ -2689,7 +2939,7 @@ module MarkdownExec
2689
2939
  stream = StringIO.new("line 1\nline 2\n")
2690
2940
  file_type = :stdout
2691
2941
 
2692
- Thread.new { @hd.handle_stream(stream, file_type) }
2942
+ Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
2693
2943
 
2694
2944
  @hd.wait_for_stream_processing
2695
2945
 
@@ -2702,7 +2952,7 @@ module MarkdownExec
2702
2952
  file_type = :stdout
2703
2953
  stream.stubs(:each_line).raises(IOError)
2704
2954
 
2705
- Thread.new { @hd.handle_stream(stream, file_type) }
2955
+ Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
2706
2956
 
2707
2957
  @hd.wait_for_stream_processing
2708
2958
 
@@ -2866,13 +3116,13 @@ module MarkdownExec
2866
3116
  HashDelegator.expects(:default_block_title_from_body).with(@fcb)
2867
3117
  Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], {})
2868
3118
 
2869
- HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message])
3119
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
2870
3120
  end
2871
3121
 
2872
3122
  def test_update_menu_attrib_yield_selected_without_body
2873
3123
  @fcb.stubs(:body).returns(nil)
2874
3124
  HashDelegator.expects(:initialize_fcb_names).with(@fcb)
2875
- HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message])
3125
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
2876
3126
  end
2877
3127
  end
2878
3128
 
@@ -2943,4 +3193,30 @@ module MarkdownExec
2943
3193
  refute block_called
2944
3194
  end
2945
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
2946
3222
  end # module MarkdownExec