markdown_exec 2.8.5 → 3.0.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -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-invalid.bats +1 -1
  9. data/bats/block-type-ux-required-variables.bats +20 -0
  10. data/bats/block-type-ux-row-format.bats +1 -1
  11. data/bats/block-type-ux-sources.bats +36 -0
  12. data/bats/border.bats +1 -1
  13. data/bats/cli.bats +2 -2
  14. data/bats/command-substitution-options.bats +14 -0
  15. data/bats/command-substitution.bats +1 -1
  16. data/bats/fail.bats +5 -2
  17. data/bats/import.bats +8 -0
  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 -7
  25. data/docs/dev/block-type-ux-auto.md +8 -5
  26. data/docs/dev/block-type-ux-chained.md +4 -2
  27. data/docs/dev/block-type-ux-echo-hash.md +6 -7
  28. data/docs/dev/block-type-ux-echo.md +2 -2
  29. data/docs/dev/block-type-ux-exec.md +3 -5
  30. data/docs/dev/block-type-ux-hidden.md +3 -0
  31. data/docs/dev/{block-type-ux-preconditions.md → block-type-ux-required-variables.md} +2 -3
  32. data/docs/dev/block-type-ux-row-format.md +3 -4
  33. data/docs/dev/block-type-ux-sources.md +57 -0
  34. data/docs/dev/block-type-ux-transform.md +0 -4
  35. data/docs/dev/command-substitution-options.md +61 -0
  36. data/docs/dev/indented-block-type-vars.md +1 -0
  37. data/docs/dev/menu-pagination-indent.md +123 -0
  38. data/docs/dev/menu-pagination.md +111 -0
  39. data/docs/dev/option-expansion.md +10 -0
  40. data/lib/ansi_formatter.rb +2 -0
  41. data/lib/block_cache.rb +197 -0
  42. data/lib/cached_nested_file_reader.rb +3 -1
  43. data/lib/command_result.rb +57 -0
  44. data/lib/constants.rb +19 -1
  45. data/lib/error_reporting.rb +38 -0
  46. data/lib/evaluate_shell_expressions.rb +43 -18
  47. data/lib/fcb.rb +98 -7
  48. data/lib/hash_delegator.rb +544 -330
  49. data/lib/markdown_exec/version.rb +1 -1
  50. data/lib/markdown_exec.rb +136 -45
  51. data/lib/mdoc.rb +59 -10
  52. data/lib/menu.src.yml +23 -11
  53. data/lib/menu.yml +22 -12
  54. data/lib/value_or_exception.rb +76 -0
  55. metadata +16 -4
  56. data/bats/block-type-ux-preconditions.bats +0 -8
@@ -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,21 +1078,6 @@ 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
999
- )
1000
- return :ux_exec_prohibited if output == :invalidated
1001
-
1002
- output.split("\n")
1003
- end
1004
-
1005
1081
  def code_from_automatic_ux_blocks(
1006
1082
  all_blocks,
1007
1083
  mdoc
@@ -1010,25 +1086,25 @@ module MarkdownExec
1010
1086
  return
1011
1087
  end
1012
1088
 
1013
- blocks = select_automatic_ux_blocks(all_blocks)
1014
- if blocks.empty?
1015
- blocks = select_automatic_ux_blocks(all_blocks)
1016
- end
1089
+ blocks = select_automatic_ux_blocks(
1090
+ all_blocks.reject(&:is_split_rest?)
1091
+ )
1017
1092
  return if blocks.empty?
1018
1093
 
1019
1094
  @ux_most_recent_filename = @delegate_object[:filename]
1020
1095
 
1021
1096
  (blocks.each.with_object([]) do |block, merged_options|
1022
- code = code_from_ux_block_to_set_environment_variables(
1097
+ command_result_w_e_t_nl = code_from_ux_block_to_set_environment_variables(
1023
1098
  block,
1024
1099
  mdoc,
1025
1100
  force: @delegate_object[:ux_auto_load_force_default],
1026
- only_default: true
1101
+ only_default: true,
1102
+ silent: true
1027
1103
  )
1028
- if code == :ux_exec_prohibited
1104
+ if command_result_w_e_t_nl.failure?
1029
1105
  merged_options
1030
1106
  else
1031
- merged_options.push(code)
1107
+ merged_options.push(command_result_w_e_t_nl.stdout)
1032
1108
  end
1033
1109
  end).to_a
1034
1110
  end
@@ -1036,7 +1112,8 @@ module MarkdownExec
1036
1112
  # parse YAML body defining the UX for a single variable
1037
1113
  # set ENV value for the variable and return code lines for the same
1038
1114
  def code_from_ux_block_to_set_environment_variables(
1039
- selected, mdoc, inherited_code: nil, force: true, only_default: false
1115
+ selected, mdoc, inherited_code: nil, force: true, only_default: false,
1116
+ silent:
1040
1117
  )
1041
1118
  exit_prompt = @delegate_object[:prompt_filespec_back]
1042
1119
 
@@ -1048,7 +1125,7 @@ module MarkdownExec
1048
1125
  )
1049
1126
 
1050
1127
  # process each ux block in sequence, setting ENV and collecting lines
1051
- code_lines = []
1128
+ required_lines = []
1052
1129
  required[:blocks].each do |block|
1053
1130
  next unless block.type == BlockType::UX
1054
1131
 
@@ -1059,35 +1136,51 @@ module MarkdownExec
1059
1136
  prompt: @delegate_object[:prompt_ux_enter_a_value],
1060
1137
  validate: '^(?<name>[^ ].*)$'
1061
1138
  )
1139
+ block.export = export
1140
+ block.export_act = FCB.act_source(export)
1141
+ block.export_init = FCB.init_source(export)
1062
1142
 
1063
- # preconditions are variable names that must be set before the UX block is executed.
1143
+ # required are variable names that must be set before the UX block is executed.
1064
1144
  # if any precondition is not set, the sequence is aborted.
1065
- export.preconditions&.each do |precondition|
1066
- code_lines.push "[[ -z $#{precondition} ]] && exit 1"
1145
+ required_variables = []
1146
+ export.required&.each do |precondition|
1147
+ required_variables.push "[[ -z $#{precondition} ]] && exit #{EXIT_STATUS_REQUIRED_EMPTY}"
1067
1148
  end
1068
1149
 
1150
+ eval_code = join_array_of_arrays(
1151
+ inherited_code, # inherited code
1152
+ required_lines, # current block requirements
1153
+ required_variables, # test conditions
1154
+ required[:code] # current block code
1155
+ )
1069
1156
  if only_default
1070
- value, exportable, transformable =
1071
- ux_block_export_automatic(
1072
- export, inherited_code, code_lines, required
1073
- )
1157
+ command_result_w_e_t_nl =
1158
+ ux_block_export_automatic(eval_code, export)
1159
+ # do not display warnings on initializing call
1160
+ return command_result_w_e_t_nl if command_result_w_e_t_nl.failure?
1161
+
1074
1162
  else
1075
- value, exportable, transformable =
1076
- ux_block_export_activated(
1077
- export, inherited_code, code_lines, required, exit_prompt
1078
- )
1163
+ command_result_w_e_t_nl =
1164
+ ux_block_export_activated(eval_code, export, exit_prompt)
1165
+ if command_result_w_e_t_nl.failure?
1166
+ warn command_result_w_e_t_nl.warning if command_result_w_e_t_nl.warning&.present? && !silent
1167
+ return command_result_w_e_t_nl
1168
+ end
1079
1169
  end
1080
- return :ux_exec_prohibited if value == :ux_exec_prohibited
1170
+ return command_result_w_e_t_nl if command_result_w_e_t_nl.failure?
1081
1171
 
1082
- if SelectResponse.continue?(value)
1083
- if transformable
1084
- value = transform_export_value(value, export)
1172
+ required_lines.concat(command_result_w_e_t_nl.new_lines)
1173
+ if SelectResponse.continue?(command_result_w_e_t_nl.stdout)
1174
+ if command_result_w_e_t_nl.transformable
1175
+ command_result_w_e_t_nl.stdout = transform_export_value(
1176
+ command_result_w_e_t_nl.stdout, export
1177
+ )
1085
1178
  end
1086
1179
 
1087
- if exportable
1088
- ENV[export.name] = value.to_s
1089
- code_lines.push code_line_safe_assign(export.name, value,
1090
- force: force)
1180
+ if command_result_w_e_t_nl.exportable
1181
+ ENV[export.name] = command_result_w_e_t_nl.stdout.to_s
1182
+ required_lines.push code_line_safe_assign(export.name, command_result_w_e_t_nl.stdout,
1183
+ force: force)
1091
1184
  end
1092
1185
  end
1093
1186
  else
@@ -1095,7 +1188,7 @@ module MarkdownExec
1095
1188
  end
1096
1189
  end
1097
1190
 
1098
- code_lines
1191
+ CommandResult.new(stdout: required_lines)
1099
1192
  end
1100
1193
 
1101
1194
  # sets ENV
@@ -1331,7 +1424,9 @@ module MarkdownExec
1331
1424
  )
1332
1425
  # Initialize a counter for named group occurrences
1333
1426
  occurrence_count = Hash.new(0)
1334
- return occurrence_count if pattern.nil? || pattern == //
1427
+ occurrence_expressions = {}
1428
+ return [occurrence_count,
1429
+ occurrence_expressions] if pattern.nil? || pattern == //
1335
1430
 
1336
1431
  blocks.each do |block|
1337
1432
  # Skip processing for shell-type blocks
@@ -1340,11 +1435,13 @@ module MarkdownExec
1340
1435
  # Scan each block name for matches of the pattern
1341
1436
  count_named_group_occurrences_block_body_fix_indent(block).scan(pattern) do |(_, _variable_name)|
1342
1437
  pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1343
- occurrence_count[$LAST_MATCH_INFO[group_name]] += 1
1438
+ id = $LAST_MATCH_INFO[group_name]
1439
+ occurrence_count[id] += 1
1440
+ occurrence_expressions[id] = $LAST_MATCH_INFO['expression']
1344
1441
  end
1345
1442
  end
1346
1443
 
1347
- occurrence_count
1444
+ [occurrence_count, occurrence_expressions]
1348
1445
  end
1349
1446
 
1350
1447
  def count_named_group_occurrences_block_body_fix_indent(block)
@@ -1459,17 +1556,17 @@ module MarkdownExec
1459
1556
  fcb.center = center
1460
1557
  fcb.chrome = true
1461
1558
  fcb.collapse = collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse
1462
- fcb.token = line_obj[:collapse]
1463
1559
  fcb.disabled = disabled ? TtyMenu::DISABLE : nil
1560
+ fcb.dname = line_obj[:indent] + decorated
1464
1561
  fcb.id = "#{id}.#{index}"
1562
+ fcb.indent = line_obj[:indent]
1465
1563
  fcb.level = level
1564
+ fcb.oname = line_obj[:text]
1466
1565
  fcb.s0indent = indent
1467
1566
  fcb.s0printable = line_obj[:text]
1468
1567
  fcb.s1decorated = decorated
1469
- fcb.dname = line_obj[:indent] + decorated
1470
- fcb.indent = line_obj[:indent]
1471
- fcb.oname = line_obj[:text]
1472
1568
  fcb.text = line_obj[:text]
1569
+ fcb.token = line_obj[:collapse]
1473
1570
  fcb.type = type
1474
1571
  use_fcb = false # next line is new record
1475
1572
  else
@@ -1477,17 +1574,17 @@ module MarkdownExec
1477
1574
  center: center,
1478
1575
  chrome: true,
1479
1576
  collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
1480
- token: line_obj[:collapse],
1481
1577
  disabled: disabled ? TtyMenu::DISABLE : nil,
1578
+ dname: line_obj[:indent] + decorated,
1482
1579
  id: "#{id}.#{index}",
1580
+ indent: line_obj[:indent],
1483
1581
  level: level,
1582
+ oname: line_obj[:text],
1484
1583
  s0indent: indent,
1485
1584
  s0printable: line_obj[:text],
1486
1585
  s1decorated: decorated,
1487
- dname: line_obj[:indent] + decorated,
1488
- indent: line_obj[:indent],
1489
- oname: line_obj[:text],
1490
1586
  text: line_obj[:text],
1587
+ token: line_obj[:collapse],
1491
1588
  type: type
1492
1589
  )
1493
1590
  end
@@ -1921,14 +2018,17 @@ module MarkdownExec
1921
2018
 
1922
2019
  elsif selected.type == BlockType::UX
1923
2020
  debounce_reset
2021
+ command_result_w_e_t_nl = code_from_ux_block_to_set_environment_variables(
2022
+ selected,
2023
+ @dml_mdoc,
2024
+ inherited_code: @dml_link_state.inherited_lines,
2025
+ silent: true
2026
+ )
2027
+ ### TBD if command_result_w_e_t_nl.failure?
1924
2028
  next_state_append_code(
1925
2029
  selected,
1926
2030
  link_state,
1927
- code_from_ux_block_to_set_environment_variables(
1928
- selected,
1929
- @dml_mdoc,
1930
- inherited_code: @dml_link_state.inherited_lines
1931
- )
2031
+ command_result_w_e_t_nl.failure? ? [] : command_result_w_e_t_nl.stdout
1932
2032
  )
1933
2033
 
1934
2034
  elsif selected.type == BlockType::VARS
@@ -1977,6 +2077,11 @@ module MarkdownExec
1977
2077
  @dml_block_state = find_block_state_by_name(block_name)
1978
2078
  dump_and_warn_block_state(name: block_name,
1979
2079
  selected: @dml_block_state.block)
2080
+ if @dml_block_state.block.fetch(:is_enabled_but_inactive, false)
2081
+ @dml_block_selection = BlockSelection.new(@dml_block_state.block.id)
2082
+ return # do nothing
2083
+ end
2084
+
1980
2085
  next_block_state =
1981
2086
  execute_block_for_state_and_name(
1982
2087
  selected: @dml_block_state.block,
@@ -2150,7 +2255,7 @@ module MarkdownExec
2150
2255
  end
2151
2256
  elsif (selected_option = select_option_with_metadata(
2152
2257
  prompt_title,
2153
- [exit_prompt] + dirs.map do |file|
2258
+ [exit_prompt] + dirs.map do |file| # tty_menu_items
2154
2259
  { name:
2155
2260
  format(
2156
2261
  block_data['view'] || view,
@@ -2163,7 +2268,8 @@ module MarkdownExec
2163
2268
  oname: file }
2164
2269
  end,
2165
2270
  menu_options.merge(
2166
- cycle: true
2271
+ cycle: true,
2272
+ match_dml: false
2167
2273
  )
2168
2274
  ))
2169
2275
  if selected_option.dname != exit_prompt
@@ -2339,9 +2445,9 @@ module MarkdownExec
2339
2445
  def execute_inherited_save(
2340
2446
  code_lines: @dml_link_state.inherited_lines
2341
2447
  )
2342
- return unless (save_filespec = save_filespec_from_expression)
2343
-
2344
- document_name_in_glob_as_file_name
2448
+ return unless (save_filespec = save_filespec_from_expression(
2449
+ document_name_in_glob_as_file_name
2450
+ ))
2345
2451
 
2346
2452
  unless write_file_with_directory_creation(
2347
2453
  content: HashDelegator.join_code_lines(code_lines),
@@ -2397,27 +2503,6 @@ module MarkdownExec
2397
2503
  post_execution_process
2398
2504
  end
2399
2505
 
2400
- def execute_temporary_script(script_code, additional_code = [])
2401
- full_code = (additional_code || []) + [script_code]
2402
-
2403
- Tempfile.create('script_exec') do |temp_file|
2404
- temp_file.write(HashDelegator.join_code_lines(full_code))
2405
- temp_file.flush
2406
- File.chmod(0o755, temp_file.path)
2407
-
2408
- output = `#{temp_file.path}`
2409
-
2410
- if $?.exitstatus != 0
2411
- return :invalidated
2412
- end
2413
-
2414
- output
2415
- end
2416
- rescue StandardError => err
2417
- warn "Error executing script: #{err.message}"
2418
- nil
2419
- end
2420
-
2421
2506
  def expand_blocks_with_replacements(
2422
2507
  menu_blocks, replacements, exclude_types: [BlockType::SHELL]
2423
2508
  )
@@ -2438,17 +2523,32 @@ module MarkdownExec
2438
2523
  def expand_references!(fcb, link_state)
2439
2524
  expand_variable_references!(
2440
2525
  blocks: [fcb],
2526
+ echo_formatter: method(:format_echo_command),
2527
+ group_name: :payload,
2441
2528
  initial_code_required: false,
2442
- key_format: @delegate_object[:variable_expression_format],
2443
2529
  link_state: link_state,
2444
- pattern: options_variable_expression_regexp
2530
+ pattern: @delegate_object[:option_expansion_expression_regexp].present? &&
2531
+ Regexp.new(@delegate_object[:option_expansion_expression_regexp])
2445
2532
  )
2533
+
2534
+ # variable expansions
2446
2535
  expand_variable_references!(
2447
2536
  blocks: [fcb],
2448
- echo_format: '%s',
2449
- group_name: :command,
2537
+ echo_formatter: lambda do |variable|
2538
+ %(echo "$#{variable}")
2539
+ end,
2540
+ group_name: @delegate_object[:variable_expansion_name_capture_group]&.to_sym,
2541
+ initial_code_required: false,
2542
+ link_state: link_state,
2543
+ pattern: options_variable_expansion_regexp
2544
+ )
2545
+
2546
+ # command substitutions
2547
+ expand_variable_references!(
2548
+ blocks: [fcb],
2549
+ echo_formatter: lambda { |command| command },
2550
+ group_name: @delegate_object[:command_substitution_name_capture_group]&.to_sym,
2450
2551
  initial_code_required: false,
2451
- key_format: @delegate_object[:command_substitution_format],
2452
2552
  link_state: link_state,
2453
2553
  pattern: options_command_substitution_regexp
2454
2554
  )
@@ -2456,23 +2556,25 @@ module MarkdownExec
2456
2556
 
2457
2557
  def expand_variable_references!(
2458
2558
  blocks:,
2459
- echo_format: 'echo "$%s"',
2460
- group_name: :variable,
2559
+ echo_formatter:,
2560
+ group_name:,
2461
2561
  initial_code_required: false,
2462
- key_format:,
2463
2562
  link_state:,
2464
2563
  pattern:
2465
2564
  )
2466
- variable_counts = count_named_group_occurrences(blocks, pattern,
2467
- group_name: group_name)
2565
+ variable_counts, occurrence_expressions = count_named_group_occurrences(
2566
+ blocks, pattern, group_name: group_name
2567
+ )
2468
2568
  return if variable_counts.nil? || variable_counts == {}
2469
2569
 
2470
- echo_commands = generate_echo_commands(variable_counts, echo_format)
2570
+ echo_commands = generate_echo_commands(
2571
+ variable_counts, formatter: echo_formatter
2572
+ )
2471
2573
 
2472
2574
  replacements = build_replacement_dictionary(
2473
2575
  echo_commands, link_state,
2474
2576
  initial_code_required: initial_code_required,
2475
- key_format: key_format
2577
+ occurrence_expressions: occurrence_expressions
2476
2578
  )
2477
2579
 
2478
2580
  return if replacements.nil?
@@ -2481,50 +2583,48 @@ module MarkdownExec
2481
2583
  expand_blocks_with_replacements(blocks, replacements)
2482
2584
  end
2483
2585
 
2484
- def export_echo_with_code_single(export_echo, inherited_code, code_lines,
2485
- required)
2486
- code = %(printf '%s' "#{export_echo}")
2487
- value = execute_temporary_script(
2488
- code,
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
2496
- end
2497
-
2498
- def export_echo_with_code(export, inherited_code, code_lines, required,
2499
- force:)
2586
+ def export_echo_with_code(
2587
+ bash_script_lines, export, force:, silent:
2588
+ )
2500
2589
  exportable = true
2590
+ command_result = nil
2591
+ new_lines = []
2501
2592
  case export.echo
2502
2593
  when String, Integer, Float, TrueClass, FalseClass
2503
- value = export_echo_with_code_single(export.echo.to_s, inherited_code,
2504
- code_lines, required)
2594
+ command_result = output_from_adhoc_bash_script_file(
2595
+ join_array_of_arrays(
2596
+ bash_script_lines,
2597
+ %(printf '%s' "#{export.echo}")
2598
+ )
2599
+ )
2600
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
2601
+ exportable = false
2602
+ command_result.warning = warning_required_empty(export) unless silent
2603
+ end
2604
+
2505
2605
  when Hash
2506
2606
  # each item in the hash is a variable name and value
2507
2607
  export.echo.each do |name, expression|
2508
- value = export_echo_with_code_single(expression, inherited_code,
2509
- code_lines, required)
2510
- ENV[name] = value.to_s
2511
- code_lines.push code_line_safe_assign(name, value, force: force)
2608
+ command_result = output_from_adhoc_bash_script_file(
2609
+ join_array_of_arrays(
2610
+ bash_script_lines,
2611
+ %(printf '%s' "#{expression}")
2612
+ )
2613
+ )
2614
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
2615
+ command_result.warning = warning_required_empty(export) unless silent
2616
+ else
2617
+ ENV[name] = command_result.stdout.to_s
2618
+ new_lines << code_line_safe_assign(name, command_result.stdout,
2619
+ force: force)
2620
+ end
2512
2621
  end
2622
+
2623
+ # individual items have been exported, none remain
2513
2624
  exportable = false
2514
2625
  end
2515
- [value, exportable]
2516
- end
2517
2626
 
2518
- def export_exec_with_code(export, inherited_code, code_lines, required)
2519
- value = execute_temporary_script(
2520
- export.exec,
2521
- (inherited_code || []) +
2522
- code_lines + required[:code]
2523
- )
2524
- if value == :invalidated
2525
- warn "A value must exist for: #{export.preconditions.join(', ')}"
2526
- end
2527
- value
2627
+ [command_result, exportable, new_lines]
2528
2628
  end
2529
2629
 
2530
2630
  # Retrieves a specific data symbol from the delegate object,
@@ -2585,6 +2685,13 @@ module MarkdownExec
2585
2685
  )
2586
2686
  end
2587
2687
 
2688
+ def find_option_by_name(name)
2689
+ name_sym = name.to_sym
2690
+ @menu_from_yaml.find do |option|
2691
+ option[:opt_name] == name_sym
2692
+ end
2693
+ end
2694
+
2588
2695
  def format_and_execute_command(
2589
2696
  code_lines:,
2590
2697
  erls:,
@@ -2604,6 +2711,24 @@ module MarkdownExec
2604
2711
  color_sym: :script_execution_frame_color)
2605
2712
  end
2606
2713
 
2714
+ def format_echo_command(payload)
2715
+ payload_match = payload.match(@delegate_object[:option_expansion_payload_regexp])
2716
+ variable = payload_match[:option]
2717
+ property = payload_match[:property]
2718
+
2719
+ echo_value = case property
2720
+ when 'default', 'description'
2721
+ item = find_option_by_name(variable)
2722
+ item ? item[property.to_sym] : ''
2723
+ when 'length'
2724
+ @delegate_object[variable.to_sym].to_s.length
2725
+ else
2726
+ @delegate_object[variable.to_sym]
2727
+ end
2728
+
2729
+ "echo #{Shellwords.escape(echo_value)}"
2730
+ end
2731
+
2607
2732
  # Format expression using environment variables and run state
2608
2733
  def format_expression(expr)
2609
2734
  data = link_load_format_data
@@ -2661,13 +2786,12 @@ module MarkdownExec
2661
2786
  color_sym: :execution_report_preview_frame_color)
2662
2787
  end
2663
2788
 
2664
- def generate_echo_commands(variable_counts, echo_format)
2789
+ def generate_echo_commands(variable_counts, formatter: nil)
2665
2790
  # commands to echo variables
2666
2791
  #
2667
2792
  commands = {}
2668
2793
  variable_counts.each_key do |variable|
2669
- command = format(echo_format, variable)
2670
- commands[variable] = command
2794
+ commands[variable] = formatter.call(variable)
2671
2795
  end
2672
2796
  commands
2673
2797
  end
@@ -2816,17 +2940,20 @@ module MarkdownExec
2816
2940
 
2817
2941
  state = initial_state
2818
2942
  selected_types = yield :filter
2943
+ index = 0
2819
2944
  cfile.readlines(
2820
2945
  @delegate_object[:filename],
2821
2946
  import_paths: options_import_paths
2822
- ).each_with_index do |nested_line, index|
2823
- next unless nested_line
2947
+ ) do |nested_line|
2948
+ next if nested_line.nil?
2824
2949
 
2825
2950
  update_line_and_block_state(
2826
2951
  nested_line, state, selected_types,
2827
- source_id: "#{@delegate_object[:filename]}_ibfnf_#{index}",
2952
+ source_id: "ItrBlkFrmNstFls:#{index}¤#{nested_line.filename}:#{nested_line.index}",
2828
2953
  &block
2829
2954
  )
2955
+
2956
+ index += 1
2830
2957
  end
2831
2958
  end
2832
2959
 
@@ -2841,15 +2968,21 @@ module MarkdownExec
2841
2968
  else
2842
2969
  iter_blocks_from_nested_files do |btype, fcb|
2843
2970
  case btype
2844
- when :blocks
2845
- yield fcb
2846
- when :filter
2847
- %i[blocks]
2971
+ when :blocks; yield fcb
2972
+ when :filter; %i[blocks]
2848
2973
  end
2849
2974
  end
2850
2975
  end
2851
2976
  end
2852
2977
 
2978
+ # join a list of arrays into a single array
2979
+ # convert single items to arrays
2980
+ def join_array_of_arrays(*args)
2981
+ args.map do |item|
2982
+ item.is_a?(Array) ? item : [item]
2983
+ end.compact.flatten(1)
2984
+ end
2985
+
2853
2986
  def link_block_data_eval(link_state, code_lines, selected, link_block_data,
2854
2987
  block_source:, shell:)
2855
2988
  all_code = HashDelegator.code_merge(link_state&.inherited_lines,
@@ -2975,7 +3108,8 @@ module MarkdownExec
2975
3108
 
2976
3109
  list = []
2977
3110
  iter_source_blocks(
2978
- @delegate_object[:list_blocks_type], source_id: source_id
3111
+ @delegate_object[:list_blocks_type],
3112
+ source_id: source_id
2979
3113
  ) do |block|
2980
3114
  list << (block_eval.present? ? eval(block_eval) : block.send(message))
2981
3115
  end
@@ -3239,14 +3373,33 @@ module MarkdownExec
3239
3373
  source_id: source_id
3240
3374
  )
3241
3375
 
3242
- ### compress empty lines
3243
3376
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
3244
- HashDelegator.tables_into_columns!(menu_blocks, @delegate_object,
3245
- screen_width_for_table)
3377
+ begin
3378
+ HashDelegator.tables_into_columns!(menu_blocks, @delegate_object,
3379
+ screen_width_for_table)
3380
+ rescue NoMethodError
3381
+ # an invalid table format
3382
+ end
3383
+ handle_consecutive_inactive_items!(menu_blocks)
3246
3384
 
3247
3385
  [all_blocks, menu_blocks, mdoc]
3248
3386
  end
3249
3387
 
3388
+ def handle_consecutive_inactive_items!(menu_blocks)
3389
+ consecutive_inactive_count = 0
3390
+ menu_blocks.each do |fcb|
3391
+ unless fcb.is_disabled?
3392
+ consecutive_inactive_count = 0
3393
+ else
3394
+ consecutive_inactive_count += 1
3395
+ if (consecutive_inactive_count % (@delegate_object[:select_page_height] / 3)).zero?
3396
+ fcb.disabled = TtyMenu::ENABLE
3397
+ fcb.is_enabled_but_inactive = true
3398
+ end
3399
+ end
3400
+ end
3401
+ end
3402
+
3250
3403
  def menu_add_disabled_option(document_glob)
3251
3404
  raise unless document_glob.present?
3252
3405
  raise if @dml_menu_blocks.nil?
@@ -3386,9 +3539,9 @@ module MarkdownExec
3386
3539
  @delegate_object[:import_paths]&.split(':') || ''
3387
3540
  end
3388
3541
 
3389
- def options_variable_expression_regexp
3390
- @delegate_object[:variable_expression_regexp].present? &&
3391
- Regexp.new(@delegate_object[:variable_expression_regexp])
3542
+ def options_variable_expansion_regexp
3543
+ @delegate_object[:variable_expansion_regexp].present? &&
3544
+ Regexp.new(@delegate_object[:variable_expansion_regexp])
3392
3545
  end
3393
3546
 
3394
3547
  def output_color_formatted(data_sym, color_sym)
@@ -3413,6 +3566,21 @@ module MarkdownExec
3413
3566
  }
3414
3567
  end
3415
3568
 
3569
+ def output_from_adhoc_bash_script_file(bash_script_lines)
3570
+ Tempfile.create('script_exec') do |temp_file|
3571
+ temp_file.write(HashDelegator.join_code_lines(bash_script_lines))
3572
+ temp_file.flush
3573
+ File.chmod(0o755, temp_file.path)
3574
+
3575
+ output = `#{temp_file.path}`
3576
+
3577
+ CommandResult.new(stdout: output, exit_status: $?.exitstatus)
3578
+ end
3579
+ rescue StandardError => err
3580
+ warn "Error executing script: #{err.message}"
3581
+ nil
3582
+ end
3583
+
3416
3584
  def output_labeled_value(label, value, level)
3417
3585
  @fout.lout format_references_send_color(
3418
3586
  context: {
@@ -3430,9 +3598,7 @@ module MarkdownExec
3430
3598
  end
3431
3599
 
3432
3600
  def persist_fcb(options)
3433
- MarkdownExec::FCB.new(options).tap do |fcb|
3434
- @fcb_store << fcb
3435
- end
3601
+ HashDelegator.persist_fcb_self(@fcb_store, options)
3436
3602
  end
3437
3603
 
3438
3604
  def pop_add_current_code_to_head_and_trigger_load(
@@ -4097,7 +4263,7 @@ module MarkdownExec
4097
4263
  # Presents a TTY prompt to select an option or exit,
4098
4264
  # returns metadata including option and selected
4099
4265
  def select_option_with_metadata(
4100
- prompt_text, menu_items, opts = {}, menu_blocks: nil
4266
+ prompt_text, tty_menu_items, opts = {}, menu_blocks: nil
4101
4267
  )
4102
4268
  @dml_menu_blocks = menu_blocks if menu_blocks
4103
4269
 
@@ -4120,10 +4286,10 @@ module MarkdownExec
4120
4286
  per_page: @delegate_object[:select_page_height]
4121
4287
  }.freeze
4122
4288
 
4123
- if menu_items.all? do |item|
4289
+ if tty_menu_items.all? do |item|
4124
4290
  !item.is_a?(String) && item[:disabled]
4125
4291
  end
4126
- menu_items.each do |prompt_item|
4292
+ tty_menu_items.each do |prompt_item|
4127
4293
  puts prompt_item[:dname]
4128
4294
  end
4129
4295
  return
@@ -4133,19 +4299,21 @@ module MarkdownExec
4133
4299
  # crashes if default is not an existing item
4134
4300
  #
4135
4301
  selection = @prompt.select(prompt_text,
4136
- menu_items,
4302
+ tty_menu_items,
4137
4303
  opts.merge(props))
4138
4304
  rescue TTY::Prompt::ConfigurationError
4139
4305
  # prompt fails when collapsible block name has changed; clear default
4140
4306
  selection = @prompt.select(prompt_text,
4141
- menu_items,
4307
+ tty_menu_items,
4142
4308
  opts.merge(props).merge(default: nil))
4143
4309
  rescue NoMethodError
4144
4310
  # no enabled options in page
4145
4311
  return
4146
4312
  end
4147
4313
 
4148
- selected = @dml_menu_blocks.find do |item|
4314
+ menu_list = opts.fetch(:match_dml, true) ? @dml_menu_blocks : menu_items
4315
+ menu_list ||= tty_menu_items
4316
+ selected = menu_list.find do |item|
4149
4317
  if item.instance_of?(Hash)
4150
4318
  [item[:id], item[:name], item[:dname]].include?(selection)
4151
4319
  elsif item.instance_of?(MarkdownExec::FCB)
@@ -4155,7 +4323,15 @@ module MarkdownExec
4155
4323
  end
4156
4324
  end
4157
4325
 
4326
+ # new FCB if selected is not an object
4327
+ if selected.instance_of?(String)
4328
+ selected = FCB.new(dname: selected)
4329
+ elsif selected.instance_of?(Hash)
4330
+ selected = FCB.new(selected)
4331
+ end
4332
+
4158
4333
  unless selected
4334
+ report_and_reraise('menu item not found')
4159
4335
  HashDelegator.error_handler('select_option_with_metadata',
4160
4336
  error: 'menu item not found')
4161
4337
  exit 1
@@ -4407,164 +4583,193 @@ module MarkdownExec
4407
4583
  @delegate_object.merge!(options)
4408
4584
  end
4409
4585
 
4410
- def ux_block_export_activated(export, inherited_code, code_lines, required,
4411
- exit_prompt)
4586
+ def ux_block_export_activated(
4587
+ bash_script_lines, export, exit_prompt
4588
+ )
4412
4589
  exportable = true
4413
4590
  transformable = true
4414
- [if export.allowed.present?
4415
- if export.allowed == :echo
4416
- output, exportable = export_echo_with_code(
4417
- export, inherited_code, code_lines, required, force: true
4418
- )
4419
- return :ux_exec_prohibited if output == :invalidated
4420
-
4421
- menu_from_list_with_back(output.split("\n"))
4422
-
4423
- elsif export.allowed == :exec
4424
- output = process_allowed_exec(export, inherited_code,
4425
- code_lines, required)
4426
- return :ux_exec_prohibited if output == :ux_exec_prohibited
4427
-
4428
- menu_from_list_with_back(output)
4429
-
4430
- else
4431
- menu_from_list_with_back(export.allowed)
4432
- end
4433
-
4434
- # echo > exec
4435
- elsif export.echo
4436
- output, exportable = export_echo_with_code(
4437
- export, inherited_code, code_lines, required, force: true
4438
- )
4439
- return :ux_exec_prohibited if output == :invalidated
4440
-
4441
- output
4442
-
4443
- # exec > allowed
4444
- elsif export.exec
4445
- output = export_exec_with_code(
4446
- export, inherited_code, code_lines, required
4447
- )
4448
- return :ux_exec_prohibited if output == :invalidated
4449
-
4450
- output
4451
-
4452
- # allowed > prompt
4453
- elsif export.allowed && export.allowed.count.positive?
4454
- case (choice = prompt_select_from_list(
4455
- [exit_prompt] + export.allowed,
4456
- string: export.prompt,
4457
- color_sym: :prompt_color_after_script_execution
4458
- ))
4459
- when exit_prompt
4460
- exportable = false
4461
- transformable = false
4462
- nil
4463
- else
4464
- choice
4465
- end
4466
-
4467
- # prompt > default
4468
- elsif export.prompt.present?
4469
- begin
4470
- loop do
4471
- print "#{export.prompt} [#{export.default}]: "
4472
- output = gets.chomp
4473
- output = export.default.to_s if output.empty?
4474
- caps = NamedCaptureExtractor.extract_named_groups(output,
4475
- export.validate)
4476
- break if caps
4477
-
4478
- # invalid input, retry
4479
- end
4480
- rescue Interrupt
4481
- exportable = false
4482
- transformable = false
4483
- end
4484
-
4485
- output
4486
-
4487
- # default
4488
- else
4489
- transformable = false
4490
- export.default
4491
- end, exportable, transformable]
4492
- end
4493
-
4494
- def ux_block_export_automatic(export, inherited_code, code_lines, required)
4495
- transformable = true
4496
- exportable = true
4591
+ new_lines = []
4592
+ command_result = nil
4593
+
4594
+ case as = FCB.act_source(export)
4595
+ when false, UxActSource::FALSE
4596
+ raise 'Should not be reached.'
4597
+
4598
+ when ':allow', UxActSource::ALLOW
4599
+ raise unless export.allow.present?
4600
+
4601
+ case export.allow
4602
+ when :echo, ExportValueSource::ECHO
4603
+ command_result, exportable, new_lines = export_echo_with_code(
4604
+ bash_script_lines,
4605
+ export,
4606
+ force: true,
4607
+ silent: false
4608
+ )
4609
+ if command_result.failure?
4610
+ command_result
4611
+ else
4612
+ command_result = CommandResult.new(
4613
+ stdout: menu_from_list_with_back(command_result.stdout.split("\n"))
4614
+ )
4615
+ end
4497
4616
 
4498
- if export.default == false
4499
- exportable = false
4500
- transformable = false
4501
- value = nil
4502
- else
4503
- if export.default.nil?
4504
- if export.echo.present?
4505
- export.default = :echo
4506
- elsif export.exec.present?
4507
- export.default = :exec
4508
- elsif export.allowed.present?
4509
- export.default = export.allowed.first
4617
+ when ':exec', UxActSource::EXEC
4618
+ command_result = output_from_adhoc_bash_script_file(
4619
+ join_array_of_arrays(bash_script_lines, export.exec)
4620
+ )
4621
+
4622
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
4623
+ command_result
4624
+ else
4625
+ command_result = CommandResult.new(
4626
+ stdout: menu_from_list_with_back(
4627
+ command_result.stdout.split("\n")
4628
+ )
4629
+ )
4510
4630
  end
4631
+
4632
+ else
4633
+ command_result = CommandResult.new(
4634
+ stdout: menu_from_list_with_back(export.allow)
4635
+ )
4511
4636
  end
4512
4637
 
4513
- value = case export.default
4514
- when :allowed
4515
- raise unless export.allowed.present?
4638
+ when ':echo', UxActSource::ECHO
4639
+ command_result, exportable, new_lines = export_echo_with_code(
4640
+ bash_script_lines,
4641
+ export,
4642
+ force: true,
4643
+ silent: false
4644
+ )
4516
4645
 
4517
- if export.allowed == :echo
4518
- output, exportable = export_echo_with_code(
4519
- export, inherited_code, code_lines, required, force: false
4520
- )
4521
- return :ux_exec_prohibited if output == :invalidated
4646
+ command_result
4522
4647
 
4523
- exportable && output.split("\n").first
4648
+ when ':edit', UxActSource::EDIT
4649
+ output = nil
4650
+ begin
4651
+ loop do
4652
+ print "#{export.prompt} [#{export.default}]: "
4653
+ output = gets.chomp
4654
+ output = export.default.to_s if output.empty?
4655
+ caps = NamedCaptureExtractor.extract_named_groups(output,
4656
+ export.validate)
4657
+ break if caps
4524
4658
 
4525
- elsif export.allowed == :exec
4526
- output = process_allowed_exec(export, inherited_code,
4527
- code_lines, required)
4528
- return :ux_exec_prohibited if output == :ux_exec_prohibited
4659
+ # invalid input, retry
4660
+ end
4661
+ rescue Interrupt
4662
+ exportable = false
4663
+ transformable = false
4664
+ end
4529
4665
 
4530
- output.first
4666
+ command_result = CommandResult.new(stdout: output)
4531
4667
 
4532
- else
4533
- export.allowed.first
4534
- end
4668
+ when ':exec', UxActSource::EXEC
4669
+ command_result = output_from_adhoc_bash_script_file(
4670
+ join_array_of_arrays(bash_script_lines, export.exec)
4671
+ )
4535
4672
 
4536
- # echo > default
4537
- when :echo
4538
- raise unless export.echo.present?
4673
+ command_result
4539
4674
 
4540
- output, exportable = export_echo_with_code(
4541
- export, inherited_code, code_lines, required, force: false
4542
- )
4543
- return :ux_exec_prohibited if output == :invalidated
4675
+ else
4676
+ transformable = false
4677
+ command_result = CommandResult.new(stdout: export.default.to_s)
4678
+ end
4544
4679
 
4545
- output
4680
+ # add message for required variables
4681
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
4682
+ command_result.warning = warning_required_empty(export)
4683
+ # warn command_result.warning
4684
+ end
4546
4685
 
4547
- # exec > default
4548
- when :exec
4549
- raise unless export.exec.present?
4686
+ command_result.exportable = exportable
4687
+ command_result.transformable = transformable
4688
+ command_result.new_lines = new_lines
4689
+ command_result
4690
+ end
4550
4691
 
4551
- output = export_exec_with_code(
4552
- export, inherited_code, code_lines, required
4553
- )
4554
- return :ux_exec_prohibited if output == :invalidated
4692
+ def ux_block_export_automatic(bash_script_lines, export)
4693
+ transformable = true
4694
+ exportable = true
4695
+ new_lines = []
4696
+ command_result = nil
4697
+ silent = true
4555
4698
 
4556
- output
4699
+ case FCB.init_source(export)
4700
+ when false, UxActSource::FALSE
4701
+ exportable = false
4702
+ transformable = false
4703
+ command_result = CommandResult.new
4704
+
4705
+ when ':allow', UxActSource::ALLOW
4706
+ raise unless export.allow.present?
4707
+
4708
+ case export.allow
4709
+ when :echo, ExportValueSource::ECHO
4710
+ command_result, exportable, new_lines = export_echo_with_code(
4711
+ bash_script_lines,
4712
+ export,
4713
+ force: false,
4714
+ silent: silent
4715
+ )
4716
+ unless command_result.failure?
4717
+ command_result.stdout = (exportable && command_result.stdout.split("\n").first) || ''
4718
+ end
4557
4719
 
4558
- # default
4559
- else
4560
- transformable = false
4561
- export.default.to_s
4562
- end
4720
+ when :exec, ExportValueSource::EXEC
4721
+ command_result = output_from_adhoc_bash_script_file(
4722
+ join_array_of_arrays(bash_script_lines, export.exec)
4723
+ )
4724
+ unless command_result.failure?
4725
+ command_result.stdout = command_result.stdout.split("\n").first
4726
+ end
4727
+
4728
+ else
4729
+ # must be a list
4730
+ command_result = CommandResult.new(stdout: export.allow.first)
4731
+ end
4732
+
4733
+ when ':default', UxActSource::DEFAULT
4734
+ transformable = false
4735
+ command_result = CommandResult.new(stdout: export.default.to_s)
4736
+
4737
+ when ':echo', UxActSource::ECHO
4738
+ raise unless export.echo.present?
4739
+
4740
+ command_result, exportable, new_lines = export_echo_with_code(
4741
+ bash_script_lines,
4742
+ export,
4743
+ force: false,
4744
+ silent: silent
4745
+ )
4746
+
4747
+ when ':exec', UxActSource::EXEC
4748
+ raise unless export.exec.present?
4749
+
4750
+ command_result = output_from_adhoc_bash_script_file(
4751
+ join_array_of_arrays(bash_script_lines, export.exec)
4752
+ )
4753
+
4754
+ else
4755
+ command_result = CommandResult.new(stdout: export.init.to_s)
4756
+ # raise "Unknown FCB.init_source(export) #{FCB.init_source(export)}"
4563
4757
  end
4564
4758
 
4565
- [value,
4566
- exportable,
4567
- transformable]
4759
+ # add message for required variables
4760
+ if command_result.exit_status == EXIT_STATUS_REQUIRED_EMPTY
4761
+ command_result.warning = warning_required_empty(export)
4762
+ warn command_result.warning unless silent
4763
+ end
4764
+
4765
+ command_result.exportable = exportable
4766
+ command_result.transformable = transformable
4767
+ command_result.new_lines = new_lines
4768
+ command_result
4769
+ end
4770
+
4771
+ def warning_required_empty(export)
4772
+ "A value must exist for: #{export.required.join(', ')}"
4568
4773
  end
4569
4774
 
4570
4775
  def vux_await_user_selection(prior_answer: @dml_block_selection)
@@ -4791,7 +4996,8 @@ module MarkdownExec
4791
4996
  #
4792
4997
  # @return [Nil] Returns nil if no code block is selected
4793
4998
  # or an error occurs.
4794
- def vux_main_loop
4999
+ def vux_main_loop(menu_from_yaml: nil)
5000
+ @menu_from_yaml = menu_from_yaml
4795
5001
  vux_init
4796
5002
  vux_load_code_files_into_state
4797
5003
  formatted_choice_ostructs = vux_formatted_names_for_state_chrome_blocks
@@ -4841,10 +5047,10 @@ module MarkdownExec
4841
5047
  case msg
4842
5048
  when :parse_document # once for each menu
4843
5049
  count = 0
4844
- vux_parse_document(source_id: "#{@delegate_object[:filename]}_vmlpd")
5050
+ vux_parse_document(source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®PrsDoc")
4845
5051
  vux_menu_append_history_files(
4846
5052
  formatted_choice_ostructs,
4847
- source_id: "#{@delegate_object[:filename]}_vmlhf"
5053
+ source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®HstFls"
4848
5054
  )
4849
5055
  vux_publish_document_file_name_for_external_automation
4850
5056
 
@@ -4856,7 +5062,7 @@ module MarkdownExec
4856
5062
  # yield :end_of_cli, @delegate_object
4857
5063
 
4858
5064
  if @delegate_object[:list_blocks]
4859
- list_blocks(source_id: "#{@delegate_object[:filename]}_vmleoc")
5065
+ list_blocks(source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®EndCLI")
4860
5066
  :exit
4861
5067
  end
4862
5068
 
@@ -5001,6 +5207,7 @@ module MarkdownExec
5001
5207
  mdoc_menu_and_blocks_from_nested_files(
5002
5208
  @dml_link_state, source_id: source_id
5003
5209
  )
5210
+
5004
5211
  dump_delobj(@dml_blocks_in_file, @dml_menu_blocks, @dml_link_state)
5005
5212
  end
5006
5213
 
@@ -5081,8 +5288,8 @@ module MarkdownExec
5081
5288
  :prompt_color_after_script_execution
5082
5289
  )
5083
5290
 
5084
- menu_items = blocks_as_menu_items(menu_blocks)
5085
- if menu_items.empty?
5291
+ tty_menu_items = blocks_as_menu_items(menu_blocks)
5292
+ if tty_menu_items.empty?
5086
5293
  return SelectedBlockMenuState.new(nil, OpenStruct.new,
5087
5294
  MenuState::EXIT)
5088
5295
  end
@@ -5117,8 +5324,9 @@ module MarkdownExec
5117
5324
  { cycle: @delegate_object[:select_page_cycle],
5118
5325
  per_page: @delegate_object[:select_page_height] }
5119
5326
  )
5120
- selected_option = select_option_with_metadata(prompt_title, menu_items,
5121
- selection_opts)
5327
+ selected_option = select_option_with_metadata(
5328
+ prompt_title, tty_menu_items, selection_opts
5329
+ )
5122
5330
  determine_block_state(selected_option)
5123
5331
  end
5124
5332
 
@@ -5192,9 +5400,13 @@ module MarkdownExec
5192
5400
  save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
5193
5401
  if save_expr.present?
5194
5402
  save_filespec = save_filespec_from_expression(save_expr)
5195
- File.write(save_filespec,
5196
- HashDelegator.join_code_lines(link_state&.inherited_lines))
5197
- @delegate_object[:filename]
5403
+ if save_filespec.present?
5404
+ File.write(save_filespec,
5405
+ HashDelegator.join_code_lines(link_state&.inherited_lines))
5406
+ @delegate_object[:filename]
5407
+ else
5408
+ link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
5409
+ end
5198
5410
  else
5199
5411
  link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
5200
5412
  end
@@ -5202,6 +5414,8 @@ module MarkdownExec
5202
5414
  end
5203
5415
 
5204
5416
  class HashDelegator < HashDelegatorParent
5417
+ include ::ErrorReporting
5418
+
5205
5419
  # Cleans a value, handling both Hash and Struct types.
5206
5420
  # For Structs, the cleaned version is converted to a hash.
5207
5421
  def self.clean_value(value)
@@ -5414,7 +5628,7 @@ module MarkdownExec
5414
5628
  input: MarkdownExec::FCB.new(title: '',
5415
5629
  body: ['def add(x, y)',
5416
5630
  ' x + y', 'end']),
5417
- output: "def add(x, y)\n x + y\n end\n"
5631
+ output: "def add(x, y)\n x + y\n end"
5418
5632
  },
5419
5633
  {
5420
5634
  input: MarkdownExec::FCB.new(title: 'foo', body: %w[bar baz]),