markdown_exec 2.8.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -1
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +0 -33
  5. data/bats/bats.bats +2 -0
  6. data/bats/block-type-link.bats +1 -1
  7. data/bats/block-type-ux-allowed.bats +2 -2
  8. data/bats/block-type-ux-default.bats +8 -0
  9. data/bats/block-type-ux-invalid.bats +1 -1
  10. data/bats/{block-type-ux-preconditions.bats → block-type-ux-required-variables.bats} +1 -1
  11. data/bats/block-type-ux-row-format.bats +1 -1
  12. data/bats/block-type-ux-sources.bats +36 -0
  13. data/bats/border.bats +1 -1
  14. data/bats/cli.bats +2 -2
  15. data/bats/command-substitution-options.bats +14 -0
  16. data/bats/command-substitution.bats +1 -1
  17. data/bats/fail.bats +5 -2
  18. data/bats/indented-block-type-vars.bats +1 -1
  19. data/bats/markup.bats +1 -1
  20. data/bats/option-expansion.bats +8 -0
  21. data/bats/table-column-truncate.bats +1 -1
  22. data/bats/test_helper.bash +50 -5
  23. data/docs/dev/bats-document-configuration.md +1 -1
  24. data/docs/dev/block-type-ux-allowed.md +5 -5
  25. data/docs/dev/block-type-ux-auto.md +9 -5
  26. data/docs/dev/block-type-ux-chained.md +4 -2
  27. data/docs/dev/block-type-ux-default.md +42 -0
  28. data/docs/dev/block-type-ux-echo-hash.md +6 -1
  29. data/docs/dev/block-type-ux-echo.md +3 -1
  30. data/docs/dev/block-type-ux-exec.md +3 -4
  31. data/docs/dev/block-type-ux-hidden.md +3 -0
  32. data/docs/dev/block-type-ux-require.md +9 -18
  33. data/docs/dev/{block-type-ux-preconditions.md → block-type-ux-required-variables.md} +1 -2
  34. data/docs/dev/block-type-ux-row-format.md +3 -4
  35. data/docs/dev/block-type-ux-sources.md +57 -0
  36. data/docs/dev/block-type-ux-transform.md +0 -4
  37. data/docs/dev/command-substitution-options.md +61 -0
  38. data/docs/dev/indented-block-type-vars.md +1 -0
  39. data/docs/dev/menu-pagination-indent.md +123 -0
  40. data/docs/dev/menu-pagination.md +111 -0
  41. data/docs/dev/option-expansion.md +10 -0
  42. data/lib/ansi_formatter.rb +2 -0
  43. data/lib/block_cache.rb +197 -0
  44. data/lib/command_result.rb +57 -0
  45. data/lib/constants.rb +18 -0
  46. data/lib/error_reporting.rb +38 -0
  47. data/lib/evaluate_shell_expressions.rb +43 -18
  48. data/lib/fcb.rb +114 -11
  49. data/lib/hash_delegator.rb +595 -359
  50. data/lib/markdown_exec/version.rb +1 -1
  51. data/lib/markdown_exec.rb +136 -45
  52. data/lib/mdoc.rb +74 -23
  53. data/lib/menu.src.yml +27 -9
  54. data/lib/menu.yml +23 -8
  55. data/lib/namer.rb +1 -3
  56. data/lib/value_or_exception.rb +76 -0
  57. metadata +18 -4
@@ -22,8 +22,10 @@ require_relative 'array'
22
22
  require_relative 'array_util'
23
23
  require_relative 'block_types'
24
24
  require_relative 'cached_nested_file_reader'
25
+ require_relative 'command_result'
25
26
  require_relative 'constants'
26
27
  require_relative 'directory_searcher'
28
+ require_relative 'error_reporting'
27
29
  require_relative 'evaluate_shell_expressions'
28
30
  require_relative 'exceptions'
29
31
  require_relative 'fcb'
@@ -41,9 +43,11 @@ require_relative 'streams_out'
41
43
  require_relative 'string_util'
42
44
  require_relative 'table_extractor'
43
45
  require_relative 'text_analyzer'
46
+ require_relative 'value_or_exception'
44
47
 
45
48
  $pd = false unless defined?($pd)
46
49
  $table_cell_truncate = true
50
+ EXIT_STATUS_REQUIRED_EMPTY = 248
47
51
 
48
52
  module HashDelegatorSelf
49
53
  # Applies an ANSI color method to a string using a specified color key.
@@ -138,7 +142,8 @@ module HashDelegatorSelf
138
142
  def delete_consecutive_blank_lines!(blocks_menu)
139
143
  blocks_menu.process_and_conditionally_delete! do
140
144
  |prev_item, current_item, _next_item|
141
- prev_item&.fetch(:chrome, nil) &&
145
+ !current_item.is_split? &&
146
+ prev_item&.fetch(:chrome, nil) &&
142
147
  !(prev_item && prev_item.oname.present?) &&
143
148
  current_item&.fetch(:chrome, nil) &&
144
149
  !(current_item && current_item.oname.present?)
@@ -395,9 +400,19 @@ module HashDelegatorSelf
395
400
  end
396
401
 
397
402
  def persist_fcb_self(all_fcbs, options)
398
- fcb = MarkdownExec::FCB.new(options)
399
- all_fcbs << fcb if all_fcbs
400
- fcb
403
+ raise if all_fcbs.nil?
404
+
405
+ # if the id is present, update the existing fcb
406
+ if options[:id]
407
+ fcb = all_fcbs.find { |fcb| fcb.id == options[:id] }
408
+ if fcb
409
+ fcb.update(options)
410
+ return fcb
411
+ end
412
+ end
413
+ MarkdownExec::FCB.new(options).tap do |fcb|
414
+ all_fcbs << fcb
415
+ end
401
416
  end
402
417
  end
403
418
 
@@ -861,30 +876,105 @@ module MarkdownExec
861
876
  when :blocks
862
877
  result = SuccessResult.instance
863
878
  if @delegate_object[:bash]
864
- # prepare block for menu, may fail and call HashDelegator.error_handler
865
- result = fcb.for_menu!(
866
- block_calls_scan: @delegate_object[:block_calls_scan],
867
- block_name_match: @delegate_object[:block_name_match],
868
- block_name_nick_match: @delegate_object[:block_name_nick_match],
869
- id: "#{source_id}_bfnf_b_#{count}",
870
- menu_format: @delegate_object[:menu_ux_row_format],
871
- prompt: @delegate_object[:prompt_ux_enter_a_value],
872
- table_center: @delegate_object[:table_center]
873
- ) do |oname, color|
874
- apply_block_type_color_option(oname, color)
879
+ begin
880
+ mf = MenuFilter.new(@delegate_object)
881
+ if fcb.body.count > 1 && mf.fcb_in_menu?(fcb) && fcb.is_split_displayed?(@delegate_object)
882
+ # make multiple FCB blocks, one for each line; only the first is active
883
+ id_prefix = "#{fcb.id}¤BlkFrmNstFls®block:#{count}©body:"
884
+ fcb0 = fcb
885
+ menu_lines = fcb.body
886
+ menu_lines.each.with_index do |menu_line, index|
887
+ is_enabled_but_inactive = ((index + 1) % (@delegate_object[:select_page_height] / 2)).zero?
888
+ if index.zero?
889
+ # fcb.body = [menu_line]
890
+ # fcb.center = center
891
+ # fcb.collapse = collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse
892
+ # fcb.disabled = disabled ? TtyMenu::DISABLE : nil
893
+ fcb.dname = fcb.indent + menu_line
894
+ fcb.id = "#{id_prefix}#{index}"
895
+ # fcb.indent = line_obj[:indent]
896
+ fcb.is_split_first = true # the first block in a split
897
+ fcb.is_split_rest = false
898
+ # fcb.level = level
899
+ # fcb.oname # computed
900
+ # fcb.s0indent = indent
901
+ fcb.s0printable = menu_line
902
+ fcb.s1decorated = menu_line
903
+ fcb.text = menu_line
904
+ # fcb.token = line_obj[:collapse]
905
+ # fcb.type = type
906
+ else
907
+ fcb = persist_fcb(
908
+ body: fcb0.body,
909
+ center: fcb0.center,
910
+ chrome: true,
911
+ collapse: false,
912
+ disabled: is_enabled_but_inactive ? TtyMenu::ENABLE : TtyMenu::DISABLE,
913
+ dname: fcb0.indent + menu_line,
914
+ id: "#{id_prefix}#{index}",
915
+ indent: fcb0.indent,
916
+ is_enabled_but_inactive: is_enabled_but_inactive,
917
+ is_split_first: false,
918
+ is_split_rest: true, # subsequent blocks in a split
919
+ level: fcb0.level,
920
+ s0indent: fcb0.s0indent,
921
+ s0printable: menu_line,
922
+ s1decorated: menu_line,
923
+ start_line: fcb0.start_line,
924
+ text: menu_line,
925
+ # token: ,
926
+ type: fcb0.type
927
+ )
928
+ end
929
+
930
+ result = fcb.for_menu!(
931
+ block_calls_scan: @delegate_object[:block_calls_scan],
932
+ block_name_match: @delegate_object[:block_name_match],
933
+ block_name_nick_match: @delegate_object[:block_name_nick_match],
934
+ id: fcb.id,
935
+ menu_format: @delegate_object[:menu_ux_row_format],
936
+ prompt: @delegate_object[:prompt_ux_enter_a_value],
937
+ table_center: @delegate_object[:table_center]
938
+ ) do |oname, color|
939
+ apply_block_type_color_option(oname, color)
940
+ end
941
+
942
+ results[fcb.id] = result if result.failure?
943
+ blocks << fcb unless result.failure?
944
+ end
945
+ else
946
+ # prepare block for menu, may fail and call HashDelegator.error_handler
947
+ result = fcb.for_menu!(
948
+ block_calls_scan: @delegate_object[:block_calls_scan],
949
+ block_name_match: @delegate_object[:block_name_match],
950
+ block_name_nick_match: @delegate_object[:block_name_nick_match],
951
+ id: fcb.id,
952
+ menu_format: @delegate_object[:menu_ux_row_format],
953
+ prompt: @delegate_object[:prompt_ux_enter_a_value],
954
+ table_center: @delegate_object[:table_center]
955
+ ) do |oname, color|
956
+ # decorate the displayed line
957
+ apply_block_type_color_option(oname, color)
958
+ end
959
+ results[fcb.id] = result if result.failure?
960
+ blocks << fcb unless result.failure?
961
+ end
962
+ rescue StandardError
963
+ # ww $@, $!
964
+ HashDelegator.error_handler('blocks_from_nested_files',
965
+ { abort: true })
875
966
  end
876
- results[fcb.id] = result if result.failure?
877
967
  else
878
968
  expand_references!(fcb, link_state)
969
+ blocks << fcb unless result.failure?
879
970
  end
880
- blocks << fcb unless result.failure?
881
971
  when :filter # types accepted
882
972
  %i[blocks line]
883
973
  when :line
884
974
  unless @delegate_object[:no_chrome]
885
975
  # expand references only if block is recognized (not a comment)
886
976
  create_and_add_chrome_blocks(
887
- blocks, fcb, id: "#{source_id}_bfnf_l_#{count}", init_ids: init_ids
977
+ blocks, fcb, id: "#{source_id}¤BlkFrmNstFls:#{count}®line", init_ids: init_ids
888
978
  ) do
889
979
  # expand references only if block is recognized (not a comment)
890
980
  expand_references!(fcb, link_state)
@@ -894,7 +984,7 @@ module MarkdownExec
894
984
  end
895
985
  OpenStruct.new(blocks: blocks, results: results)
896
986
  rescue StandardError
897
- # ww $@, $!,
987
+ # ww $@, $!
898
988
  HashDelegator.error_handler('blocks_from_nested_files')
899
989
  end
900
990
 
@@ -907,13 +997,14 @@ module MarkdownExec
907
997
 
908
998
  def build_replacement_dictionary(
909
999
  commands, link_state,
910
- initial_code_required: false, key_format:
1000
+ initial_code_required: false,
1001
+ occurrence_expressions: nil
911
1002
  )
912
1003
  evaluate_shell_expressions(
913
1004
  (link_state&.inherited_lines_block || ''),
914
1005
  commands,
915
1006
  initial_code_required: initial_code_required,
916
- key_format: key_format
1007
+ occurrence_expressions: occurrence_expressions
917
1008
  )
918
1009
  end
919
1010
 
@@ -987,19 +1078,34 @@ module MarkdownExec
987
1078
  ]
988
1079
  end
989
1080
 
990
- # Executes the allowed exec command and processes the output
991
- # @param export [Object] The export configuration object
992
- # @param inherited_code [Array] The inherited code lines
993
- # @param code_lines [Array] The code lines to append to
994
- # @param required [Hash] Required code information
995
- # @return [String, Symbol] The command output or :ux_exec_prohibited if execution failed
996
- def process_allowed_exec(export, inherited_code, code_lines, required)
997
- output = export_exec_with_code(
998
- export, inherited_code, code_lines, required
1081
+ def code_from_automatic_ux_blocks(
1082
+ all_blocks,
1083
+ mdoc
1084
+ )
1085
+ unless @ux_most_recent_filename != @delegate_object[:filename]
1086
+ return
1087
+ end
1088
+
1089
+ blocks = select_automatic_ux_blocks(
1090
+ all_blocks.reject(&:is_split_rest?)
999
1091
  )
1000
- return :ux_exec_prohibited if output == :invalidated
1092
+ return if blocks.empty?
1001
1093
 
1002
- output.split("\n")
1094
+ @ux_most_recent_filename = @delegate_object[:filename]
1095
+
1096
+ (blocks.each.with_object([]) do |block, merged_options|
1097
+ command_result_w_e_t_nl = code_from_ux_block_to_set_environment_variables(
1098
+ block,
1099
+ mdoc,
1100
+ force: @delegate_object[:ux_auto_load_force_default],
1101
+ only_default: true
1102
+ )
1103
+ if command_result_w_e_t_nl.failure?
1104
+ merged_options
1105
+ else
1106
+ merged_options.push(command_result_w_e_t_nl.stdout)
1107
+ end
1108
+ end).to_a
1003
1109
  end
1004
1110
 
1005
1111
  # parse YAML body defining the UX for a single variable
@@ -1017,7 +1123,7 @@ module MarkdownExec
1017
1123
  )
1018
1124
 
1019
1125
  # process each ux block in sequence, setting ENV and collecting lines
1020
- code_lines = []
1126
+ required_lines = []
1021
1127
  required[:blocks].each do |block|
1022
1128
  next unless block.type == BlockType::UX
1023
1129
 
@@ -1028,35 +1134,51 @@ module MarkdownExec
1028
1134
  prompt: @delegate_object[:prompt_ux_enter_a_value],
1029
1135
  validate: '^(?<name>[^ ].*)$'
1030
1136
  )
1137
+ block.export = export
1138
+ block.export_act = FCB.act_source(export)
1139
+ block.export_init = FCB.init_source(export)
1031
1140
 
1032
- # preconditions are variable names that must be set before the UX block is executed.
1141
+ # required are variable names that must be set before the UX block is executed.
1033
1142
  # if any precondition is not set, the sequence is aborted.
1034
- export.preconditions&.each do |precondition|
1035
- code_lines.push "[[ -z $#{precondition} ]] && exit 1"
1143
+ required_variables = []
1144
+ export.required&.each do |precondition|
1145
+ required_variables.push "[[ -z $#{precondition} ]] && exit #{EXIT_STATUS_REQUIRED_EMPTY}"
1036
1146
  end
1037
1147
 
1148
+ eval_code = join_array_of_arrays(
1149
+ inherited_code, # inherited code
1150
+ required_lines, # current block requirements
1151
+ required_variables, # test conditions
1152
+ required[:code] # current block code
1153
+ )
1038
1154
  if only_default
1039
- value, exportable, transformable =
1040
- ux_block_export_automatic(
1041
- export, inherited_code, code_lines, required
1042
- )
1155
+ command_result_w_e_t_nl =
1156
+ ux_block_export_automatic(eval_code, export)
1157
+ # do not display warnings on initializing call
1158
+ return command_result_w_e_t_nl if command_result_w_e_t_nl.failure?
1159
+
1043
1160
  else
1044
- value, exportable, transformable =
1045
- ux_block_export_activated(
1046
- export, inherited_code, code_lines, required, exit_prompt
1047
- )
1161
+ command_result_w_e_t_nl =
1162
+ ux_block_export_activated(eval_code, export, exit_prompt)
1163
+ if command_result_w_e_t_nl.failure?
1164
+ warn command_result_w_e_t_nl.warning if command_result_w_e_t_nl.warning&.present?
1165
+ return command_result_w_e_t_nl
1166
+ end
1048
1167
  end
1049
- return :ux_exec_prohibited if value == :ux_exec_prohibited
1168
+ return command_result_w_e_t_nl if command_result_w_e_t_nl.failure?
1050
1169
 
1051
- if SelectResponse.continue?(value)
1052
- if transformable
1053
- value = transform_export_value(value, export)
1170
+ required_lines.concat(command_result_w_e_t_nl.new_lines)
1171
+ if SelectResponse.continue?(command_result_w_e_t_nl.stdout)
1172
+ if command_result_w_e_t_nl.transformable
1173
+ command_result_w_e_t_nl.stdout = transform_export_value(
1174
+ command_result_w_e_t_nl.stdout, export
1175
+ )
1054
1176
  end
1055
1177
 
1056
- if exportable
1057
- ENV[export.name] = value.to_s
1058
- code_lines.push code_line_safe_assign(export.name, value,
1059
- force: force)
1178
+ if command_result_w_e_t_nl.exportable
1179
+ ENV[export.name] = command_result_w_e_t_nl.stdout.to_s
1180
+ required_lines.push code_line_safe_assign(export.name, command_result_w_e_t_nl.stdout,
1181
+ force: force)
1060
1182
  end
1061
1183
  end
1062
1184
  else
@@ -1064,7 +1186,7 @@ module MarkdownExec
1064
1186
  end
1065
1187
  end
1066
1188
 
1067
- code_lines
1189
+ CommandResult.new(stdout: required_lines)
1068
1190
  end
1069
1191
 
1070
1192
  # sets ENV
@@ -1300,7 +1422,9 @@ module MarkdownExec
1300
1422
  )
1301
1423
  # Initialize a counter for named group occurrences
1302
1424
  occurrence_count = Hash.new(0)
1303
- return occurrence_count if pattern == //
1425
+ occurrence_expressions = {}
1426
+ return [occurrence_count,
1427
+ occurrence_expressions] if pattern.nil? || pattern == //
1304
1428
 
1305
1429
  blocks.each do |block|
1306
1430
  # Skip processing for shell-type blocks
@@ -1309,16 +1433,18 @@ module MarkdownExec
1309
1433
  # Scan each block name for matches of the pattern
1310
1434
  count_named_group_occurrences_block_body_fix_indent(block).scan(pattern) do |(_, _variable_name)|
1311
1435
  pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1312
- occurrence_count[$LAST_MATCH_INFO[group_name]] += 1
1436
+ id = $LAST_MATCH_INFO[group_name]
1437
+ occurrence_count[id] += 1
1438
+ occurrence_expressions[id] = $LAST_MATCH_INFO['expression']
1313
1439
  end
1314
1440
  end
1315
1441
 
1316
- occurrence_count
1442
+ [occurrence_count, occurrence_expressions]
1317
1443
  end
1318
1444
 
1319
1445
  def count_named_group_occurrences_block_body_fix_indent(block)
1320
1446
  ### actually double the entries, but not a problem since it's used as a boolean
1321
- ([block.oname || ''] + block.body).join("\n")
1447
+ ([block.oname || ''] + (block.body || [''])).join("\n")
1322
1448
  end
1323
1449
 
1324
1450
  ##
@@ -1366,9 +1492,9 @@ module MarkdownExec
1366
1492
  # split text with newlines, from variable expansion
1367
1493
  if line_cap[:text].include?("\n")
1368
1494
  lines = line_cap[:text].split("\n")
1369
- line_caps = (lines.map do |line|
1370
- line_cap.dup.merge(text: line)
1371
- end.to_a)
1495
+ line_caps = lines.map do |line|
1496
+ line_cap.dup.merge(text: line)
1497
+ end.to_a
1372
1498
  end
1373
1499
 
1374
1500
  # wrap text on multiple lines to screen width, replacing line_caps
@@ -1376,7 +1502,7 @@ module MarkdownExec
1376
1502
  line_caps = line_caps.flat_map do |line_cap|
1377
1503
  text = line_cap[:text]
1378
1504
  wrapper = StringWrapper.new(width: screen_width_for_wrapping - line_cap[:indent].length)
1379
-
1505
+
1380
1506
  if text.length > screen_width_for_wrapping
1381
1507
  # Wrap this text and create line_cap objects for each part
1382
1508
  wrapper.wrap(text).map do |wrapped_text|
@@ -1428,17 +1554,17 @@ module MarkdownExec
1428
1554
  fcb.center = center
1429
1555
  fcb.chrome = true
1430
1556
  fcb.collapse = collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse
1431
- fcb.token = line_obj[:collapse]
1432
1557
  fcb.disabled = disabled ? TtyMenu::DISABLE : nil
1558
+ fcb.dname = line_obj[:indent] + decorated
1433
1559
  fcb.id = "#{id}.#{index}"
1560
+ fcb.indent = line_obj[:indent]
1434
1561
  fcb.level = level
1562
+ fcb.oname = line_obj[:text]
1435
1563
  fcb.s0indent = indent
1436
1564
  fcb.s0printable = line_obj[:text]
1437
1565
  fcb.s1decorated = decorated
1438
- fcb.dname = line_obj[:indent] + decorated
1439
- fcb.indent = line_obj[:indent]
1440
- fcb.oname = line_obj[:text]
1441
1566
  fcb.text = line_obj[:text]
1567
+ fcb.token = line_obj[:collapse]
1442
1568
  fcb.type = type
1443
1569
  use_fcb = false # next line is new record
1444
1570
  else
@@ -1446,17 +1572,17 @@ module MarkdownExec
1446
1572
  center: center,
1447
1573
  chrome: true,
1448
1574
  collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
1449
- token: line_obj[:collapse],
1450
1575
  disabled: disabled ? TtyMenu::DISABLE : nil,
1576
+ dname: line_obj[:indent] + decorated,
1451
1577
  id: "#{id}.#{index}",
1578
+ indent: line_obj[:indent],
1452
1579
  level: level,
1580
+ oname: line_obj[:text],
1453
1581
  s0indent: indent,
1454
1582
  s0printable: line_obj[:text],
1455
1583
  s1decorated: decorated,
1456
- dname: line_obj[:indent] + decorated,
1457
- indent: line_obj[:indent],
1458
- oname: line_obj[:text],
1459
1584
  text: line_obj[:text],
1585
+ token: line_obj[:collapse],
1460
1586
  type: type
1461
1587
  )
1462
1588
  end
@@ -1486,7 +1612,9 @@ module MarkdownExec
1486
1612
  yield if block_given?
1487
1613
 
1488
1614
  # parse multiline to capture output of variable expansion
1489
- mbody = fcb.body[0].match Regexp.new(@delegate_object[criteria[:match]], Regexp::MULTILINE)
1615
+ mbody = fcb.body[0].match Regexp.new(
1616
+ @delegate_object[criteria[:match]], Regexp::MULTILINE
1617
+ )
1490
1618
  end
1491
1619
 
1492
1620
  create_and_add_chrome_block(
@@ -1888,14 +2016,16 @@ module MarkdownExec
1888
2016
 
1889
2017
  elsif selected.type == BlockType::UX
1890
2018
  debounce_reset
2019
+ command_result_w_e_t_nl = code_from_ux_block_to_set_environment_variables(
2020
+ selected,
2021
+ @dml_mdoc,
2022
+ inherited_code: @dml_link_state.inherited_lines
2023
+ )
2024
+ ### TBD if command_result_w_e_t_nl.failure?
1891
2025
  next_state_append_code(
1892
2026
  selected,
1893
2027
  link_state,
1894
- code_from_ux_block_to_set_environment_variables(
1895
- selected,
1896
- @dml_mdoc,
1897
- inherited_code: @dml_link_state.inherited_lines
1898
- )
2028
+ command_result_w_e_t_nl.failure? ? [] : command_result_w_e_t_nl.stdout
1899
2029
  )
1900
2030
 
1901
2031
  elsif selected.type == BlockType::VARS
@@ -1944,6 +2074,11 @@ module MarkdownExec
1944
2074
  @dml_block_state = find_block_state_by_name(block_name)
1945
2075
  dump_and_warn_block_state(name: block_name,
1946
2076
  selected: @dml_block_state.block)
2077
+ if @dml_block_state.block.fetch(:is_enabled_but_inactive, false)
2078
+ @dml_block_selection = BlockSelection.new(@dml_block_state.block.id)
2079
+ return # do nothing
2080
+ end
2081
+
1947
2082
  next_block_state =
1948
2083
  execute_block_for_state_and_name(
1949
2084
  selected: @dml_block_state.block,
@@ -2117,7 +2252,7 @@ module MarkdownExec
2117
2252
  end
2118
2253
  elsif (selected_option = select_option_with_metadata(
2119
2254
  prompt_title,
2120
- [exit_prompt] + dirs.map do |file|
2255
+ [exit_prompt] + dirs.map do |file| # tty_menu_items
2121
2256
  { name:
2122
2257
  format(
2123
2258
  block_data['view'] || view,
@@ -2130,7 +2265,8 @@ module MarkdownExec
2130
2265
  oname: file }
2131
2266
  end,
2132
2267
  menu_options.merge(
2133
- cycle: true
2268
+ cycle: true,
2269
+ match_dml: false
2134
2270
  )
2135
2271
  ))
2136
2272
  if selected_option.dname != exit_prompt
@@ -2306,9 +2442,9 @@ module MarkdownExec
2306
2442
  def execute_inherited_save(
2307
2443
  code_lines: @dml_link_state.inherited_lines
2308
2444
  )
2309
- return unless (save_filespec = save_filespec_from_expression)
2310
-
2311
- document_name_in_glob_as_file_name
2445
+ return unless (save_filespec = save_filespec_from_expression(
2446
+ document_name_in_glob_as_file_name
2447
+ ))
2312
2448
 
2313
2449
  unless write_file_with_directory_creation(
2314
2450
  content: HashDelegator.join_code_lines(code_lines),
@@ -2364,27 +2500,6 @@ module MarkdownExec
2364
2500
  post_execution_process
2365
2501
  end
2366
2502
 
2367
- def execute_temporary_script(script_code, additional_code = [])
2368
- full_code = (additional_code || []) + [script_code]
2369
-
2370
- Tempfile.create('script_exec') do |temp_file|
2371
- temp_file.write(HashDelegator.join_code_lines(full_code))
2372
- temp_file.flush
2373
- File.chmod(0o755, temp_file.path)
2374
-
2375
- output = `#{temp_file.path}`
2376
-
2377
- if $?.exitstatus != 0
2378
- return :invalidated
2379
- end
2380
-
2381
- output
2382
- end
2383
- rescue StandardError => err
2384
- warn "Error executing script: #{err.message}"
2385
- nil
2386
- end
2387
-
2388
2503
  def expand_blocks_with_replacements(
2389
2504
  menu_blocks, replacements, exclude_types: [BlockType::SHELL]
2390
2505
  )
@@ -2405,15 +2520,32 @@ module MarkdownExec
2405
2520
  def expand_references!(fcb, link_state)
2406
2521
  expand_variable_references!(
2407
2522
  blocks: [fcb],
2523
+ echo_formatter: method(:format_echo_command),
2524
+ group_name: :payload,
2525
+ initial_code_required: false,
2526
+ link_state: link_state,
2527
+ pattern: @delegate_object[:option_expansion_expression_regexp].present? &&
2528
+ Regexp.new(@delegate_object[:option_expansion_expression_regexp])
2529
+ )
2530
+
2531
+ # variable expansions
2532
+ expand_variable_references!(
2533
+ blocks: [fcb],
2534
+ echo_formatter: lambda do |variable|
2535
+ %(echo "$#{variable}")
2536
+ end,
2537
+ group_name: @delegate_object[:variable_expansion_name_capture_group]&.to_sym,
2408
2538
  initial_code_required: false,
2409
- link_state: link_state
2539
+ link_state: link_state,
2540
+ pattern: options_variable_expansion_regexp
2410
2541
  )
2542
+
2543
+ # command substitutions
2411
2544
  expand_variable_references!(
2412
2545
  blocks: [fcb],
2413
- echo_format: '%s',
2414
- group_name: :command,
2546
+ echo_formatter: lambda { |command| command },
2547
+ group_name: @delegate_object[:command_substitution_name_capture_group]&.to_sym,
2415
2548
  initial_code_required: false,
2416
- key_format: '$(%s)',
2417
2549
  link_state: link_state,
2418
2550
  pattern: options_command_substitution_regexp
2419
2551
  )
@@ -2421,26 +2553,25 @@ module MarkdownExec
2421
2553
 
2422
2554
  def expand_variable_references!(
2423
2555
  blocks:,
2424
- echo_format: 'echo "$%s"',
2425
- group_name: :variable,
2556
+ echo_formatter:,
2557
+ group_name:,
2426
2558
  initial_code_required: false,
2427
- key_format: '${%s}',
2428
2559
  link_state:,
2429
- pattern: nil
2560
+ pattern:
2430
2561
  )
2431
- pattern ||= options_variable_expression_regexp
2432
- return if pattern.nil?
2433
-
2434
- variable_counts = count_named_group_occurrences(blocks, pattern,
2435
- group_name: group_name)
2562
+ variable_counts, occurrence_expressions = count_named_group_occurrences(
2563
+ blocks, pattern, group_name: group_name
2564
+ )
2436
2565
  return if variable_counts.nil? || variable_counts == {}
2437
2566
 
2438
- echo_commands = generate_echo_commands(variable_counts, echo_format)
2567
+ echo_commands = generate_echo_commands(
2568
+ variable_counts, formatter: echo_formatter
2569
+ )
2439
2570
 
2440
2571
  replacements = build_replacement_dictionary(
2441
2572
  echo_commands, link_state,
2442
2573
  initial_code_required: initial_code_required,
2443
- key_format: key_format
2574
+ occurrence_expressions: occurrence_expressions
2444
2575
  )
2445
2576
 
2446
2577
  return if replacements.nil?
@@ -2449,50 +2580,48 @@ module MarkdownExec
2449
2580
  expand_blocks_with_replacements(blocks, replacements)
2450
2581
  end
2451
2582
 
2452
- def export_echo_with_code_single(export_echo, inherited_code, code_lines,
2453
- required)
2454
- code = %(printf '%s' "#{export_echo}")
2455
- value = execute_temporary_script(
2456
- code,
2457
- (inherited_code || []) +
2458
- code_lines + required[:code]
2459
- )
2460
- if value == :invalidated
2461
- warn "A value must exist for: #{export.preconditions.join(', ')}"
2462
- end
2463
- value
2464
- end
2465
-
2466
- def export_echo_with_code(export, inherited_code, code_lines, required,
2467
- force:)
2583
+ def export_echo_with_code(
2584
+ bash_script_lines, export, force:
2585
+ )
2468
2586
  exportable = true
2587
+ command_result = nil
2588
+ new_lines = []
2469
2589
  case export.echo
2470
- when String
2471
- value = export_echo_with_code_single(export.echo, inherited_code,
2472
- code_lines, required)
2590
+ when String, Integer, Float, TrueClass, FalseClass
2591
+ command_result = output_from_adhoc_bash_script_file(
2592
+ join_array_of_arrays(
2593
+ bash_script_lines,
2594
+ %(printf '%s' "#{export.echo}")
2595
+ )
2596
+ )
2597
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
2598
+ exportable = false
2599
+ command_result.warning = warning_required_empty(export)
2600
+ end
2601
+
2473
2602
  when Hash
2474
2603
  # each item in the hash is a variable name and value
2475
2604
  export.echo.each do |name, expression|
2476
- value = export_echo_with_code_single(expression, inherited_code,
2477
- code_lines, required)
2478
- ENV[name] = value.to_s
2479
- code_lines.push code_line_safe_assign(name, value, force: force)
2605
+ command_result = output_from_adhoc_bash_script_file(
2606
+ join_array_of_arrays(
2607
+ bash_script_lines,
2608
+ %(printf '%s' "#{expression}")
2609
+ )
2610
+ )
2611
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
2612
+ command_result.warning = warning_required_empty(export)
2613
+ else
2614
+ ENV[name] = command_result.stdout.to_s
2615
+ new_lines << code_line_safe_assign(name, command_result.stdout,
2616
+ force: force)
2617
+ end
2480
2618
  end
2619
+
2620
+ # individual items have been exported, none remain
2481
2621
  exportable = false
2482
2622
  end
2483
- [value, exportable]
2484
- end
2485
2623
 
2486
- def export_exec_with_code(export, inherited_code, code_lines, required)
2487
- value = execute_temporary_script(
2488
- export.exec,
2489
- (inherited_code || []) +
2490
- code_lines + required[:code]
2491
- )
2492
- if value == :invalidated
2493
- warn "A value must exist for: #{export.preconditions.join(', ')}"
2494
- end
2495
- value
2624
+ [command_result, exportable, new_lines]
2496
2625
  end
2497
2626
 
2498
2627
  # Retrieves a specific data symbol from the delegate object,
@@ -2553,6 +2682,13 @@ module MarkdownExec
2553
2682
  )
2554
2683
  end
2555
2684
 
2685
+ def find_option_by_name(name)
2686
+ name_sym = name.to_sym
2687
+ @menu_from_yaml.find do |option|
2688
+ option[:opt_name] == name_sym
2689
+ end
2690
+ end
2691
+
2556
2692
  def format_and_execute_command(
2557
2693
  code_lines:,
2558
2694
  erls:,
@@ -2572,6 +2708,24 @@ module MarkdownExec
2572
2708
  color_sym: :script_execution_frame_color)
2573
2709
  end
2574
2710
 
2711
+ def format_echo_command(payload)
2712
+ payload_match = payload.match(@delegate_object[:option_expansion_payload_regexp])
2713
+ variable = payload_match[:option]
2714
+ property = payload_match[:property]
2715
+
2716
+ echo_value = case property
2717
+ when 'default', 'description'
2718
+ item = find_option_by_name(variable)
2719
+ item ? item[property.to_sym] : ''
2720
+ when 'length'
2721
+ @delegate_object[variable.to_sym].to_s.length
2722
+ else
2723
+ @delegate_object[variable.to_sym]
2724
+ end
2725
+
2726
+ "echo #{Shellwords.escape(echo_value)}"
2727
+ end
2728
+
2575
2729
  # Format expression using environment variables and run state
2576
2730
  def format_expression(expr)
2577
2731
  data = link_load_format_data
@@ -2629,13 +2783,12 @@ module MarkdownExec
2629
2783
  color_sym: :execution_report_preview_frame_color)
2630
2784
  end
2631
2785
 
2632
- def generate_echo_commands(variable_counts, echo_format)
2786
+ def generate_echo_commands(variable_counts, formatter: nil)
2633
2787
  # commands to echo variables
2634
2788
  #
2635
2789
  commands = {}
2636
2790
  variable_counts.each_key do |variable|
2637
- command = format(echo_format, variable)
2638
- commands[variable] = command
2791
+ commands[variable] = formatter.call(variable)
2639
2792
  end
2640
2793
  commands
2641
2794
  end
@@ -2792,7 +2945,7 @@ module MarkdownExec
2792
2945
 
2793
2946
  update_line_and_block_state(
2794
2947
  nested_line, state, selected_types,
2795
- source_id: "#{@delegate_object[:filename]}_ibfnf_#{index}",
2948
+ source_id: "#{@delegate_object[:filename]}¤ItrBlkFrmNstFls:#{index}",
2796
2949
  &block
2797
2950
  )
2798
2951
  end
@@ -2818,6 +2971,14 @@ module MarkdownExec
2818
2971
  end
2819
2972
  end
2820
2973
 
2974
+ # join a list of arrays into a single array
2975
+ # convert single items to arrays
2976
+ def join_array_of_arrays(*args)
2977
+ args.map do |item|
2978
+ item.is_a?(Array) ? item : [item]
2979
+ end.compact.flatten(1)
2980
+ end
2981
+
2821
2982
  def link_block_data_eval(link_state, code_lines, selected, link_block_data,
2822
2983
  block_source:, shell:)
2823
2984
  all_code = HashDelegator.code_merge(link_state&.inherited_lines,
@@ -2943,7 +3104,8 @@ module MarkdownExec
2943
3104
 
2944
3105
  list = []
2945
3106
  iter_source_blocks(
2946
- @delegate_object[:list_blocks_type], source_id: source_id
3107
+ @delegate_object[:list_blocks_type],
3108
+ source_id: source_id
2947
3109
  ) do |block|
2948
3110
  list << (block_eval.present? ? eval(block_eval) : block.send(message))
2949
3111
  end
@@ -2952,69 +3114,45 @@ module MarkdownExec
2952
3114
  @fout.fout_list(list)
2953
3115
  end
2954
3116
 
2955
- # Loads auto blocks based on delegate object settings and updates
2956
- # if new filename is detected.
2957
- # Executes a specified block once per filename.
2958
- # @param all_blocks [Array] Array of all block elements.
2959
- # @return [Boolean, nil] True if values were modified, nil otherwise.
3117
+ # Loads and updates auto options for document blocks if the current filename has changed.
3118
+ #
3119
+ # This method checks if the delegate object specifies a document load options block name and if the filename
3120
+ # has been updated. It then selects the appropriate blocks, collects their dependencies, processes their
3121
+ # options, and updates the menu base with the merged options.
3122
+ #
3123
+ # @param all_blocks [Array] An array of all block elements.
3124
+ # @param mdoc [Object] The document object managing dependencies and options.
3125
+ # @return [Boolean, nil] Returns true if options were updated; nil otherwise.
2960
3126
  def load_auto_opts_block(all_blocks, mdoc:)
2961
- block_name = @delegate_object[:document_load_opts_block_name]
2962
- unless block_name.present? &&
2963
- @opts_most_recent_filename != @delegate_object[:filename]
2964
- return
2965
- end
3127
+ opts_block_name = @delegate_object[:document_load_opts_block_name]
3128
+ current_filename = @delegate_object[:filename]
2966
3129
 
2967
- blocks = HashDelegator.block_select(all_blocks, :oname, block_name)
2968
- return if blocks.empty?
3130
+ return unless opts_block_name.present? &&
3131
+ @opts_most_recent_filename != current_filename
2969
3132
 
2970
- update_menu_base(
2971
- blocks.each.with_object({}) do |block, merged_options|
3133
+ selected_blocks = HashDelegator.block_select(all_blocks, :oname,
3134
+ opts_block_name)
3135
+ return if selected_blocks.empty?
3136
+
3137
+ dependency_map = {}
3138
+ selected_blocks.each do |block|
3139
+ mdoc.collect_dependencies(memo: dependency_map, block: block)
3140
+ end
3141
+
3142
+ collected_options =
3143
+ dependency_map.each.with_object({}) do |(block_id, _), merged_options|
3144
+ matching_block = HashDelegator.block_find(all_blocks, :id, block_id)
2972
3145
  options_state = read_show_options_and_trigger_reuse(
2973
- mdoc: mdoc,
2974
- selected: block
3146
+ mdoc: mdoc, selected: matching_block
2975
3147
  )
2976
3148
  merged_options.merge!(options_state.options)
2977
3149
  end
2978
- )
2979
3150
 
2980
- @opts_most_recent_filename = @delegate_object[:filename]
3151
+ update_menu_base(collected_options)
3152
+ @opts_most_recent_filename = current_filename
2981
3153
  true
2982
3154
  end
2983
3155
 
2984
- def load_auto_ux_block(
2985
- all_blocks,
2986
- mdoc,
2987
- block_name: @delegate_object[:document_load_ux_block_name]
2988
- )
2989
- unless block_name.present? &&
2990
- @ux_most_recent_filename != @delegate_object[:filename]
2991
- return
2992
- end
2993
-
2994
- blocks = HashDelegator.block_select(all_blocks, :oname, block_name)
2995
- if blocks.empty?
2996
- blocks = HashDelegator.block_match(all_blocks, :nickname,
2997
- Regexp.new(block_name))
2998
- end
2999
- return if blocks.empty?
3000
-
3001
- @ux_most_recent_filename = @delegate_object[:filename]
3002
-
3003
- (blocks.each.with_object([]) do |block, merged_options|
3004
- code = code_from_ux_block_to_set_environment_variables(
3005
- block,
3006
- mdoc,
3007
- force: @delegate_object[:ux_auto_load_force_default],
3008
- only_default: true
3009
- )
3010
- if code == :ux_exec_prohibited
3011
- merged_options
3012
- else
3013
- merged_options.push(code)
3014
- end
3015
- end).to_a
3016
- end
3017
-
3018
3156
  def load_auto_vars_block(
3019
3157
  all_blocks,
3020
3158
  block_name: @delegate_object[:document_load_vars_block_name]
@@ -3175,7 +3313,7 @@ module MarkdownExec
3175
3313
 
3176
3314
  # load document ux block
3177
3315
  #
3178
- if (code_lines = load_auto_ux_block(all_blocks, mdoc))
3316
+ if (code_lines = code_from_automatic_ux_blocks(all_blocks, mdoc))
3179
3317
  new_code = HashDelegator.code_merge(link_state.inherited_lines,
3180
3318
  code_lines)
3181
3319
  next_state_set_code(nil, link_state, new_code)
@@ -3231,14 +3369,33 @@ module MarkdownExec
3231
3369
  source_id: source_id
3232
3370
  )
3233
3371
 
3234
- ### compress empty lines
3235
3372
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
3236
- HashDelegator.tables_into_columns!(menu_blocks, @delegate_object,
3237
- screen_width_for_table)
3373
+ begin
3374
+ HashDelegator.tables_into_columns!(menu_blocks, @delegate_object,
3375
+ screen_width_for_table)
3376
+ rescue NoMethodError
3377
+ # an invalid table format
3378
+ end
3379
+ handle_consecutive_inactive_items!(menu_blocks)
3238
3380
 
3239
3381
  [all_blocks, menu_blocks, mdoc]
3240
3382
  end
3241
3383
 
3384
+ def handle_consecutive_inactive_items!(menu_blocks)
3385
+ consecutive_inactive_count = 0
3386
+ menu_blocks.each do |fcb|
3387
+ unless fcb.is_disabled?
3388
+ consecutive_inactive_count = 0
3389
+ else
3390
+ consecutive_inactive_count += 1
3391
+ if (consecutive_inactive_count % (@delegate_object[:select_page_height] / 3)).zero?
3392
+ fcb.disabled = TtyMenu::ENABLE
3393
+ fcb.is_enabled_but_inactive = true
3394
+ end
3395
+ end
3396
+ end
3397
+ end
3398
+
3242
3399
  def menu_add_disabled_option(document_glob)
3243
3400
  raise unless document_glob.present?
3244
3401
  raise if @dml_menu_blocks.nil?
@@ -3378,9 +3535,9 @@ module MarkdownExec
3378
3535
  @delegate_object[:import_paths]&.split(':') || ''
3379
3536
  end
3380
3537
 
3381
- def options_variable_expression_regexp
3382
- @delegate_object[:variable_expression_regexp].present? &&
3383
- Regexp.new(@delegate_object[:variable_expression_regexp])
3538
+ def options_variable_expansion_regexp
3539
+ @delegate_object[:variable_expansion_regexp].present? &&
3540
+ Regexp.new(@delegate_object[:variable_expansion_regexp])
3384
3541
  end
3385
3542
 
3386
3543
  def output_color_formatted(data_sym, color_sym)
@@ -3405,6 +3562,21 @@ module MarkdownExec
3405
3562
  }
3406
3563
  end
3407
3564
 
3565
+ def output_from_adhoc_bash_script_file(bash_script_lines)
3566
+ Tempfile.create('script_exec') do |temp_file|
3567
+ temp_file.write(HashDelegator.join_code_lines(bash_script_lines))
3568
+ temp_file.flush
3569
+ File.chmod(0o755, temp_file.path)
3570
+
3571
+ output = `#{temp_file.path}`
3572
+
3573
+ CommandResult.new(stdout: output, exit_status: $?.exitstatus)
3574
+ end
3575
+ rescue StandardError => err
3576
+ warn "Error executing script: #{err.message}"
3577
+ nil
3578
+ end
3579
+
3408
3580
  def output_labeled_value(label, value, level)
3409
3581
  @fout.lout format_references_send_color(
3410
3582
  context: {
@@ -3422,9 +3594,7 @@ module MarkdownExec
3422
3594
  end
3423
3595
 
3424
3596
  def persist_fcb(options)
3425
- MarkdownExec::FCB.new(options).tap do |fcb|
3426
- @fcb_store << fcb
3427
- end
3597
+ HashDelegator.persist_fcb_self(@fcb_store, options)
3428
3598
  end
3429
3599
 
3430
3600
  def pop_add_current_code_to_head_and_trigger_load(
@@ -3491,6 +3661,11 @@ module MarkdownExec
3491
3661
  fout_execution_report if @delegate_object[:output_execution_report]
3492
3662
  end
3493
3663
 
3664
+ # all UX blocks are automatic for the document
3665
+ def select_automatic_ux_blocks(blocks)
3666
+ blocks.select { |item| item.type == 'ux' }
3667
+ end
3668
+
3494
3669
  # Filter blocks per block_name_include_match, block_name_wrapper_match.
3495
3670
  #
3496
3671
  # @param all_blocks [Array<Hash>] The list of blocks from the file.
@@ -4084,7 +4259,7 @@ module MarkdownExec
4084
4259
  # Presents a TTY prompt to select an option or exit,
4085
4260
  # returns metadata including option and selected
4086
4261
  def select_option_with_metadata(
4087
- prompt_text, menu_items, opts = {}, menu_blocks: nil
4262
+ prompt_text, tty_menu_items, opts = {}, menu_blocks: nil
4088
4263
  )
4089
4264
  @dml_menu_blocks = menu_blocks if menu_blocks
4090
4265
 
@@ -4107,10 +4282,10 @@ module MarkdownExec
4107
4282
  per_page: @delegate_object[:select_page_height]
4108
4283
  }.freeze
4109
4284
 
4110
- if menu_items.all? do |item|
4285
+ if tty_menu_items.all? do |item|
4111
4286
  !item.is_a?(String) && item[:disabled]
4112
4287
  end
4113
- menu_items.each do |prompt_item|
4288
+ tty_menu_items.each do |prompt_item|
4114
4289
  puts prompt_item[:dname]
4115
4290
  end
4116
4291
  return
@@ -4120,19 +4295,21 @@ module MarkdownExec
4120
4295
  # crashes if default is not an existing item
4121
4296
  #
4122
4297
  selection = @prompt.select(prompt_text,
4123
- menu_items,
4298
+ tty_menu_items,
4124
4299
  opts.merge(props))
4125
4300
  rescue TTY::Prompt::ConfigurationError
4126
4301
  # prompt fails when collapsible block name has changed; clear default
4127
4302
  selection = @prompt.select(prompt_text,
4128
- menu_items,
4303
+ tty_menu_items,
4129
4304
  opts.merge(props).merge(default: nil))
4130
4305
  rescue NoMethodError
4131
4306
  # no enabled options in page
4132
4307
  return
4133
4308
  end
4134
4309
 
4135
- selected = @dml_menu_blocks.find do |item|
4310
+ menu_list = opts.fetch(:match_dml, true) ? @dml_menu_blocks : menu_items
4311
+ menu_list ||= tty_menu_items
4312
+ selected = menu_list.find do |item|
4136
4313
  if item.instance_of?(Hash)
4137
4314
  [item[:id], item[:name], item[:dname]].include?(selection)
4138
4315
  elsif item.instance_of?(MarkdownExec::FCB)
@@ -4142,7 +4319,15 @@ module MarkdownExec
4142
4319
  end
4143
4320
  end
4144
4321
 
4322
+ # new FCB if selected is not an object
4323
+ if selected.instance_of?(String)
4324
+ selected = FCB.new(dname: selected)
4325
+ elsif selected.instance_of?(Hash)
4326
+ selected = FCB.new(selected)
4327
+ end
4328
+
4145
4329
  unless selected
4330
+ report_and_reraise('menu item not found')
4146
4331
  HashDelegator.error_handler('select_option_with_metadata',
4147
4332
  error: 'menu item not found')
4148
4333
  exit 1
@@ -4394,145 +4579,188 @@ module MarkdownExec
4394
4579
  @delegate_object.merge!(options)
4395
4580
  end
4396
4581
 
4397
- def ux_block_export_activated(export, inherited_code, code_lines, required,
4398
- exit_prompt)
4582
+ def ux_block_export_activated(
4583
+ bash_script_lines, export, exit_prompt
4584
+ )
4399
4585
  exportable = true
4400
4586
  transformable = true
4401
- [if export.allowed.present?
4402
- if export.allowed == :echo
4403
- output, exportable = export_echo_with_code(
4404
- export, inherited_code, code_lines, required, force: true
4405
- )
4406
- return :ux_exec_prohibited if output == :invalidated
4407
-
4408
- menu_from_list_with_back(output.split("\n"))
4409
-
4410
- elsif export.allowed == :exec
4411
- output = process_allowed_exec(export, inherited_code,
4412
- code_lines, required)
4413
- return :ux_exec_prohibited if output == :ux_exec_prohibited
4414
-
4415
- menu_from_list_with_back(output)
4416
-
4417
- else
4418
- menu_from_list_with_back(export.allowed)
4419
- end
4420
-
4421
- # echo > exec
4422
- elsif export.echo
4423
- output, exportable = export_echo_with_code(
4424
- export, inherited_code, code_lines, required, force: true
4425
- )
4426
- return :ux_exec_prohibited if output == :invalidated
4427
-
4428
- output
4429
-
4430
- # exec > allowed
4431
- elsif export.exec
4432
- output = export_exec_with_code(
4433
- export, inherited_code, code_lines, required
4434
- )
4435
- return :ux_exec_prohibited if output == :invalidated
4436
-
4437
- output
4438
-
4439
- # allowed > prompt
4440
- elsif export.allowed && export.allowed.count.positive?
4441
- case (choice = prompt_select_from_list(
4442
- [exit_prompt] + export.allowed,
4443
- string: export.prompt,
4444
- color_sym: :prompt_color_after_script_execution
4445
- ))
4446
- when exit_prompt
4447
- exportable = false
4448
- transformable = false
4449
- nil
4450
- else
4451
- choice
4452
- end
4453
-
4454
- # prompt > default
4455
- elsif export.prompt.present?
4456
- begin
4457
- loop do
4458
- print "#{export.prompt} [#{export.default}]: "
4459
- output = gets.chomp
4460
- output = export.default.to_s if output.empty?
4461
- caps = NamedCaptureExtractor.extract_named_groups(output,
4462
- export.validate)
4463
- break if caps
4464
-
4465
- # invalid input, retry
4466
- end
4467
- rescue Interrupt
4468
- exportable = false
4469
- transformable = false
4470
- end
4471
-
4472
- output
4473
-
4474
- # default
4475
- else
4476
- transformable = false
4477
- export.default
4478
- end, exportable, transformable]
4479
- end
4480
-
4481
- def ux_block_export_automatic(export, inherited_code, code_lines, required)
4587
+ new_lines = []
4588
+ command_result = nil
4589
+
4590
+ case as = FCB.act_source(export)####
4591
+ when false, UxActSource::FALSE
4592
+ raise 'Should not be reached.'
4593
+
4594
+ when ':allow', UxActSource::ALLOW
4595
+ raise unless export.allow.present?
4596
+
4597
+ case export.allow
4598
+ when :echo, ExportValueSource::ECHO
4599
+ command_result, exportable, new_lines = export_echo_with_code(
4600
+ bash_script_lines,
4601
+ export,
4602
+ force: true
4603
+ )
4604
+ if command_result.failure?
4605
+ command_result
4606
+ else
4607
+ command_result = CommandResult.new(
4608
+ stdout: menu_from_list_with_back(command_result.stdout.split("\n"))
4609
+ )
4610
+ end
4611
+
4612
+ when ':exec', UxActSource::EXEC
4613
+ command_result = output_from_adhoc_bash_script_file(
4614
+ join_array_of_arrays(bash_script_lines, export.exec)
4615
+ )
4616
+
4617
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
4618
+ command_result
4619
+ else
4620
+ command_result = CommandResult.new(
4621
+ stdout: menu_from_list_with_back(
4622
+ command_result.stdout.split("\n")
4623
+ )
4624
+ )
4625
+ end
4626
+
4627
+ else
4628
+ command_result = CommandResult.new(
4629
+ stdout: menu_from_list_with_back(export.allow)
4630
+ )
4631
+ end
4632
+
4633
+ when ':echo', UxActSource::ECHO
4634
+ command_result, exportable, new_lines = export_echo_with_code(
4635
+ bash_script_lines,
4636
+ export,
4637
+ force: true
4638
+ )
4639
+
4640
+ command_result
4641
+
4642
+ when ':edit', UxActSource::EDIT
4643
+ output = nil
4644
+ begin
4645
+ loop do
4646
+ print "#{export.prompt} [#{export.default}]: "
4647
+ output = gets.chomp
4648
+ output = export.default.to_s if output.empty?
4649
+ caps = NamedCaptureExtractor.extract_named_groups(output,
4650
+ export.validate)
4651
+ break if caps
4652
+
4653
+ # invalid input, retry
4654
+ end
4655
+ rescue Interrupt
4656
+ exportable = false
4657
+ transformable = false
4658
+ end
4659
+
4660
+ command_result = CommandResult.new(stdout: output)
4661
+
4662
+ when ':exec', UxActSource::EXEC
4663
+ command_result = output_from_adhoc_bash_script_file(
4664
+ join_array_of_arrays(bash_script_lines, export.exec)
4665
+ )
4666
+
4667
+ command_result
4668
+
4669
+ else
4670
+ transformable = false
4671
+ command_result = CommandResult.new(stdout: export.default.to_s)
4672
+ end
4673
+
4674
+ # add message for required variables
4675
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
4676
+ command_result.warning = warning_required_empty(export)
4677
+ # warn command_result.warning
4678
+ end
4679
+
4680
+ command_result.exportable = exportable
4681
+ command_result.transformable = transformable
4682
+ command_result.new_lines = new_lines
4683
+ command_result
4684
+ end
4685
+
4686
+ def ux_block_export_automatic(bash_script_lines, export)
4482
4687
  transformable = true
4483
4688
  exportable = true
4484
- [case export.default
4485
- when :allowed
4486
- raise unless export.allowed.present?
4689
+ new_lines = []
4690
+ command_result = nil
4487
4691
 
4488
- if export.allowed == :echo
4489
- output, exportable = export_echo_with_code(
4490
- export, inherited_code, code_lines, required, force: false
4491
- )
4492
- return :ux_exec_prohibited if output == :invalidated
4692
+ case FCB.init_source(export)
4693
+ when false, UxActSource::FALSE
4694
+ exportable = false
4695
+ transformable = false
4696
+ command_result = CommandResult.new
4697
+
4698
+ when ':allow', UxActSource::ALLOW
4699
+ raise unless export.allow.present?
4700
+
4701
+ case export.allow
4702
+ when :echo, ExportValueSource::ECHO
4703
+ command_result, exportable, new_lines = export_echo_with_code(
4704
+ bash_script_lines,
4705
+ export,
4706
+ force: false
4707
+ )
4708
+ unless command_result.failure?
4709
+ command_result.stdout = (exportable && command_result.stdout.split("\n").first) || ''
4710
+ end
4493
4711
 
4494
- exportable && output.split("\n").first
4712
+ when :exec, ExportValueSource::EXEC
4713
+ command_result = output_from_adhoc_bash_script_file(
4714
+ join_array_of_arrays(bash_script_lines, export.exec)
4715
+ )
4716
+ unless command_result.failure?
4717
+ command_result.stdout = command_result.stdout.split("\n").first
4718
+ end
4495
4719
 
4496
- elsif export.allowed == :exec
4497
- output = process_allowed_exec(export, inherited_code,
4498
- code_lines, required)
4499
- return :ux_exec_prohibited if output == :ux_exec_prohibited
4720
+ else
4721
+ # must be a list
4722
+ command_result = CommandResult.new(stdout: export.allow.first)
4723
+ end
4500
4724
 
4501
- output.first
4725
+ when ':default', UxActSource::DEFAULT
4726
+ transformable = false
4727
+ command_result = CommandResult.new(stdout: export.default.to_s)
4502
4728
 
4503
- else
4504
- export.allowed.first
4505
- end
4729
+ when ':echo', UxActSource::ECHO
4730
+ raise unless export.echo.present?
4506
4731
 
4507
- # echo > default
4508
- when :echo
4509
- raise unless export.echo.present?
4732
+ command_result, exportable, new_lines = export_echo_with_code(
4733
+ bash_script_lines,
4734
+ export,
4735
+ force: false
4736
+ )
4510
4737
 
4511
- output, exportable = export_echo_with_code(
4512
- export, inherited_code, code_lines, required, force: false
4513
- )
4514
- return :ux_exec_prohibited if output == :invalidated
4738
+ when ':exec', UxActSource::EXEC
4739
+ raise unless export.exec.present?
4515
4740
 
4516
- output
4741
+ command_result = output_from_adhoc_bash_script_file(
4742
+ join_array_of_arrays(bash_script_lines, export.exec)
4743
+ )
4517
4744
 
4518
- # exec > default
4519
- when :exec
4520
- raise unless export.exec.present?
4745
+ else
4746
+ command_result = CommandResult.new(stdout: export.init.to_s)
4747
+ # raise "Unknown FCB.init_source(export) #{FCB.init_source(export)}"
4748
+ end
4521
4749
 
4522
- output = export_exec_with_code(
4523
- export, inherited_code, code_lines, required
4524
- )
4525
- return :ux_exec_prohibited if output == :invalidated
4750
+ # add message for required variables
4751
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
4752
+ command_result.warning = warning_required_empty(export)
4753
+ warn command_result.warning
4754
+ end
4526
4755
 
4527
- output
4756
+ command_result.exportable = exportable
4757
+ command_result.transformable = transformable
4758
+ command_result.new_lines = new_lines
4759
+ command_result
4760
+ end
4528
4761
 
4529
- # default
4530
- else
4531
- transformable = false
4532
- export.default.to_s
4533
- end,
4534
- exportable,
4535
- transformable]
4762
+ def warning_required_empty(export)
4763
+ "A value must exist for: #{export.required.join(', ')}"
4536
4764
  end
4537
4765
 
4538
4766
  def vux_await_user_selection(prior_answer: @dml_block_selection)
@@ -4759,7 +4987,8 @@ module MarkdownExec
4759
4987
  #
4760
4988
  # @return [Nil] Returns nil if no code block is selected
4761
4989
  # or an error occurs.
4762
- def vux_main_loop
4990
+ def vux_main_loop(menu_from_yaml: nil)
4991
+ @menu_from_yaml = menu_from_yaml
4763
4992
  vux_init
4764
4993
  vux_load_code_files_into_state
4765
4994
  formatted_choice_ostructs = vux_formatted_names_for_state_chrome_blocks
@@ -4809,10 +5038,10 @@ module MarkdownExec
4809
5038
  case msg
4810
5039
  when :parse_document # once for each menu
4811
5040
  count = 0
4812
- vux_parse_document(source_id: "#{@delegate_object[:filename]}_vmlpd")
5041
+ vux_parse_document(source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®PrsDoc")
4813
5042
  vux_menu_append_history_files(
4814
5043
  formatted_choice_ostructs,
4815
- source_id: "#{@delegate_object[:filename]}_vmlhf"
5044
+ source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®HstFls"
4816
5045
  )
4817
5046
  vux_publish_document_file_name_for_external_automation
4818
5047
 
@@ -4824,7 +5053,7 @@ module MarkdownExec
4824
5053
  # yield :end_of_cli, @delegate_object
4825
5054
 
4826
5055
  if @delegate_object[:list_blocks]
4827
- list_blocks(source_id: "#{@delegate_object[:filename]}_vmleoc")
5056
+ list_blocks(source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®EndCLI")
4828
5057
  :exit
4829
5058
  end
4830
5059
 
@@ -5049,8 +5278,8 @@ module MarkdownExec
5049
5278
  :prompt_color_after_script_execution
5050
5279
  )
5051
5280
 
5052
- menu_items = blocks_as_menu_items(menu_blocks)
5053
- if menu_items.empty?
5281
+ tty_menu_items = blocks_as_menu_items(menu_blocks)
5282
+ if tty_menu_items.empty?
5054
5283
  return SelectedBlockMenuState.new(nil, OpenStruct.new,
5055
5284
  MenuState::EXIT)
5056
5285
  end
@@ -5085,8 +5314,9 @@ module MarkdownExec
5085
5314
  { cycle: @delegate_object[:select_page_cycle],
5086
5315
  per_page: @delegate_object[:select_page_height] }
5087
5316
  )
5088
- selected_option = select_option_with_metadata(prompt_title, menu_items,
5089
- selection_opts)
5317
+ selected_option = select_option_with_metadata(
5318
+ prompt_title, tty_menu_items, selection_opts
5319
+ )
5090
5320
  determine_block_state(selected_option)
5091
5321
  end
5092
5322
 
@@ -5160,9 +5390,13 @@ module MarkdownExec
5160
5390
  save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
5161
5391
  if save_expr.present?
5162
5392
  save_filespec = save_filespec_from_expression(save_expr)
5163
- File.write(save_filespec,
5164
- HashDelegator.join_code_lines(link_state&.inherited_lines))
5165
- @delegate_object[:filename]
5393
+ if save_filespec.present?
5394
+ File.write(save_filespec,
5395
+ HashDelegator.join_code_lines(link_state&.inherited_lines))
5396
+ @delegate_object[:filename]
5397
+ else
5398
+ link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
5399
+ end
5166
5400
  else
5167
5401
  link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
5168
5402
  end
@@ -5170,6 +5404,8 @@ module MarkdownExec
5170
5404
  end
5171
5405
 
5172
5406
  class HashDelegator < HashDelegatorParent
5407
+ include ::ErrorReporting
5408
+
5173
5409
  # Cleans a value, handling both Hash and Struct types.
5174
5410
  # For Structs, the cleaned version is converted to a hash.
5175
5411
  def self.clean_value(value)
@@ -5382,7 +5618,7 @@ module MarkdownExec
5382
5618
  input: MarkdownExec::FCB.new(title: '',
5383
5619
  body: ['def add(x, y)',
5384
5620
  ' x + y', 'end']),
5385
- output: "def add(x, y)\n x + y\n end\n"
5621
+ output: "def add(x, y)\n x + y\n end"
5386
5622
  },
5387
5623
  {
5388
5624
  input: MarkdownExec::FCB.new(title: 'foo', body: %w[bar baz]),