markdown_exec 2.0.4 → 2.0.6

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.
@@ -200,7 +200,7 @@ module HashDelegatorSelf
200
200
 
201
201
  # Determine the state of breaker based on was_using_cli and the block type
202
202
  # true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block[:shell] equals BlockType::BASH. In all other scenarios, breaker is false.
203
- breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH
203
+ breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
204
204
 
205
205
  # Reset block_name_from_cli if the conditions are not met
206
206
  block_name_from_cli ||= false
@@ -232,14 +232,20 @@ module HashDelegatorSelf
232
232
  FileUtils.rm_f(path)
233
233
  end
234
234
 
235
- # Evaluates the given string as Ruby code and rescues any StandardErrors.
235
+ # Evaluates the given string as Ruby code within a safe context.
236
236
  # If an error occurs, it calls the error_handler method with 'safeval'.
237
237
  # @param str [String] The string to be evaluated.
238
238
  # @return [Object] The result of evaluating the string.
239
239
  def safeval(str)
240
- eval(str)
240
+ result = nil
241
+ binding.eval("result = #{str}")
242
+
243
+ result
241
244
  rescue StandardError # catches NameError, StandardError
245
+ pp $!, $@
246
+ pp "code: #{str}"
242
247
  error_handler('safeval')
248
+ exit 1
243
249
  end
244
250
 
245
251
  def set_file_permissions(file_path, chmod_value)
@@ -281,11 +287,11 @@ module HashDelegatorSelf
281
287
  File.write(
282
288
  filespec,
283
289
  ["-STDOUT-\n",
284
- format_execution_streams(ExecutionStreams::StdOut, files),
290
+ format_execution_streams(ExecutionStreams::STD_OUT, files),
285
291
  "-STDERR-\n",
286
- format_execution_streams(ExecutionStreams::StdErr, files),
292
+ format_execution_streams(ExecutionStreams::STD_ERR, files),
287
293
  "-STDIN-\n",
288
- format_execution_streams(ExecutionStreams::StdIn, files),
294
+ format_execution_streams(ExecutionStreams::STD_IN, files),
289
295
  "\n"].join
290
296
  )
291
297
  end
@@ -357,7 +363,7 @@ module PathUtils
357
363
  # @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
358
364
  # @return [String] The absolute path or the expression with the wildcard replaced by the path.
359
365
  def self.resolve_path_or_substitute(path, expression)
360
- if path.include?('/')
366
+ if path.start_with?('/')
361
367
  path
362
368
  else
363
369
  expression.gsub('*', path)
@@ -479,9 +485,21 @@ module MarkdownExec
479
485
  history_state_partition
480
486
  option_name = @delegate_object[:menu_option_back_name]
481
487
  insert_at_top = @delegate_object[:menu_back_at_top]
488
+ when MenuState::EDIT
489
+ option_name = @delegate_object[:menu_option_edit_name]
490
+ insert_at_top = @delegate_object[:menu_load_at_top]
482
491
  when MenuState::EXIT
483
492
  option_name = @delegate_object[:menu_option_exit_name]
484
493
  insert_at_top = @delegate_object[:menu_exit_at_top]
494
+ when MenuState::LOAD
495
+ option_name = @delegate_object[:menu_option_load_name]
496
+ insert_at_top = @delegate_object[:menu_load_at_top]
497
+ when MenuState::SAVE
498
+ option_name = @delegate_object[:menu_option_save_name]
499
+ insert_at_top = @delegate_object[:menu_load_at_top]
500
+ when MenuState::VIEW
501
+ option_name = @delegate_object[:menu_option_view_name]
502
+ insert_at_top = @delegate_object[:menu_load_at_top]
485
503
  end
486
504
 
487
505
  formatted_name = format(@delegate_object[:menu_link_format],
@@ -489,7 +507,7 @@ module MarkdownExec
489
507
  chrome_block = FCB.new(
490
508
  chrome: true,
491
509
  dname: HashDelegator.new(@delegate_object).string_send_color(
492
- formatted_name, :menu_link_color
510
+ formatted_name, :menu_chrome_color
493
511
  ),
494
512
  oname: formatted_name
495
513
  )
@@ -587,6 +605,18 @@ module MarkdownExec
587
605
  HashDelegator.error_handler('blocks_from_nested_files')
588
606
  end
589
607
 
608
+ def block_state_for_name_from_cli(block_name)
609
+ SelectedBlockMenuState.new(
610
+ @dml_blocks_in_file.find do |item|
611
+ item[:oname] == block_name
612
+ end&.merge(
613
+ block_name_from_cli: true,
614
+ block_name_from_ui: false
615
+ ),
616
+ MenuState::CONTINUE
617
+ )
618
+ end
619
+
590
620
  # private
591
621
 
592
622
  def calc_logged_stdout_filename(block_name:)
@@ -695,14 +725,14 @@ module MarkdownExec
695
725
  '-c', command,
696
726
  @delegate_object[:filename],
697
727
  *args) do |stdin, stdout, stderr, exec_thr|
698
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
728
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
699
729
  yield nil, line, nil, exec_thr if block_given?
700
730
  end
701
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
731
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
702
732
  yield nil, nil, line, exec_thr if block_given?
703
733
  end
704
734
 
705
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
735
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
706
736
  stdin.puts(line)
707
737
  yield line, nil, nil, exec_thr if block_given?
708
738
  end
@@ -720,34 +750,17 @@ module MarkdownExec
720
750
  @run_state.aborted_at = Time.now.utc
721
751
  @run_state.error_message = err.message
722
752
  @run_state.error = err
723
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
753
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
724
754
  @fout.fout "Error ENOENT: #{err.inspect}"
725
755
  rescue SignalException => err
726
756
  # Handle SignalException
727
757
  @run_state.aborted_at = Time.now.utc
728
758
  @run_state.error_message = 'SIGTERM'
729
759
  @run_state.error = err
730
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
760
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
731
761
  @fout.fout "Error ENOENT: #{err.inspect}"
732
762
  end
733
763
 
734
- def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
735
- if @delegate_object[:block_name].present?
736
- block = all_blocks.find do |item|
737
- item[:oname] == @delegate_object[:block_name]
738
- end&.merge(block_name_from_ui: false)
739
- else
740
- block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
741
- default)
742
- block = block_state.block&.merge(block_name_from_ui: true)
743
- state = block_state.state
744
- end
745
-
746
- SelectedBlockMenuState.new(block, state)
747
- rescue StandardError
748
- HashDelegator.error_handler('load_cli_or_user_selected_block')
749
- end
750
-
751
764
  # This method is responsible for handling the execution of generic blocks in a markdown document.
752
765
  # It collects the required code lines from the document and, depending on the configuration,
753
766
  # may display the code for user approval before execution. It then executes the approved block.
@@ -769,7 +782,12 @@ module MarkdownExec
769
782
  execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
770
783
 
771
784
  link_state.block_name = nil
772
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
785
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
786
+ end
787
+
788
+ # Check if the expression contains wildcard characters
789
+ def contains_wildcards?(expr)
790
+ expr.match(%r{\*|\?|\[})
773
791
  end
774
792
 
775
793
  def copy_to_clipboard(required_lines)
@@ -920,6 +938,312 @@ module MarkdownExec
920
938
  @delegate_object[:logged_stdout_filespec])
921
939
  end
922
940
 
941
+ # Select and execute a code block from a Markdown document.
942
+ #
943
+ # This method allows the user to interactively select a code block from a
944
+ # Markdown document, obtain approval, and execute the chosen block of code.
945
+ #
946
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
947
+ def document_inpseq
948
+ @menu_base_options = @delegate_object
949
+ @dml_link_state = LinkState.new(
950
+ block_name: @delegate_object[:block_name],
951
+ document_filename: @delegate_object[:filename]
952
+ )
953
+ @run_state.block_name_from_cli = @dml_link_state.block_name.present?
954
+ @cli_block_name = @dml_link_state.block_name
955
+ @dml_now_using_cli = @run_state.block_name_from_cli
956
+ @dml_menu_default_dname = nil
957
+ @dml_block_state = SelectedBlockMenuState.new
958
+ @doc_saved_lines_files = []
959
+
960
+ ## load file with code lines per options
961
+ #
962
+ if @menu_base_options[:load_code].present?
963
+ @dml_link_state.inherited_lines = []
964
+ @menu_base_options[:load_code].split(':').map do |path|
965
+ @dml_link_state.inherited_lines += File.readlines(path, chomp: true)
966
+ end
967
+
968
+ inherited_block_names = []
969
+ inherited_dependencies = {}
970
+ selected = { oname: 'load_code' }
971
+ pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
972
+ end
973
+
974
+ item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
975
+ item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
976
+ item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
977
+ item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
978
+ item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
979
+
980
+ @run_state.batch_random = Random.new.rand
981
+ @run_state.batch_index = 0
982
+
983
+ InputSequencer.new(
984
+ @delegate_object[:filename],
985
+ @delegate_object[:input_cli_rest]
986
+ ).run do |msg, data|
987
+ case msg
988
+ when :parse_document # once for each menu
989
+ # puts "@ - parse document #{data}"
990
+ inpseq_parse_document(data)
991
+
992
+ if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
993
+
994
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
995
+ files = sf ? Dir.glob(sf) : []
996
+ @doc_saved_lines_files = files.count.positive? ? files : []
997
+
998
+ lines_count = @dml_link_state.inherited_lines&.count || 0
999
+
1000
+ # add menu items (glob, load, save) and enable selectively
1001
+ menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
1002
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD)
1003
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT)
1004
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), lines_count, 'lines', menu_state: MenuState::SAVE)
1005
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), lines_count, 'lines', menu_state: MenuState::VIEW)
1006
+ end
1007
+
1008
+ when :display_menu
1009
+ # warn "@ - display menu:"
1010
+ # ii_display_menu
1011
+ @dml_block_state = SelectedBlockMenuState.new
1012
+ @delegate_object[:block_name] = nil
1013
+
1014
+ when :user_choice
1015
+ if @dml_link_state.block_name.present?
1016
+ # @prior_block_was_link = true
1017
+ @dml_block_state.block = @dml_blocks_in_file.find do |item|
1018
+ item[:oname] == @dml_link_state.block_name
1019
+ end
1020
+ @dml_link_state.block_name = nil
1021
+ else
1022
+ # puts "? - Select a block to execute (or type #{$texit} to exit):"
1023
+ break if inpseq_user_choice == :break # into @dml_block_state
1024
+ break if @dml_block_state.block.nil? # no block matched
1025
+ end
1026
+ # puts "! - Executing block: #{data}"
1027
+ # @dml_block_state.block[:oname]
1028
+ @dml_block_state.block&.fetch(:oname, nil)
1029
+
1030
+ when :execute_block
1031
+ case (block_name = data)
1032
+ when item_back
1033
+ debounce_reset
1034
+ @menu_user_clicked_back_link = true
1035
+ load_file_link_state = pop_link_history_and_trigger_load
1036
+ @dml_link_state = load_file_link_state.link_state
1037
+
1038
+ InputSequencer.merge_link_state(
1039
+ @dml_link_state,
1040
+ InputSequencer.next_link_state(
1041
+ block_name: @dml_link_state.block_name,
1042
+ document_filename: @dml_link_state.document_filename,
1043
+ prior_block_was_link: true
1044
+ )
1045
+ )
1046
+
1047
+ when item_edit
1048
+ edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
1049
+ @dml_link_state.inherited_lines = edited.split("\n") if edited
1050
+ InputSequencer.next_link_state(prior_block_was_link: true)
1051
+
1052
+ when item_load
1053
+ debounce_reset
1054
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1055
+ load_filespec = load_filespec_from_expression(sf)
1056
+ if load_filespec
1057
+ @dml_link_state.inherited_lines ||= []
1058
+ @dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
1059
+ end
1060
+ InputSequencer.next_link_state(prior_block_was_link: true)
1061
+
1062
+ when item_save
1063
+ debounce_reset
1064
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1065
+ save_filespec = save_filespec_from_expression(sf)
1066
+ if save_filespec && !write_file_with_directory_creation(
1067
+ save_filespec,
1068
+ HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
1069
+ )
1070
+ return :break
1071
+
1072
+ end
1073
+
1074
+ InputSequencer.next_link_state(prior_block_was_link: true)
1075
+
1076
+ when item_view
1077
+ warn @dml_link_state.inherited_lines.join("\n")
1078
+ InputSequencer.next_link_state(prior_block_was_link: true)
1079
+
1080
+ else
1081
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1082
+ if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
1083
+ debounce_reset
1084
+ link_state = LinkState.new
1085
+ options_state = read_show_options_and_trigger_reuse(
1086
+ selected: @dml_block_state.block,
1087
+ link_state: link_state
1088
+ )
1089
+
1090
+ @menu_base_options.merge!(options_state.options)
1091
+ @delegate_object.merge!(options_state.options)
1092
+ options_state.load_file_link_state.link_state
1093
+ else
1094
+ inpseq_execute_block(block_name)
1095
+
1096
+ if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1097
+ selected: @dml_block_state.block)
1098
+ return :break
1099
+ end
1100
+
1101
+ ## order of block name processing: link block, cli, from user
1102
+ #
1103
+ @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1104
+ HashDelegator.next_link_state(
1105
+ block_name: @dml_link_state.block_name,
1106
+ block_name_from_cli: !@dml_link_state.block_name.present?,
1107
+ block_state: @dml_block_state,
1108
+ was_using_cli: @dml_now_using_cli
1109
+ )
1110
+
1111
+ if !@dml_block_state.block[:block_name_from_ui] && cli_break
1112
+ # &bsp '!block_name_from_ui + cli_break -> break'
1113
+ return :break
1114
+ end
1115
+
1116
+ InputSequencer.next_link_state(
1117
+ block_name: @dml_link_state.block_name,
1118
+ prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
1119
+ )
1120
+ end
1121
+ end
1122
+
1123
+ when :exit?
1124
+ data == $texit
1125
+ when :stay?
1126
+ data == $stay
1127
+ else
1128
+ raise "Invalid message: #{msg}"
1129
+ end
1130
+ end
1131
+ rescue StandardError
1132
+ HashDelegator.error_handler('document_inpseq',
1133
+ { abort: true })
1134
+ end
1135
+
1136
+ # remove leading "./"
1137
+ # replace characters: / : . * (space) with: (underscore)
1138
+ def document_name_in_glob_as_file_name(document_filename, glob)
1139
+ return document_filename if document_filename.nil? || document_filename.empty?
1140
+
1141
+ format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
1142
+ end
1143
+
1144
+ def dump_and_warn_block_state(selected:)
1145
+ if selected.nil?
1146
+ Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
1147
+ { abort: true })
1148
+ end
1149
+
1150
+ return unless @delegate_object[:dump_selected_block]
1151
+
1152
+ warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
1153
+ end
1154
+
1155
+ # Outputs warnings based on the delegate object's configuration
1156
+ #
1157
+ # @param delegate_object [Hash] The delegate object containing configuration flags.
1158
+ # @param blocks_in_file [Hash] Hash of blocks present in the file.
1159
+ # @param menu_blocks [Hash] Hash of menu blocks.
1160
+ # @param link_state [LinkState] Current state of the link.
1161
+ def dump_delobj(blocks_in_file, menu_blocks, link_state)
1162
+ warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
1163
+
1164
+ if @delegate_object[:dump_blocks_in_file]
1165
+ warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
1166
+ label: 'blocks_in_file')
1167
+ end
1168
+
1169
+ if @delegate_object[:dump_menu_blocks]
1170
+ warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
1171
+ label: 'menu_blocks')
1172
+ end
1173
+
1174
+ warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
1175
+ warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
1176
+ return unless @delegate_object[:dump_inherited_lines]
1177
+
1178
+ warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
1179
+ end
1180
+
1181
+ # Opens text in an editor for user modification and returns the modified text.
1182
+ #
1183
+ # This method reads the provided text, opens it in the default editor,
1184
+ # and allows the user to modify it. If the user makes changes, the
1185
+ # modified text is returned. If the user exits the editor without
1186
+ # making changes or the editor is closed abruptly, appropriate messages
1187
+ # are displayed.
1188
+ #
1189
+ # @param [String] initial_text The initial text to be edited.
1190
+ # @param [String] temp_name The base name for the temporary file (default: 'edit_text').
1191
+ # @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly.
1192
+ def edit_text(initial_text, temp_name: 'edit_text')
1193
+ # Create a temporary file to store the initial text
1194
+ temp_file = Tempfile.new(temp_name)
1195
+ temp_file.write(initial_text)
1196
+ temp_file.rewind
1197
+
1198
+ # Capture the modification time of the temporary file before editing
1199
+ before_mtime = temp_file.mtime
1200
+
1201
+ # Open the temporary file in the default editor
1202
+ system("#{ENV['EDITOR'] || 'vi'} #{temp_file.path}")
1203
+
1204
+ # Capture the exit status of the editor
1205
+ editor_exit_status = $?.exitstatus
1206
+
1207
+ # Reopen the file to ensure the updated modification time is read
1208
+ temp_file.open
1209
+ after_mtime = temp_file.mtime
1210
+
1211
+ # Check if the editor was exited normally or was interrupted
1212
+ if editor_exit_status != 0
1213
+ warn 'The editor was closed abruptly. No changes were made.'
1214
+ temp_file.close
1215
+ temp_file.unlink
1216
+ return
1217
+ end
1218
+
1219
+ result_text = nil
1220
+ # Read the file if it was modified
1221
+ if before_mtime != after_mtime
1222
+ temp_file.rewind
1223
+ result_text = temp_file.read
1224
+ end
1225
+
1226
+ # Remove the temporary file
1227
+ temp_file.close
1228
+ temp_file.unlink
1229
+
1230
+ result_text
1231
+ end
1232
+
1233
+ def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
1234
+ lfls = execute_shell_type(
1235
+ selected: selected,
1236
+ mdoc: mdoc,
1237
+ link_state: link_state,
1238
+ block_source: block_source
1239
+ )
1240
+
1241
+ # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1242
+ [lfls.link_state,
1243
+ lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
1244
+ #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1245
+ end
1246
+
923
1247
  # Executes a block of code that has been approved for execution.
924
1248
  # It sets the script block name, writes command files if required, and handles the execution
925
1249
  # including output formatting and summarization.
@@ -977,7 +1301,7 @@ module MarkdownExec
977
1301
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
978
1302
  next_block_name: '',
979
1303
  next_document_filename: @delegate_object[:filename],
980
- next_load_file: LoadFile::Reuse
1304
+ next_load_file: LoadFile::REUSE
981
1305
  )
982
1306
 
983
1307
  elsif selected[:shell] == BlockType::VARS
@@ -993,7 +1317,7 @@ module MarkdownExec
993
1317
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
994
1318
  next_block_name: '',
995
1319
  next_document_filename: @delegate_object[:filename],
996
- next_load_file: LoadFile::Reuse
1320
+ next_load_file: LoadFile::REUSE
997
1321
  )
998
1322
 
999
1323
  elsif debounce_allows
@@ -1003,7 +1327,7 @@ module MarkdownExec
1003
1327
  block_source: block_source)
1004
1328
 
1005
1329
  else
1006
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
1330
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
1007
1331
  end
1008
1332
  end
1009
1333
 
@@ -1030,6 +1354,23 @@ module MarkdownExec
1030
1354
  color_sym: :script_execution_frame_color)
1031
1355
  end
1032
1356
 
1357
+ # Format expression using environment variables and run state
1358
+ def format_expression(expr)
1359
+ data = link_load_format_data
1360
+ ENV.each { |key, value| data[key] = value }
1361
+ format(expr, data)
1362
+ end
1363
+
1364
+ # Formats multiline body content as a title string.
1365
+ # indents all but first line with two spaces so it displays correctly in menu
1366
+ # @param body_lines [Array<String>] The lines of body content.
1367
+ # @return [String] Formatted title.
1368
+ def format_multiline_body_as_title(body_lines)
1369
+ body_lines.map.with_index do |line, index|
1370
+ index.zero? ? line : " #{line}"
1371
+ end.join("\n") + "\n"
1372
+ end
1373
+
1033
1374
  # Formats a string based on a given context and applies color styling to it.
1034
1375
  # It retrieves format and color information from the delegate object and processes accordingly.
1035
1376
  #
@@ -1046,10 +1387,15 @@ module MarkdownExec
1046
1387
  string_send_color(formatted_string, color_sym)
1047
1388
  end
1048
1389
 
1049
- # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1050
- # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1051
- #
1052
- # @param fcb [Object] An object representing a functional code block.
1390
+ # Expand expression if it contains format specifiers
1391
+ def formatted_expression(expr)
1392
+ expr.include?('%{') ? format_expression(expr) : expr
1393
+ end
1394
+
1395
+ # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1396
+ # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1397
+ #
1398
+ # @param fcb [Object] An object representing a functional code block.
1053
1399
  # @return [Object] The modified functional code block with updated summary attributes.
1054
1400
  def get_block_summary(fcb)
1055
1401
  return fcb unless @delegate_object[:bash]
@@ -1080,16 +1426,6 @@ module MarkdownExec
1080
1426
  fcb
1081
1427
  end
1082
1428
 
1083
- # Formats multiline body content as a title string.
1084
- # indents all but first line with two spaces so it displays correctly in menu
1085
- # @param body_lines [Array<String>] The lines of body content.
1086
- # @return [String] Formatted title.
1087
- def format_multiline_body_as_title(body_lines)
1088
- body_lines.map.with_index do |line, index|
1089
- index.zero? ? line : " #{line}"
1090
- end.join("\n") + "\n"
1091
- end
1092
-
1093
1429
  # Updates the delegate object's state based on the provided block state.
1094
1430
  # It sets the block name and determines if the user clicked the back link in the menu.
1095
1431
  #
@@ -1142,6 +1478,46 @@ module MarkdownExec
1142
1478
  }
1143
1479
  end
1144
1480
 
1481
+ def inpseq_execute_block(block_name)
1482
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1483
+
1484
+ dump_and_warn_block_state(selected: @dml_block_state.block)
1485
+ @dml_link_state, @dml_menu_default_dname = \
1486
+ exec_bash_next_state(
1487
+ selected: @dml_block_state.block,
1488
+ mdoc: @dml_mdoc,
1489
+ link_state: @dml_link_state,
1490
+ block_source: {
1491
+ document_filename: @delegate_object[:filename],
1492
+ time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1493
+ }
1494
+ )
1495
+ end
1496
+
1497
+ def inpseq_parse_document(_document_filename)
1498
+ @run_state.batch_index += 1
1499
+ @run_state.in_own_window = false
1500
+
1501
+ # &bsp 'loop', block_name_from_cli, @cli_block_name
1502
+ @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
1503
+ set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
1504
+ now_using_cli: @dml_now_using_cli,
1505
+ link_state: @dml_link_state)
1506
+ end
1507
+
1508
+ def inpseq_user_choice
1509
+ @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1510
+ menu_blocks: @dml_menu_blocks,
1511
+ default: @dml_menu_default_dname)
1512
+ # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1513
+ if !@dml_block_state
1514
+ HashDelegator.error_handler('block_state missing', { abort: true })
1515
+ elsif @dml_block_state.state == MenuState::EXIT
1516
+ # &bsp 'load_cli_or_user_selected_block -> break'
1517
+ :break
1518
+ end
1519
+ end
1520
+
1145
1521
  # Iterates through blocks in a file, applying the provided block to each line.
1146
1522
  # The iteration only occurs if the file exists.
1147
1523
  # @yield [Symbol] :filter Yields to obtain selected messages for processing.
@@ -1168,18 +1544,18 @@ module MarkdownExec
1168
1544
  file.write(all_code.join("\n"))
1169
1545
  file.rewind
1170
1546
 
1171
- if link_block_data.fetch(LinkKeys::Exec, false)
1547
+ if link_block_data.fetch(LinkKeys::EXEC, false)
1172
1548
  @run_state.files = Hash.new([])
1173
1549
 
1174
1550
  Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
1175
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
1551
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
1176
1552
  output_lines.push(line)
1177
1553
  end
1178
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
1554
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
1179
1555
  output_lines.push(line)
1180
1556
  end
1181
1557
 
1182
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
1558
+ in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
1183
1559
  stdin.puts(line)
1184
1560
  end
1185
1561
 
@@ -1245,102 +1621,6 @@ module MarkdownExec
1245
1621
  )
1246
1622
  end
1247
1623
 
1248
- # format + glob + select for file in load block
1249
- # name has references to ENV vars and doc and batch vars incl. timestamp
1250
- def load_filespec_from_expression(expression)
1251
- # Process expression with embedded formatting
1252
- expanded_expression = formatted_expression(expression)
1253
-
1254
- # Handle wildcards or direct file specification
1255
- if contains_wildcards?(expanded_expression)
1256
- load_filespec_wildcard_expansion(expanded_expression)
1257
- else
1258
- expanded_expression
1259
- end
1260
- end
1261
-
1262
- def save_filespec_from_expression(expression)
1263
- # Process expression with embedded formatting
1264
- formatted = formatted_expression(expression)
1265
-
1266
- # Handle wildcards or direct file specification
1267
- if contains_wildcards?(formatted)
1268
- save_filespec_wildcard_expansion(formatted)
1269
- else
1270
- formatted
1271
- end
1272
- end
1273
-
1274
- # private
1275
-
1276
- # Expand expression if it contains format specifiers
1277
- def formatted_expression(expr)
1278
- expr.include?('%{') ? format_expression(expr) : expr
1279
- end
1280
-
1281
- # Format expression using environment variables and run state
1282
- def format_expression(expr)
1283
- data = link_load_format_data
1284
- ENV.each { |key, value| data[key] = value }
1285
- format(expr, data)
1286
- end
1287
-
1288
- # Check if the expression contains wildcard characters
1289
- def contains_wildcards?(expr)
1290
- expr.match(%r{\*|\?|\[})
1291
- end
1292
-
1293
- # Handle expression with wildcard characters
1294
- def load_filespec_wildcard_expansion(expr)
1295
- files = find_files(expr)
1296
- case files.count
1297
- when 0
1298
- HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
1299
- when 1
1300
- files.first
1301
- else
1302
- prompt_select_code_filename(files)
1303
- end
1304
- end
1305
-
1306
- # Handle expression with wildcard characters
1307
- # allow user to select or enter
1308
- def puts_gets_oprompt_(filespec)
1309
- puts format(@delegate_object[:prompt_show_expr_format],
1310
- { expr: filespec })
1311
- puts @delegate_object[:prompt_enter_filespec]
1312
- gets.chomp
1313
- end
1314
-
1315
- # prompt user to enter a path (i.e. containing a path separator)
1316
- # or name to substitute into the wildcard expression
1317
- def prompt_for_filespec_with_wildcard(filespec)
1318
- puts format(@delegate_object[:prompt_show_expr_format],
1319
- { expr: filespec })
1320
- puts @delegate_object[:prompt_enter_filespec]
1321
- PathUtils.resolve_path_or_substitute(gets.chomp, filespec)
1322
- end
1323
-
1324
- # Handle expression with wildcard characters
1325
- # allow user to select or enter
1326
- def save_filespec_wildcard_expansion(filespec)
1327
- files = find_files(filespec)
1328
- case files.count
1329
- when 0
1330
- prompt_for_filespec_with_wildcard(filespec)
1331
- else
1332
- ## user selects from existing files or other
1333
- # input into path with wildcard for easy entry
1334
- #
1335
- name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
1336
- if name == @delegate_object[:prompt_filespec_other]
1337
- prompt_for_filespec_with_wildcard(filespec)
1338
- else
1339
- name
1340
- end
1341
- end
1342
- end
1343
-
1344
1624
  def link_load_format_data
1345
1625
  {
1346
1626
  batch_index: @run_state.batch_index,
@@ -1353,29 +1633,6 @@ module MarkdownExec
1353
1633
  }
1354
1634
  end
1355
1635
 
1356
- # # Loads auto link block.
1357
- # def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
1358
- # block_name = @delegate_object[:document_load_link_block_name]
1359
- # return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1360
-
1361
- # block = HashDelegator.block_find(all_blocks, :oname, block_name)
1362
- # return unless block
1363
-
1364
- # if block.fetch(:shell, '') != BlockType::LINK
1365
- # HashDelegator.error_handler('must be Link block type', { abort: true })
1366
-
1367
- # else
1368
- # # debounce_reset
1369
- # push_link_history_and_trigger_load(
1370
- # link_block_body: block.fetch(:body, ''),
1371
- # mdoc: mdoc,
1372
- # selected: block,
1373
- # link_state: link_state,
1374
- # block_source: block_source
1375
- # )
1376
- # end
1377
- # end
1378
-
1379
1636
  # Loads auto blocks based on delegate object settings and updates if new filename is detected.
1380
1637
  # Executes a specified block once per filename.
1381
1638
  # @param all_blocks [Array] Array of all block elements.
@@ -1395,6 +1652,55 @@ module MarkdownExec
1395
1652
  true
1396
1653
  end
1397
1654
 
1655
+ def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
1656
+ if @delegate_object[:block_name].present?
1657
+ block = all_blocks.find do |item|
1658
+ item[:oname] == @delegate_object[:block_name]
1659
+ end&.merge(block_name_from_ui: false)
1660
+ else
1661
+ block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
1662
+ default)
1663
+ block = block_state.block&.merge(block_name_from_ui: true)
1664
+ state = block_state.state
1665
+ end
1666
+
1667
+ SelectedBlockMenuState.new(block, state)
1668
+ rescue StandardError
1669
+ HashDelegator.error_handler('load_cli_or_user_selected_block')
1670
+ end
1671
+
1672
+ # format + glob + select for file in load block
1673
+ # name has references to ENV vars and doc and batch vars incl. timestamp
1674
+ def load_filespec_from_expression(expression)
1675
+ # Process expression with embedded formatting
1676
+ expanded_expression = formatted_expression(expression)
1677
+
1678
+ # Handle wildcards or direct file specification
1679
+ if contains_wildcards?(expanded_expression)
1680
+ load_filespec_wildcard_expansion(expanded_expression)
1681
+ else
1682
+ expanded_expression
1683
+ end
1684
+ end
1685
+ # Handle expression with wildcard characters
1686
+ def load_filespec_wildcard_expansion(expr, auto_load_single: false)
1687
+ files = find_files(expr)
1688
+ if files.count.zero?
1689
+ HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
1690
+ elsif auto_load_single && files.count == 1
1691
+ files.first
1692
+ else
1693
+ ## user selects from existing files or other
1694
+ #
1695
+ case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
1696
+ when @delegate_object[:prompt_filespec_back]
1697
+ # do nothing
1698
+ else
1699
+ name
1700
+ end
1701
+ end
1702
+ end
1703
+
1398
1704
  def mdoc_and_blocks_from_nested_files
1399
1705
  menu_blocks = blocks_from_nested_files
1400
1706
  mdoc = MDoc.new(menu_blocks) do |nopts|
@@ -1411,15 +1717,41 @@ module MarkdownExec
1411
1717
  # recreate menu with new options
1412
1718
  #
1413
1719
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
1414
- # load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
1415
1720
 
1416
1721
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1417
1722
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
1418
1723
  ### compress empty lines
1419
- HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true
1724
+ HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
1420
1725
  [all_blocks, menu_blocks, mdoc]
1421
1726
  end
1422
1727
 
1728
+ def menu_add_disabled_option(name)
1729
+ raise unless name.present?
1730
+ raise if @dml_menu_blocks.nil?
1731
+
1732
+ block = @dml_menu_blocks.find { |item| item[:oname] == name }
1733
+
1734
+ # create menu item when it is needed (count > 0)
1735
+ #
1736
+ return unless block.nil?
1737
+
1738
+ # append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD)
1739
+ chrome_block = FCB.new(
1740
+ chrome: true,
1741
+ disabled: '',
1742
+ dname: HashDelegator.new(@delegate_object).string_send_color(
1743
+ name, :menu_inherited_lines_color
1744
+ ),
1745
+ oname: formatted_name
1746
+ )
1747
+
1748
+ if insert_at_top
1749
+ @dml_menu_blocks.unshift(chrome_block)
1750
+ else
1751
+ @dml_menu_blocks.push(chrome_block)
1752
+ end
1753
+ end
1754
+
1423
1755
  # Formats and optionally colors a menu option based on delegate object's configuration.
1424
1756
  # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
1425
1757
  # @return [String] The formatted and possibly colored value of the menu option.
@@ -1444,6 +1776,47 @@ module MarkdownExec
1444
1776
  end
1445
1777
  end
1446
1778
 
1779
+ def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
1780
+ raise unless name.present?
1781
+ raise if @dml_menu_blocks.nil?
1782
+
1783
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
1784
+
1785
+ # create menu item when it is needed (count > 0)
1786
+ #
1787
+ if item.nil? && count.positive?
1788
+ append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
1789
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
1790
+ end
1791
+
1792
+ # update item if it exists
1793
+ #
1794
+ return unless item
1795
+
1796
+ item[:dname] = "#{name} (#{count} #{type})"
1797
+ if count.positive?
1798
+ item.delete(:disabled)
1799
+ else
1800
+ item[:disabled] = ''
1801
+ end
1802
+ end
1803
+
1804
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
1805
+ if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
1806
+ # &bsp 'pause cli control, allow user to select block'
1807
+ block_name_from_cli = false
1808
+ now_using_cli = false
1809
+ @menu_base_options[:block_name] = \
1810
+ @delegate_object[:block_name] = \
1811
+ link_state.block_name = \
1812
+ @cli_block_name = nil
1813
+ end
1814
+
1815
+ @delegate_object = @menu_base_options.dup
1816
+ @menu_user_clicked_back_link = false
1817
+ [block_name_from_cli, now_using_cli]
1818
+ end
1819
+
1447
1820
  # If a method is missing, treat it as a key for the @delegate_object.
1448
1821
  def method_missing(method_name, *args, &block)
1449
1822
  if @delegate_object.respond_to?(method_name)
@@ -1525,7 +1898,7 @@ module MarkdownExec
1525
1898
  @link_history.push(next_state)
1526
1899
 
1527
1900
  next_state.block_name = nil
1528
- LoadFileLinkState.new(LoadFile::Load, next_state)
1901
+ LoadFileLinkState.new(LoadFile::LOAD, next_state)
1529
1902
  else
1530
1903
  # no history exists; must have been called independently => retain script
1531
1904
  link_history_push_and_next(
@@ -1534,11 +1907,11 @@ module MarkdownExec
1534
1907
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1535
1908
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1536
1909
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1537
- next_block_name: '', # not link_block_data[LinkKeys::Block] || ''
1910
+ next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || ''
1538
1911
  next_document_filename: @delegate_object[:filename], # not next_document_filename
1539
- next_load_file: LoadFile::Reuse # not next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1912
+ next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
1540
1913
  )
1541
- # LoadFileLinkState.new(LoadFile::Reuse, link_state)
1914
+ # LoadFileLinkState.new(LoadFile::REUSE, link_state)
1542
1915
  end
1543
1916
  end
1544
1917
 
@@ -1549,7 +1922,7 @@ module MarkdownExec
1549
1922
  def pop_link_history_and_trigger_load
1550
1923
  pop = @link_history.pop
1551
1924
  peek = @link_history.peek
1552
- LoadFileLinkState.new(LoadFile::Load, LinkState.new(
1925
+ LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
1553
1926
  document_filename: pop.document_filename,
1554
1927
  inherited_block_names: peek.inherited_block_names,
1555
1928
  inherited_dependencies: peek.inherited_dependencies,
@@ -1663,6 +2036,24 @@ module MarkdownExec
1663
2036
  exit 1
1664
2037
  end
1665
2038
 
2039
+ # Prompts the user to enter a path or name to substitute into the wildcard expression.
2040
+ # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
2041
+ #
2042
+ # @param filespec [String] the wildcard expression to be substituted
2043
+ # @return [String, nil] the resolved path or substituted expression, or nil if interrupted
2044
+ def prompt_for_filespec_with_wildcard(filespec)
2045
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
2046
+ puts @delegate_object[:prompt_enter_filespec]
2047
+
2048
+ begin
2049
+ input = gets.chomp
2050
+ PathUtils.resolve_path_or_substitute(input, filespec)
2051
+ rescue Interrupt
2052
+ puts "\nOperation interrupted. Returning nil."
2053
+ nil
2054
+ end
2055
+ end
2056
+
1666
2057
  ##
1667
2058
  # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1668
2059
  # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
@@ -1706,38 +2097,47 @@ module MarkdownExec
1706
2097
  exit 1
1707
2098
  end
1708
2099
 
1709
- def prompt_select_continue
1710
- sel = @prompt.select(
1711
- string_send_color(@delegate_object[:prompt_after_script_execution],
2100
+ # public
2101
+
2102
+ def prompt_select_code_filename(filenames)
2103
+ @prompt.select(
2104
+ string_send_color(@delegate_object[:prompt_select_code_file],
1712
2105
  :prompt_color_after_script_execution),
1713
2106
  filter: true,
1714
2107
  quiet: true
1715
2108
  ) do |menu|
1716
- menu.choice @delegate_object[:prompt_yes]
1717
- menu.choice @delegate_object[:prompt_exit]
2109
+ filenames.each do |filename|
2110
+ menu.choice filename
2111
+ end
1718
2112
  end
1719
- sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1720
2113
  rescue TTY::Reader::InputInterrupt
1721
2114
  exit 1
1722
2115
  end
1723
2116
 
1724
- # public
1725
-
1726
- def prompt_select_code_filename(filenames)
1727
- @prompt.select(
1728
- string_send_color(@delegate_object[:prompt_select_code_file],
2117
+ def prompt_select_continue
2118
+ sel = @prompt.select(
2119
+ string_send_color(@delegate_object[:prompt_after_script_execution],
1729
2120
  :prompt_color_after_script_execution),
1730
2121
  filter: true,
1731
2122
  quiet: true
1732
2123
  ) do |menu|
1733
- filenames.each do |filename|
1734
- menu.choice filename
1735
- end
2124
+ menu.choice @delegate_object[:prompt_yes]
2125
+ menu.choice @delegate_object[:prompt_exit]
1736
2126
  end
2127
+ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1737
2128
  rescue TTY::Reader::InputInterrupt
1738
2129
  exit 1
1739
2130
  end
1740
2131
 
2132
+ # user prompt to exit if the menu will be displayed again
2133
+ #
2134
+ def prompt_user_exit(block_name_from_cli:, selected:)
2135
+ !block_name_from_cli &&
2136
+ selected[:shell] == BlockType::BASH &&
2137
+ @delegate_object[:pause_after_script_execution] &&
2138
+ prompt_select_continue == MenuState::EXIT
2139
+ end
2140
+
1741
2141
  # Handles the processing of a link block in Markdown Execution.
1742
2142
  # It loads YAML data from the link_block_body content, pushes the state to history,
1743
2143
  # sets environment variables, and decides on the next block to load.
@@ -1766,361 +2166,161 @@ module MarkdownExec
1766
2166
  block_names = []
1767
2167
  code_lines = []
1768
2168
  dependencies = {}
1769
- end
1770
-
1771
- # load key and values from link block into current environment
1772
- #
1773
- if link_block_data[LinkKeys::Vars]
1774
- code_lines.push BashCommentFormatter.format_comment(selected[:oname])
1775
- (link_block_data[LinkKeys::Vars] || []).each do |(key, value)|
1776
- ENV[key] = value.to_s
1777
- code_lines.push(assign_key_value_in_bash(key, value))
1778
- end
1779
- end
1780
-
1781
- ## append blocks loaded, apply LinkKeys::Eval
1782
- #
1783
- if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
1784
- load_filespec = load_filespec_from_expression(load_expr)
1785
- code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
1786
- end
1787
-
1788
- # if an eval link block, evaluate code_lines and return its standard output
1789
- #
1790
- if link_block_data.fetch(LinkKeys::Eval,
1791
- false) || link_block_data.fetch(LinkKeys::Exec, false)
1792
- code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
1793
- end
1794
-
1795
- next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
1796
-
1797
- if link_block_data[LinkKeys::Return]
1798
- pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
1799
- dependencies, selected)
1800
-
1801
- else
1802
- link_history_push_and_next(
1803
- curr_block_name: selected[:oname],
1804
- curr_document_filename: @delegate_object[:filename],
1805
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1806
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1807
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1808
- next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
1809
- nil) || link_block_data[LinkKeys::Block] || '',
1810
- next_document_filename: next_document_filename,
1811
- next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1812
- )
1813
- end
1814
- end
1815
-
1816
- def runtime_exception(exception_sym, name, items)
1817
- if @delegate_object[exception_sym] != 0
1818
- data = { name: name, detail: items.join(', ') }
1819
- warn(
1820
- format(
1821
- @delegate_object.fetch(:exception_format_name, "\n%{name}"),
1822
- data
1823
- ).send(@delegate_object.fetch(:exception_color_name, :red)) +
1824
- format(
1825
- @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
1826
- data
1827
- ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
1828
- )
1829
- end
1830
- return unless (@delegate_object[exception_sym]).positive?
1831
-
1832
- exit @delegate_object[exception_sym]
1833
- end
1834
-
1835
- def save_to_file(required_lines:, selected:)
1836
- write_command_file(required_lines: required_lines, selected: selected)
1837
- @fout.fout "File saved: #{@run_state.saved_filespec}"
1838
- end
1839
-
1840
- def block_state_for_name_from_cli(block_name)
1841
- SelectedBlockMenuState.new(
1842
- @dml_blocks_in_file.find do |item|
1843
- item[:oname] == block_name
1844
- end&.merge(
1845
- block_name_from_cli: true,
1846
- block_name_from_ui: false
1847
- ),
1848
- MenuState::CONTINUE
1849
- )
1850
- end
1851
-
1852
- # Select and execute a code block from a Markdown document.
1853
- #
1854
- # This method allows the user to interactively select a code block from a
1855
- # Markdown document, obtain approval, and execute the chosen block of code.
1856
- #
1857
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1858
- def document_menu_loop
1859
- @menu_base_options = @delegate_object
1860
- @dml_link_state = LinkState.new(
1861
- block_name: @delegate_object[:block_name],
1862
- document_filename: @delegate_object[:filename]
1863
- )
1864
- @run_state.block_name_from_cli = @dml_link_state.block_name.present?
1865
- @cli_block_name = @dml_link_state.block_name
1866
- @dml_now_using_cli = @run_state.block_name_from_cli
1867
- @dml_menu_default_dname = nil
1868
- @dml_block_state = SelectedBlockMenuState.new
1869
-
1870
- @run_state.batch_random = Random.new.rand
1871
- @run_state.batch_index = 0
1872
-
1873
- InputSequencer.new(
1874
- @delegate_object[:filename],
1875
- @delegate_object[:input_cli_rest]
1876
- ).run do |msg, data|
1877
- case msg
1878
- when :parse_document # once for each menu
1879
- # puts "@ - parse document #{data}"
1880
- ii_parse_document(data)
1881
-
1882
- when :display_menu
1883
- # warn "@ - display menu:"
1884
- # ii_display_menu
1885
- @dml_block_state = SelectedBlockMenuState.new
1886
- @delegate_object[:block_name] = nil
1887
-
1888
- when :user_choice
1889
- if @dml_link_state.block_name.present?
1890
- # @prior_block_was_link = true
1891
- @dml_block_state.block = @dml_blocks_in_file.find do |item|
1892
- item[:oname] == @dml_link_state.block_name
1893
- end
1894
- @dml_link_state.block_name = nil
1895
- else
1896
- # puts "? - Select a block to execute (or type #{$texit} to exit):"
1897
- break if ii_user_choice == :break # into @dml_block_state
1898
- break if @dml_block_state.block.nil? # no block matched
1899
- end
1900
- # puts "! - Executing block: #{data}"
1901
- # @dml_block_state.block[:oname]
1902
- @dml_block_state.block&.fetch(:oname, nil)
1903
-
1904
- when :execute_block
1905
- block_name = data
1906
- if block_name == '* Back' ####
1907
- debounce_reset
1908
- @menu_user_clicked_back_link = true
1909
- load_file_link_state = pop_link_history_and_trigger_load
1910
- @dml_link_state = load_file_link_state.link_state
1911
-
1912
- InputSequencer.merge_link_state(
1913
- @dml_link_state,
1914
- InputSequencer.next_link_state(
1915
- block_name: @dml_link_state.block_name,
1916
- document_filename: @dml_link_state.document_filename,
1917
- prior_block_was_link: true
1918
- )
1919
- )
1920
-
1921
- else
1922
- @dml_block_state = block_state_for_name_from_cli(block_name)
1923
- if @dml_block_state.block[:shell] == BlockType::OPTS
1924
- debounce_reset
1925
- link_state = LinkState.new
1926
- options_state = read_show_options_and_trigger_reuse(
1927
- selected: @dml_block_state.block,
1928
- link_state: link_state
1929
- )
1930
-
1931
- @menu_base_options.merge!(options_state.options)
1932
- @delegate_object.merge!(options_state.options)
1933
- options_state.load_file_link_state.link_state
1934
- else
1935
- ii_execute_block(block_name)
1936
-
1937
- if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1938
- selected: @dml_block_state.block)
1939
- return :break
1940
- end
1941
-
1942
- ## order of block name processing: link block, cli, from user
1943
- #
1944
- @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1945
- HashDelegator.next_link_state(
1946
- block_name: @dml_link_state.block_name,
1947
- block_name_from_cli: !@dml_link_state.block_name.present?,
1948
- block_state: @dml_block_state,
1949
- was_using_cli: @dml_now_using_cli
1950
- )
1951
-
1952
- if !@dml_block_state.block[:block_name_from_ui] && cli_break
1953
- # &bsp '!block_name_from_ui + cli_break -> break'
1954
- return :break
1955
- end
1956
-
1957
- InputSequencer.next_link_state(
1958
- block_name: @dml_link_state.block_name,
1959
- prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
1960
- )
1961
- end
1962
- end
1963
-
1964
- when :exit?
1965
- data == $texit
1966
- when :stay?
1967
- data == $stay
1968
- else
1969
- raise "Invalid message: #{msg}"
1970
- end
1971
- end
1972
- rescue StandardError
1973
- HashDelegator.error_handler('document_menu_loop',
1974
- { abort: true })
1975
- end
1976
-
1977
- def ii_parse_document(_document_filename)
1978
- @run_state.batch_index += 1
1979
- @run_state.in_own_window = false
1980
-
1981
- # &bsp 'loop', block_name_from_cli, @cli_block_name
1982
- @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
1983
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
1984
- now_using_cli: @dml_now_using_cli,
1985
- link_state: @dml_link_state)
1986
- end
1987
-
1988
- def ii_user_choice
1989
- @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1990
- menu_blocks: @dml_menu_blocks,
1991
- default: @dml_menu_default_dname)
1992
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1993
- if !@dml_block_state
1994
- HashDelegator.error_handler('block_state missing', { abort: true })
1995
- elsif @dml_block_state.state == MenuState::EXIT
1996
- # &bsp 'load_cli_or_user_selected_block -> break'
1997
- :break
1998
- end
1999
- end
2000
-
2001
- def ii_execute_block(block_name)
2002
- @dml_block_state = block_state_for_name_from_cli(block_name)
2003
-
2004
- dump_and_warn_block_state(selected: @dml_block_state.block)
2005
- @dml_link_state, @dml_menu_default_dname = \
2006
- exec_bash_next_state(
2007
- selected: @dml_block_state.block,
2008
- mdoc: @dml_mdoc,
2009
- link_state: @dml_link_state,
2010
- block_source: {
2011
- document_filename: @delegate_object[:filename],
2012
- time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
2013
- }
2014
- )
2015
- end
2016
-
2017
- def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
2018
- lfls = execute_shell_type(
2019
- selected: selected,
2020
- mdoc: mdoc,
2021
- link_state: link_state,
2022
- block_source: block_source
2023
- )
2169
+ end
2024
2170
 
2025
- # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
2026
- [lfls.link_state,
2027
- lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
2028
- end
2171
+ # load key and values from link block into current environment
2172
+ #
2173
+ if link_block_data[LinkKeys::VARS]
2174
+ code_lines.push BashCommentFormatter.format_comment(selected[:oname])
2175
+ (link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
2176
+ ENV[key] = value.to_s
2177
+ code_lines.push(assign_key_value_in_bash(key, value))
2178
+ end
2179
+ end
2029
2180
 
2030
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2031
- block_name_from_cli, now_using_cli = \
2032
- manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2033
- now_using_cli: now_using_cli,
2034
- link_state: link_state)
2035
- set_delob_filename_block_name(link_state: link_state,
2036
- block_name_from_cli: block_name_from_cli)
2181
+ ## append blocks loaded, apply LinkKeys::EVAL
2182
+ #
2183
+ if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
2184
+ load_filespec = load_filespec_from_expression(load_expr)
2185
+ code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
2186
+ end
2037
2187
 
2038
- # update @delegate_object and @menu_base_options in auto_load
2188
+ # if an eval link block, evaluate code_lines and return its standard output
2039
2189
  #
2040
- blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2041
- dump_delobj(blocks_in_file, menu_blocks, link_state)
2190
+ if link_block_data.fetch(LinkKeys::EVAL,
2191
+ false) || link_block_data.fetch(LinkKeys::EXEC, false)
2192
+ code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
2193
+ end
2042
2194
 
2043
- [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2044
- end
2195
+ next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
2045
2196
 
2046
- # user prompt to exit if the menu will be displayed again
2047
- #
2048
- def prompt_user_exit(block_name_from_cli:, selected:)
2049
- !block_name_from_cli &&
2050
- selected[:shell] == BlockType::BASH &&
2051
- @delegate_object[:pause_after_script_execution] &&
2052
- prompt_select_continue == MenuState::EXIT
2053
- end
2197
+ if link_block_data[LinkKeys::RETURN]
2198
+ pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
2199
+ dependencies, selected)
2054
2200
 
2055
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
2056
- if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2057
- # &bsp 'pause cli control, allow user to select block'
2058
- block_name_from_cli = false
2059
- now_using_cli = false
2060
- @menu_base_options[:block_name] = \
2061
- @delegate_object[:block_name] = \
2062
- link_state.block_name = \
2063
- @cli_block_name = nil
2201
+ else
2202
+ link_history_push_and_next(
2203
+ curr_block_name: selected[:oname],
2204
+ curr_document_filename: @delegate_object[:filename],
2205
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2206
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2207
+ inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2208
+ next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK,
2209
+ nil) || link_block_data[LinkKeys::BLOCK] || '',
2210
+ next_document_filename: next_document_filename,
2211
+ next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
2212
+ )
2064
2213
  end
2065
-
2066
- @delegate_object = @menu_base_options.dup
2067
- @menu_user_clicked_back_link = false
2068
- [block_name_from_cli, now_using_cli]
2069
2214
  end
2070
2215
 
2071
- # Update the block name in the link state and delegate object.
2072
- #
2073
- # This method updates the block name based on whether it was specified
2074
- # through the CLI or derived from the link state.
2075
- #
2076
- # @param link_state [LinkState] The current link state object.
2077
- # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2078
- def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2079
- @delegate_object[:filename] = link_state.document_filename
2080
- link_state.block_name = @delegate_object[:block_name] =
2081
- block_name_from_cli ? @cli_block_name : link_state.block_name
2216
+ # Handle expression with wildcard characters
2217
+ # allow user to select or enter
2218
+ def puts_gets_oprompt_(filespec)
2219
+ puts format(@delegate_object[:prompt_show_expr_format],
2220
+ { expr: filespec })
2221
+ puts @delegate_object[:prompt_enter_filespec]
2222
+ gets.chomp
2082
2223
  end
2083
2224
 
2084
- # Outputs warnings based on the delegate object's configuration
2085
- #
2086
- # @param delegate_object [Hash] The delegate object containing configuration flags.
2087
- # @param blocks_in_file [Hash] Hash of blocks present in the file.
2088
- # @param menu_blocks [Hash] Hash of menu blocks.
2089
- # @param link_state [LinkState] Current state of the link.
2090
- def dump_delobj(blocks_in_file, menu_blocks, link_state)
2091
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
2225
+ # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2226
+ # @param selected [Hash] Selected item from the menu containing a YAML body.
2227
+ # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2228
+ # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2229
+ def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2230
+ obj = {}
2231
+ data = YAML.load(selected[:body].join("\n"))
2232
+ (data || []).each do |key, value|
2233
+ sym_key = key.to_sym
2234
+ obj[sym_key] = value
2092
2235
 
2093
- if @delegate_object[:dump_blocks_in_file]
2094
- warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
2095
- label: 'blocks_in_file')
2236
+ print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2096
2237
  end
2097
2238
 
2098
- if @delegate_object[:dump_menu_blocks]
2099
- warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
2100
- label: 'menu_blocks')
2239
+ link_state.block_name = nil
2240
+ OpenStruct.new(options: obj,
2241
+ load_file_link_state: LoadFileLinkState.new(
2242
+ LoadFile::REUSE, link_state
2243
+ ))
2244
+ end
2245
+
2246
+ # Check if the delegate object responds to a given method.
2247
+ # @param method_name [Symbol] The name of the method to check.
2248
+ # @param include_private [Boolean] Whether to include private methods in the check.
2249
+ # @return [Boolean] true if the delegate object responds to the method, false otherwise.
2250
+ def respond_to?(method_name, include_private = false)
2251
+ if super
2252
+ true
2253
+ elsif @delegate_object.respond_to?(method_name, include_private)
2254
+ true
2255
+ elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
2256
+ true
2257
+ else
2258
+ @delegate_object.respond_to?(method_name, include_private)
2101
2259
  end
2260
+ end
2102
2261
 
2103
- warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
2104
- warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
2105
- return unless @delegate_object[:dump_inherited_lines]
2262
+ def runtime_exception(exception_sym, name, items)
2263
+ if @delegate_object[exception_sym] != 0
2264
+ data = { name: name, detail: items.join(', ') }
2265
+ warn(
2266
+ format(
2267
+ @delegate_object.fetch(:exception_format_name, "\n%{name}"),
2268
+ data
2269
+ ).send(@delegate_object.fetch(:exception_color_name, :red)) +
2270
+ format(
2271
+ @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
2272
+ data
2273
+ ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
2274
+ )
2275
+ end
2276
+ return unless (@delegate_object[exception_sym]).positive?
2106
2277
 
2107
- warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
2278
+ exit @delegate_object[exception_sym]
2108
2279
  end
2109
2280
 
2110
- def dump_and_warn_block_state(selected:)
2111
- if selected.nil?
2112
- Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
2113
- { abort: true })
2281
+ # allow user to select or enter
2282
+ def save_filespec_from_expression(expression)
2283
+ # Process expression with embedded formatting
2284
+ formatted = formatted_expression(expression)
2285
+
2286
+ # Handle wildcards or direct file specification
2287
+ if contains_wildcards?(formatted)
2288
+ save_filespec_wildcard_expansion(formatted)
2289
+ else
2290
+ formatted
2114
2291
  end
2292
+ end
2115
2293
 
2116
- return unless @delegate_object[:dump_selected_block]
2294
+ # Handle expression with wildcard characters
2295
+ # allow user to select or enter
2296
+ def save_filespec_wildcard_expansion(filespec)
2297
+ files = find_files(filespec)
2298
+ case files.count
2299
+ when 0
2300
+ prompt_for_filespec_with_wildcard(filespec)
2301
+ else
2302
+ ## user selects from existing files or other
2303
+ # input into path with wildcard for easy entry
2304
+ #
2305
+ name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
2306
+ case name
2307
+ when @delegate_object[:prompt_filespec_back]
2308
+ # do nothing
2309
+ when @delegate_object[:prompt_filespec_other]
2310
+ prompt_for_filespec_with_wildcard(filespec)
2311
+ else
2312
+ name
2313
+ end
2314
+ end
2315
+ end
2117
2316
 
2118
- warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
2317
+ def save_to_file(required_lines:, selected:)
2318
+ write_command_file(required_lines: required_lines, selected: selected)
2319
+ @fout.fout "File saved: #{@run_state.saved_filespec}"
2119
2320
  end
2120
2321
 
2121
2322
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
2122
2323
  def select_option_with_metadata(prompt_text, names, opts = {})
2123
-
2124
2324
  ## configure to environment
2125
2325
  #
2126
2326
  unless opts[:select_page_height].positive?
@@ -2128,23 +2328,24 @@ module MarkdownExec
2128
2328
  opts[:per_page] = opts[:select_page_height] = [IO.console.winsize[0] - 3, 4].max
2129
2329
  end
2130
2330
 
2331
+ # crashes if all menu options are disabled
2131
2332
  selection = @prompt.select(prompt_text,
2132
2333
  names,
2133
2334
  opts.merge(filter: true))
2134
- item = names.find do |item|
2135
- if item.instance_of?(String)
2136
- item == selection
2137
- else
2335
+ selected_name = names.find do |item|
2336
+ if item.instance_of?(Hash)
2138
2337
  item[:dname] == selection
2338
+ else
2339
+ item == selection
2139
2340
  end
2140
2341
  end
2141
- item = { dname: item } if item.instance_of?(String)
2142
- unless item
2342
+ selected_name = { dname: selected_name } if selected_name.instance_of?(String)
2343
+ unless selected_name
2143
2344
  HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
2144
2345
  exit 1
2145
2346
  end
2146
2347
 
2147
- item.merge(
2348
+ selected_name.merge(
2148
2349
  if selection == menu_chrome_colored_option(:menu_option_back_name)
2149
2350
  { option: selection, shell: BlockType::LINK }
2150
2351
  elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
@@ -2159,6 +2360,35 @@ module MarkdownExec
2159
2360
  HashDelegator.error_handler('select_option_with_metadata')
2160
2361
  end
2161
2362
 
2363
+ # Update the block name in the link state and delegate object.
2364
+ #
2365
+ # This method updates the block name based on whether it was specified
2366
+ # through the CLI or derived from the link state.
2367
+ #
2368
+ # @param link_state [LinkState] The current link state object.
2369
+ # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2370
+ def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2371
+ @delegate_object[:filename] = link_state.document_filename
2372
+ link_state.block_name = @delegate_object[:block_name] =
2373
+ block_name_from_cli ? @cli_block_name : link_state.block_name
2374
+ end
2375
+
2376
+ def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2377
+ block_name_from_cli, now_using_cli = \
2378
+ manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2379
+ now_using_cli: now_using_cli,
2380
+ link_state: link_state)
2381
+ set_delob_filename_block_name(link_state: link_state,
2382
+ block_name_from_cli: block_name_from_cli)
2383
+
2384
+ # update @delegate_object and @menu_base_options in auto_load
2385
+ #
2386
+ blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2387
+ dump_delobj(blocks_in_file, menu_blocks, link_state)
2388
+
2389
+ [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2390
+ end
2391
+
2162
2392
  def set_environment_variables_for_block(selected)
2163
2393
  code_lines = []
2164
2394
  YAML.load(selected[:body].join("\n"))&.each do |key, value|
@@ -2294,27 +2524,6 @@ module MarkdownExec
2294
2524
  end
2295
2525
  end
2296
2526
 
2297
- # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2298
- # @param selected [Hash] Selected item from the menu containing a YAML body.
2299
- # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2300
- # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2301
- def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2302
- obj = {}
2303
- data = YAML.load(selected[:body].join("\n"))
2304
- (data || []).each do |key, value|
2305
- sym_key = key.to_sym
2306
- obj[sym_key] = value
2307
-
2308
- print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2309
- end
2310
-
2311
- link_state.block_name = nil
2312
- OpenStruct.new(options: obj,
2313
- load_file_link_state: LoadFileLinkState.new(
2314
- LoadFile::Reuse, link_state
2315
- ))
2316
- end
2317
-
2318
2527
  def wait_for_stream_processing
2319
2528
  @process_mutex.synchronize do
2320
2529
  @process_cv.wait(@process_mutex)
@@ -2389,14 +2598,34 @@ module MarkdownExec
2389
2598
  HashDelegator.error_handler('write_command_file')
2390
2599
  end
2391
2600
 
2601
+ # Ensure the directory exists before writing the file
2602
+ def write_file_with_directory_creation(save_filespec, content)
2603
+ directory = File.dirname(save_filespec)
2604
+
2605
+ begin
2606
+ FileUtils.mkdir_p(directory)
2607
+ File.write(save_filespec, content)
2608
+ rescue Errno::EACCES
2609
+ warn "Permission denied: Unable to write to file '#{save_filespec}'"
2610
+ nil
2611
+ rescue Errno::EROFS
2612
+ warn "Read-only file system: Unable to write to file '#{save_filespec}'"
2613
+ nil
2614
+ rescue StandardError => err
2615
+ warn "An error occurred while writing to file '#{save_filespec}': #{err.message}"
2616
+ nil
2617
+ end
2618
+ end
2619
+
2620
+ # return next document file name
2392
2621
  def write_inherited_lines_to_file(link_state, link_block_data)
2393
- save_expr = link_block_data.fetch(LinkKeys::Save, '')
2622
+ save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
2394
2623
  if save_expr.present?
2395
2624
  save_filespec = save_filespec_from_expression(save_expr)
2396
2625
  File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
2397
2626
  @delegate_object[:filename]
2398
2627
  else
2399
- link_block_data[LinkKeys::File] || @delegate_object[:filename]
2628
+ link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
2400
2629
  end
2401
2630
  end
2402
2631
  end
@@ -2432,21 +2661,6 @@ module MarkdownExec
2432
2661
 
2433
2662
  def self.next_link_state(*args, **kwargs, &block)
2434
2663
  super
2435
- # result = super
2436
-
2437
- # @logger ||= StdOutErrLogger.new
2438
- # @logger.unknown(
2439
- # HashDelegator.clean_hash_recursively(
2440
- # { "HashDelegator.next_link_state":
2441
- # { 'args': args,
2442
- # 'at': Time.now.strftime('%FT%TZ'),
2443
- # 'for': /[^\/]+:\d+/.match(caller.first)[0],
2444
- # 'kwargs': kwargs,
2445
- # 'return': result } }
2446
- # )
2447
- # )
2448
-
2449
- # result
2450
2664
  end
2451
2665
  end
2452
2666
  end
@@ -2537,21 +2751,21 @@ module MarkdownExec
2537
2751
 
2538
2752
  # Test case for empty body
2539
2753
  def test_push_link_history_and_trigger_load_with_empty_body
2540
- assert_equal LoadFile::Reuse,
2754
+ assert_equal LoadFile::REUSE,
2541
2755
  @hd.push_link_history_and_trigger_load.load_file
2542
2756
  end
2543
2757
 
2544
2758
  # Test case for non-empty body without 'file' key
2545
2759
  def test_push_link_history_and_trigger_load_without_file_key
2546
2760
  body = ["vars:\n KEY: VALUE"]
2547
- assert_equal LoadFile::Reuse,
2761
+ assert_equal LoadFile::REUSE,
2548
2762
  @hd.push_link_history_and_trigger_load(link_block_body: body).load_file
2549
2763
  end
2550
2764
 
2551
2765
  # Test case for non-empty body with 'file' key
2552
2766
  def test_push_link_history_and_trigger_load_with_file_key
2553
2767
  body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
2554
- expected_result = LoadFileLinkState.new(LoadFile::Load,
2768
+ expected_result = LoadFileLinkState.new(LoadFile::LOAD,
2555
2769
  LinkState.new(block_name: 'sample_block',
2556
2770
  document_filename: 'sample_file',
2557
2771
  inherited_dependencies: {},
@@ -2589,8 +2803,9 @@ module MarkdownExec
2589
2803
  end
2590
2804
 
2591
2805
  def test_safeval_rescue_from_error
2592
- HashDelegator.stubs(:error_handler).with('safeval')
2593
- assert_nil HashDelegator.safeval('invalid code')
2806
+ assert_raises(SystemExit) do
2807
+ HashDelegator.safeval('invalid_code_raises_exception')
2808
+ end
2594
2809
  end
2595
2810
 
2596
2811
  def test_set_fcb_title
@@ -2996,7 +3211,7 @@ module MarkdownExec
2996
3211
 
2997
3212
  # Asserting the result is an instance of LoadFileLinkState
2998
3213
  assert_instance_of LoadFileLinkState, result
2999
- assert_equal LoadFile::Load, result.load_file
3214
+ assert_equal LoadFile::LOAD, result.load_file
3000
3215
  assert_nil result.link_state.block_name
3001
3216
  end
3002
3217
  end
@@ -3383,4 +3598,60 @@ module MarkdownExec
3383
3598
  assert_equal expected, BashCommentFormatter.format_comment(input)
3384
3599
  end
3385
3600
  end
3601
+
3602
+ class PromptForFilespecWithWildcardTest < Minitest::Test
3603
+ def setup
3604
+ @delegate_object = {
3605
+ prompt_show_expr_format: 'Current expression: %{expr}',
3606
+ prompt_enter_filespec: 'Please enter a filespec:'
3607
+ }
3608
+ @original_stdin = $stdin
3609
+ end
3610
+
3611
+ def teardown
3612
+ $stdin = @original_stdin
3613
+ end
3614
+
3615
+ def test_prompt_for_filespec_with_normal_input
3616
+ $stdin = StringIO.new("test_input\n")
3617
+ result = prompt_for_filespec_with_wildcard('*.txt')
3618
+ assert_equal 'resolved_path_or_substituted_value', result
3619
+ end
3620
+
3621
+ def test_prompt_for_filespec_with_interruption
3622
+ $stdin = StringIO.new
3623
+ # rubocop disable:Lint/NestedMethodDefinition
3624
+ def $stdin.gets; raise Interrupt; end
3625
+ # rubocop enable:Lint/NestedMethodDefinition
3626
+
3627
+ result = prompt_for_filespec_with_wildcard('*.txt')
3628
+ assert_nil result
3629
+ end
3630
+
3631
+ def test_prompt_for_filespec_with_empty_input
3632
+ $stdin = StringIO.new("\n")
3633
+ result = prompt_for_filespec_with_wildcard('*.txt')
3634
+ assert_equal 'resolved_path_or_substituted_value', result
3635
+ end
3636
+
3637
+ private
3638
+
3639
+ def prompt_for_filespec_with_wildcard(filespec)
3640
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
3641
+ puts @delegate_object[:prompt_enter_filespec]
3642
+
3643
+ begin
3644
+ input = gets.chomp
3645
+ PathUtils.resolve_path_or_substitute(input, filespec)
3646
+ rescue Interrupt
3647
+ nil
3648
+ end
3649
+ end
3650
+
3651
+ module PathUtils
3652
+ def self.resolve_path_or_substitute(input, filespec)
3653
+ 'resolved_path_or_substituted_value' # Placeholder implementation
3654
+ end
3655
+ end
3656
+ end
3386
3657
  end # module MarkdownExec