markdown_exec 1.8.9 → 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.
@@ -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