markdown_exec 2.8.5 → 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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -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-preconditions.bats → block-type-ux-required-variables.bats} +1 -1
  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/indented-block-type-vars.bats +1 -1
  18. data/bats/markup.bats +1 -1
  19. data/bats/option-expansion.bats +8 -0
  20. data/bats/table-column-truncate.bats +1 -1
  21. data/bats/test_helper.bash +50 -5
  22. data/docs/dev/bats-document-configuration.md +1 -1
  23. data/docs/dev/block-type-ux-allowed.md +5 -7
  24. data/docs/dev/block-type-ux-auto.md +8 -5
  25. data/docs/dev/block-type-ux-chained.md +4 -2
  26. data/docs/dev/block-type-ux-echo-hash.md +6 -7
  27. data/docs/dev/block-type-ux-echo.md +2 -2
  28. data/docs/dev/block-type-ux-exec.md +3 -5
  29. data/docs/dev/block-type-ux-hidden.md +3 -0
  30. data/docs/dev/{block-type-ux-preconditions.md → block-type-ux-required-variables.md} +1 -2
  31. data/docs/dev/block-type-ux-row-format.md +3 -4
  32. data/docs/dev/block-type-ux-sources.md +57 -0
  33. data/docs/dev/block-type-ux-transform.md +0 -4
  34. data/docs/dev/command-substitution-options.md +61 -0
  35. data/docs/dev/indented-block-type-vars.md +1 -0
  36. data/docs/dev/menu-pagination-indent.md +123 -0
  37. data/docs/dev/menu-pagination.md +111 -0
  38. data/docs/dev/option-expansion.md +10 -0
  39. data/lib/ansi_formatter.rb +2 -0
  40. data/lib/block_cache.rb +197 -0
  41. data/lib/command_result.rb +57 -0
  42. data/lib/constants.rb +18 -0
  43. data/lib/error_reporting.rb +38 -0
  44. data/lib/evaluate_shell_expressions.rb +43 -18
  45. data/lib/fcb.rb +98 -7
  46. data/lib/hash_delegator.rb +526 -322
  47. data/lib/markdown_exec/version.rb +1 -1
  48. data/lib/markdown_exec.rb +136 -45
  49. data/lib/mdoc.rb +59 -10
  50. data/lib/menu.src.yml +23 -11
  51. data/lib/menu.yml +22 -12
  52. data/lib/value_or_exception.rb +76 -0
  53. metadata +16 -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,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,24 @@ 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
1101
  only_default: true
1027
1102
  )
1028
- if code == :ux_exec_prohibited
1103
+ if command_result_w_e_t_nl.failure?
1029
1104
  merged_options
1030
1105
  else
1031
- merged_options.push(code)
1106
+ merged_options.push(command_result_w_e_t_nl.stdout)
1032
1107
  end
1033
1108
  end).to_a
1034
1109
  end
@@ -1048,7 +1123,7 @@ module MarkdownExec
1048
1123
  )
1049
1124
 
1050
1125
  # process each ux block in sequence, setting ENV and collecting lines
1051
- code_lines = []
1126
+ required_lines = []
1052
1127
  required[:blocks].each do |block|
1053
1128
  next unless block.type == BlockType::UX
1054
1129
 
@@ -1059,35 +1134,51 @@ module MarkdownExec
1059
1134
  prompt: @delegate_object[:prompt_ux_enter_a_value],
1060
1135
  validate: '^(?<name>[^ ].*)$'
1061
1136
  )
1137
+ block.export = export
1138
+ block.export_act = FCB.act_source(export)
1139
+ block.export_init = FCB.init_source(export)
1062
1140
 
1063
- # 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.
1064
1142
  # if any precondition is not set, the sequence is aborted.
1065
- export.preconditions&.each do |precondition|
1066
- 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}"
1067
1146
  end
1068
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
+ )
1069
1154
  if only_default
1070
- value, exportable, transformable =
1071
- ux_block_export_automatic(
1072
- export, inherited_code, code_lines, required
1073
- )
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
+
1074
1160
  else
1075
- value, exportable, transformable =
1076
- ux_block_export_activated(
1077
- export, inherited_code, code_lines, required, exit_prompt
1078
- )
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
1079
1167
  end
1080
- 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?
1081
1169
 
1082
- if SelectResponse.continue?(value)
1083
- if transformable
1084
- 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
+ )
1085
1176
  end
1086
1177
 
1087
- if exportable
1088
- ENV[export.name] = value.to_s
1089
- code_lines.push code_line_safe_assign(export.name, value,
1090
- 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)
1091
1182
  end
1092
1183
  end
1093
1184
  else
@@ -1095,7 +1186,7 @@ module MarkdownExec
1095
1186
  end
1096
1187
  end
1097
1188
 
1098
- code_lines
1189
+ CommandResult.new(stdout: required_lines)
1099
1190
  end
1100
1191
 
1101
1192
  # sets ENV
@@ -1331,7 +1422,9 @@ module MarkdownExec
1331
1422
  )
1332
1423
  # Initialize a counter for named group occurrences
1333
1424
  occurrence_count = Hash.new(0)
1334
- return occurrence_count if pattern.nil? || pattern == //
1425
+ occurrence_expressions = {}
1426
+ return [occurrence_count,
1427
+ occurrence_expressions] if pattern.nil? || pattern == //
1335
1428
 
1336
1429
  blocks.each do |block|
1337
1430
  # Skip processing for shell-type blocks
@@ -1340,11 +1433,13 @@ module MarkdownExec
1340
1433
  # Scan each block name for matches of the pattern
1341
1434
  count_named_group_occurrences_block_body_fix_indent(block).scan(pattern) do |(_, _variable_name)|
1342
1435
  pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1343
- 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']
1344
1439
  end
1345
1440
  end
1346
1441
 
1347
- occurrence_count
1442
+ [occurrence_count, occurrence_expressions]
1348
1443
  end
1349
1444
 
1350
1445
  def count_named_group_occurrences_block_body_fix_indent(block)
@@ -1459,17 +1554,17 @@ module MarkdownExec
1459
1554
  fcb.center = center
1460
1555
  fcb.chrome = true
1461
1556
  fcb.collapse = collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse
1462
- fcb.token = line_obj[:collapse]
1463
1557
  fcb.disabled = disabled ? TtyMenu::DISABLE : nil
1558
+ fcb.dname = line_obj[:indent] + decorated
1464
1559
  fcb.id = "#{id}.#{index}"
1560
+ fcb.indent = line_obj[:indent]
1465
1561
  fcb.level = level
1562
+ fcb.oname = line_obj[:text]
1466
1563
  fcb.s0indent = indent
1467
1564
  fcb.s0printable = line_obj[:text]
1468
1565
  fcb.s1decorated = decorated
1469
- fcb.dname = line_obj[:indent] + decorated
1470
- fcb.indent = line_obj[:indent]
1471
- fcb.oname = line_obj[:text]
1472
1566
  fcb.text = line_obj[:text]
1567
+ fcb.token = line_obj[:collapse]
1473
1568
  fcb.type = type
1474
1569
  use_fcb = false # next line is new record
1475
1570
  else
@@ -1477,17 +1572,17 @@ module MarkdownExec
1477
1572
  center: center,
1478
1573
  chrome: true,
1479
1574
  collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
1480
- token: line_obj[:collapse],
1481
1575
  disabled: disabled ? TtyMenu::DISABLE : nil,
1576
+ dname: line_obj[:indent] + decorated,
1482
1577
  id: "#{id}.#{index}",
1578
+ indent: line_obj[:indent],
1483
1579
  level: level,
1580
+ oname: line_obj[:text],
1484
1581
  s0indent: indent,
1485
1582
  s0printable: line_obj[:text],
1486
1583
  s1decorated: decorated,
1487
- dname: line_obj[:indent] + decorated,
1488
- indent: line_obj[:indent],
1489
- oname: line_obj[:text],
1490
1584
  text: line_obj[:text],
1585
+ token: line_obj[:collapse],
1491
1586
  type: type
1492
1587
  )
1493
1588
  end
@@ -1921,14 +2016,16 @@ module MarkdownExec
1921
2016
 
1922
2017
  elsif selected.type == BlockType::UX
1923
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?
1924
2025
  next_state_append_code(
1925
2026
  selected,
1926
2027
  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
- )
2028
+ command_result_w_e_t_nl.failure? ? [] : command_result_w_e_t_nl.stdout
1932
2029
  )
1933
2030
 
1934
2031
  elsif selected.type == BlockType::VARS
@@ -1977,6 +2074,11 @@ module MarkdownExec
1977
2074
  @dml_block_state = find_block_state_by_name(block_name)
1978
2075
  dump_and_warn_block_state(name: block_name,
1979
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
+
1980
2082
  next_block_state =
1981
2083
  execute_block_for_state_and_name(
1982
2084
  selected: @dml_block_state.block,
@@ -2150,7 +2252,7 @@ module MarkdownExec
2150
2252
  end
2151
2253
  elsif (selected_option = select_option_with_metadata(
2152
2254
  prompt_title,
2153
- [exit_prompt] + dirs.map do |file|
2255
+ [exit_prompt] + dirs.map do |file| # tty_menu_items
2154
2256
  { name:
2155
2257
  format(
2156
2258
  block_data['view'] || view,
@@ -2163,7 +2265,8 @@ module MarkdownExec
2163
2265
  oname: file }
2164
2266
  end,
2165
2267
  menu_options.merge(
2166
- cycle: true
2268
+ cycle: true,
2269
+ match_dml: false
2167
2270
  )
2168
2271
  ))
2169
2272
  if selected_option.dname != exit_prompt
@@ -2339,9 +2442,9 @@ module MarkdownExec
2339
2442
  def execute_inherited_save(
2340
2443
  code_lines: @dml_link_state.inherited_lines
2341
2444
  )
2342
- return unless (save_filespec = save_filespec_from_expression)
2343
-
2344
- 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
+ ))
2345
2448
 
2346
2449
  unless write_file_with_directory_creation(
2347
2450
  content: HashDelegator.join_code_lines(code_lines),
@@ -2397,27 +2500,6 @@ module MarkdownExec
2397
2500
  post_execution_process
2398
2501
  end
2399
2502
 
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
2503
  def expand_blocks_with_replacements(
2422
2504
  menu_blocks, replacements, exclude_types: [BlockType::SHELL]
2423
2505
  )
@@ -2438,17 +2520,32 @@ module MarkdownExec
2438
2520
  def expand_references!(fcb, link_state)
2439
2521
  expand_variable_references!(
2440
2522
  blocks: [fcb],
2523
+ echo_formatter: method(:format_echo_command),
2524
+ group_name: :payload,
2441
2525
  initial_code_required: false,
2442
- key_format: @delegate_object[:variable_expression_format],
2443
2526
  link_state: link_state,
2444
- pattern: options_variable_expression_regexp
2527
+ pattern: @delegate_object[:option_expansion_expression_regexp].present? &&
2528
+ Regexp.new(@delegate_object[:option_expansion_expression_regexp])
2445
2529
  )
2530
+
2531
+ # variable expansions
2446
2532
  expand_variable_references!(
2447
2533
  blocks: [fcb],
2448
- echo_format: '%s',
2449
- group_name: :command,
2534
+ echo_formatter: lambda do |variable|
2535
+ %(echo "$#{variable}")
2536
+ end,
2537
+ group_name: @delegate_object[:variable_expansion_name_capture_group]&.to_sym,
2538
+ initial_code_required: false,
2539
+ link_state: link_state,
2540
+ pattern: options_variable_expansion_regexp
2541
+ )
2542
+
2543
+ # command substitutions
2544
+ expand_variable_references!(
2545
+ blocks: [fcb],
2546
+ echo_formatter: lambda { |command| command },
2547
+ group_name: @delegate_object[:command_substitution_name_capture_group]&.to_sym,
2450
2548
  initial_code_required: false,
2451
- key_format: @delegate_object[:command_substitution_format],
2452
2549
  link_state: link_state,
2453
2550
  pattern: options_command_substitution_regexp
2454
2551
  )
@@ -2456,23 +2553,25 @@ module MarkdownExec
2456
2553
 
2457
2554
  def expand_variable_references!(
2458
2555
  blocks:,
2459
- echo_format: 'echo "$%s"',
2460
- group_name: :variable,
2556
+ echo_formatter:,
2557
+ group_name:,
2461
2558
  initial_code_required: false,
2462
- key_format:,
2463
2559
  link_state:,
2464
2560
  pattern:
2465
2561
  )
2466
- variable_counts = count_named_group_occurrences(blocks, pattern,
2467
- group_name: group_name)
2562
+ variable_counts, occurrence_expressions = count_named_group_occurrences(
2563
+ blocks, pattern, group_name: group_name
2564
+ )
2468
2565
  return if variable_counts.nil? || variable_counts == {}
2469
2566
 
2470
- echo_commands = generate_echo_commands(variable_counts, echo_format)
2567
+ echo_commands = generate_echo_commands(
2568
+ variable_counts, formatter: echo_formatter
2569
+ )
2471
2570
 
2472
2571
  replacements = build_replacement_dictionary(
2473
2572
  echo_commands, link_state,
2474
2573
  initial_code_required: initial_code_required,
2475
- key_format: key_format
2574
+ occurrence_expressions: occurrence_expressions
2476
2575
  )
2477
2576
 
2478
2577
  return if replacements.nil?
@@ -2481,50 +2580,48 @@ module MarkdownExec
2481
2580
  expand_blocks_with_replacements(blocks, replacements)
2482
2581
  end
2483
2582
 
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:)
2583
+ def export_echo_with_code(
2584
+ bash_script_lines, export, force:
2585
+ )
2500
2586
  exportable = true
2587
+ command_result = nil
2588
+ new_lines = []
2501
2589
  case export.echo
2502
2590
  when String, Integer, Float, TrueClass, FalseClass
2503
- value = export_echo_with_code_single(export.echo.to_s, inherited_code,
2504
- code_lines, required)
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
+
2505
2602
  when Hash
2506
2603
  # each item in the hash is a variable name and value
2507
2604
  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)
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
2512
2618
  end
2619
+
2620
+ # individual items have been exported, none remain
2513
2621
  exportable = false
2514
2622
  end
2515
- [value, exportable]
2516
- end
2517
2623
 
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
2624
+ [command_result, exportable, new_lines]
2528
2625
  end
2529
2626
 
2530
2627
  # Retrieves a specific data symbol from the delegate object,
@@ -2585,6 +2682,13 @@ module MarkdownExec
2585
2682
  )
2586
2683
  end
2587
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
+
2588
2692
  def format_and_execute_command(
2589
2693
  code_lines:,
2590
2694
  erls:,
@@ -2604,6 +2708,24 @@ module MarkdownExec
2604
2708
  color_sym: :script_execution_frame_color)
2605
2709
  end
2606
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
+
2607
2729
  # Format expression using environment variables and run state
2608
2730
  def format_expression(expr)
2609
2731
  data = link_load_format_data
@@ -2661,13 +2783,12 @@ module MarkdownExec
2661
2783
  color_sym: :execution_report_preview_frame_color)
2662
2784
  end
2663
2785
 
2664
- def generate_echo_commands(variable_counts, echo_format)
2786
+ def generate_echo_commands(variable_counts, formatter: nil)
2665
2787
  # commands to echo variables
2666
2788
  #
2667
2789
  commands = {}
2668
2790
  variable_counts.each_key do |variable|
2669
- command = format(echo_format, variable)
2670
- commands[variable] = command
2791
+ commands[variable] = formatter.call(variable)
2671
2792
  end
2672
2793
  commands
2673
2794
  end
@@ -2824,7 +2945,7 @@ module MarkdownExec
2824
2945
 
2825
2946
  update_line_and_block_state(
2826
2947
  nested_line, state, selected_types,
2827
- source_id: "#{@delegate_object[:filename]}_ibfnf_#{index}",
2948
+ source_id: "#{@delegate_object[:filename]}¤ItrBlkFrmNstFls:#{index}",
2828
2949
  &block
2829
2950
  )
2830
2951
  end
@@ -2850,6 +2971,14 @@ module MarkdownExec
2850
2971
  end
2851
2972
  end
2852
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
+
2853
2982
  def link_block_data_eval(link_state, code_lines, selected, link_block_data,
2854
2983
  block_source:, shell:)
2855
2984
  all_code = HashDelegator.code_merge(link_state&.inherited_lines,
@@ -2975,7 +3104,8 @@ module MarkdownExec
2975
3104
 
2976
3105
  list = []
2977
3106
  iter_source_blocks(
2978
- @delegate_object[:list_blocks_type], source_id: source_id
3107
+ @delegate_object[:list_blocks_type],
3108
+ source_id: source_id
2979
3109
  ) do |block|
2980
3110
  list << (block_eval.present? ? eval(block_eval) : block.send(message))
2981
3111
  end
@@ -3239,14 +3369,33 @@ module MarkdownExec
3239
3369
  source_id: source_id
3240
3370
  )
3241
3371
 
3242
- ### compress empty lines
3243
3372
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
3244
- HashDelegator.tables_into_columns!(menu_blocks, @delegate_object,
3245
- 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)
3246
3380
 
3247
3381
  [all_blocks, menu_blocks, mdoc]
3248
3382
  end
3249
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
+
3250
3399
  def menu_add_disabled_option(document_glob)
3251
3400
  raise unless document_glob.present?
3252
3401
  raise if @dml_menu_blocks.nil?
@@ -3386,9 +3535,9 @@ module MarkdownExec
3386
3535
  @delegate_object[:import_paths]&.split(':') || ''
3387
3536
  end
3388
3537
 
3389
- def options_variable_expression_regexp
3390
- @delegate_object[:variable_expression_regexp].present? &&
3391
- 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])
3392
3541
  end
3393
3542
 
3394
3543
  def output_color_formatted(data_sym, color_sym)
@@ -3413,6 +3562,21 @@ module MarkdownExec
3413
3562
  }
3414
3563
  end
3415
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
+
3416
3580
  def output_labeled_value(label, value, level)
3417
3581
  @fout.lout format_references_send_color(
3418
3582
  context: {
@@ -3430,9 +3594,7 @@ module MarkdownExec
3430
3594
  end
3431
3595
 
3432
3596
  def persist_fcb(options)
3433
- MarkdownExec::FCB.new(options).tap do |fcb|
3434
- @fcb_store << fcb
3435
- end
3597
+ HashDelegator.persist_fcb_self(@fcb_store, options)
3436
3598
  end
3437
3599
 
3438
3600
  def pop_add_current_code_to_head_and_trigger_load(
@@ -4097,7 +4259,7 @@ module MarkdownExec
4097
4259
  # Presents a TTY prompt to select an option or exit,
4098
4260
  # returns metadata including option and selected
4099
4261
  def select_option_with_metadata(
4100
- prompt_text, menu_items, opts = {}, menu_blocks: nil
4262
+ prompt_text, tty_menu_items, opts = {}, menu_blocks: nil
4101
4263
  )
4102
4264
  @dml_menu_blocks = menu_blocks if menu_blocks
4103
4265
 
@@ -4120,10 +4282,10 @@ module MarkdownExec
4120
4282
  per_page: @delegate_object[:select_page_height]
4121
4283
  }.freeze
4122
4284
 
4123
- if menu_items.all? do |item|
4285
+ if tty_menu_items.all? do |item|
4124
4286
  !item.is_a?(String) && item[:disabled]
4125
4287
  end
4126
- menu_items.each do |prompt_item|
4288
+ tty_menu_items.each do |prompt_item|
4127
4289
  puts prompt_item[:dname]
4128
4290
  end
4129
4291
  return
@@ -4133,19 +4295,21 @@ module MarkdownExec
4133
4295
  # crashes if default is not an existing item
4134
4296
  #
4135
4297
  selection = @prompt.select(prompt_text,
4136
- menu_items,
4298
+ tty_menu_items,
4137
4299
  opts.merge(props))
4138
4300
  rescue TTY::Prompt::ConfigurationError
4139
4301
  # prompt fails when collapsible block name has changed; clear default
4140
4302
  selection = @prompt.select(prompt_text,
4141
- menu_items,
4303
+ tty_menu_items,
4142
4304
  opts.merge(props).merge(default: nil))
4143
4305
  rescue NoMethodError
4144
4306
  # no enabled options in page
4145
4307
  return
4146
4308
  end
4147
4309
 
4148
- 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|
4149
4313
  if item.instance_of?(Hash)
4150
4314
  [item[:id], item[:name], item[:dname]].include?(selection)
4151
4315
  elsif item.instance_of?(MarkdownExec::FCB)
@@ -4155,7 +4319,15 @@ module MarkdownExec
4155
4319
  end
4156
4320
  end
4157
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
+
4158
4329
  unless selected
4330
+ report_and_reraise('menu item not found')
4159
4331
  HashDelegator.error_handler('select_option_with_metadata',
4160
4332
  error: 'menu item not found')
4161
4333
  exit 1
@@ -4407,164 +4579,188 @@ module MarkdownExec
4407
4579
  @delegate_object.merge!(options)
4408
4580
  end
4409
4581
 
4410
- def ux_block_export_activated(export, inherited_code, code_lines, required,
4411
- exit_prompt)
4582
+ def ux_block_export_activated(
4583
+ bash_script_lines, export, exit_prompt
4584
+ )
4412
4585
  exportable = true
4413
4586
  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
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
4497
4611
 
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
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
4510
4654
  end
4655
+ rescue Interrupt
4656
+ exportable = false
4657
+ transformable = false
4511
4658
  end
4512
4659
 
4513
- value = case export.default
4514
- when :allowed
4515
- raise unless export.allowed.present?
4660
+ command_result = CommandResult.new(stdout: output)
4516
4661
 
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
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
+ )
4522
4666
 
4523
- exportable && output.split("\n").first
4667
+ command_result
4524
4668
 
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
4669
+ else
4670
+ transformable = false
4671
+ command_result = CommandResult.new(stdout: export.default.to_s)
4672
+ end
4529
4673
 
4530
- output.first
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
4531
4679
 
4532
- else
4533
- export.allowed.first
4534
- end
4680
+ command_result.exportable = exportable
4681
+ command_result.transformable = transformable
4682
+ command_result.new_lines = new_lines
4683
+ command_result
4684
+ end
4535
4685
 
4536
- # echo > default
4537
- when :echo
4538
- raise unless export.echo.present?
4686
+ def ux_block_export_automatic(bash_script_lines, export)
4687
+ transformable = true
4688
+ exportable = true
4689
+ new_lines = []
4690
+ command_result = nil
4539
4691
 
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
4692
+ case FCB.init_source(export)
4693
+ when false, UxActSource::FALSE
4694
+ exportable = false
4695
+ transformable = false
4696
+ command_result = CommandResult.new
4544
4697
 
4545
- output
4698
+ when ':allow', UxActSource::ALLOW
4699
+ raise unless export.allow.present?
4546
4700
 
4547
- # exec > default
4548
- when :exec
4549
- raise unless export.exec.present?
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
4550
4711
 
4551
- output = export_exec_with_code(
4552
- export, inherited_code, code_lines, required
4553
- )
4554
- return :ux_exec_prohibited if output == :invalidated
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
4555
4719
 
4556
- output
4720
+ else
4721
+ # must be a list
4722
+ command_result = CommandResult.new(stdout: export.allow.first)
4723
+ end
4557
4724
 
4558
- # default
4559
- else
4560
- transformable = false
4561
- export.default.to_s
4562
- end
4725
+ when ':default', UxActSource::DEFAULT
4726
+ transformable = false
4727
+ command_result = CommandResult.new(stdout: export.default.to_s)
4728
+
4729
+ when ':echo', UxActSource::ECHO
4730
+ raise unless export.echo.present?
4731
+
4732
+ command_result, exportable, new_lines = export_echo_with_code(
4733
+ bash_script_lines,
4734
+ export,
4735
+ force: false
4736
+ )
4737
+
4738
+ when ':exec', UxActSource::EXEC
4739
+ raise unless export.exec.present?
4740
+
4741
+ command_result = output_from_adhoc_bash_script_file(
4742
+ join_array_of_arrays(bash_script_lines, export.exec)
4743
+ )
4744
+
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
4749
+
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
4563
4754
  end
4564
4755
 
4565
- [value,
4566
- exportable,
4567
- transformable]
4756
+ command_result.exportable = exportable
4757
+ command_result.transformable = transformable
4758
+ command_result.new_lines = new_lines
4759
+ command_result
4760
+ end
4761
+
4762
+ def warning_required_empty(export)
4763
+ "A value must exist for: #{export.required.join(', ')}"
4568
4764
  end
4569
4765
 
4570
4766
  def vux_await_user_selection(prior_answer: @dml_block_selection)
@@ -4791,7 +4987,8 @@ module MarkdownExec
4791
4987
  #
4792
4988
  # @return [Nil] Returns nil if no code block is selected
4793
4989
  # or an error occurs.
4794
- def vux_main_loop
4990
+ def vux_main_loop(menu_from_yaml: nil)
4991
+ @menu_from_yaml = menu_from_yaml
4795
4992
  vux_init
4796
4993
  vux_load_code_files_into_state
4797
4994
  formatted_choice_ostructs = vux_formatted_names_for_state_chrome_blocks
@@ -4841,10 +5038,10 @@ module MarkdownExec
4841
5038
  case msg
4842
5039
  when :parse_document # once for each menu
4843
5040
  count = 0
4844
- vux_parse_document(source_id: "#{@delegate_object[:filename]}_vmlpd")
5041
+ vux_parse_document(source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®PrsDoc")
4845
5042
  vux_menu_append_history_files(
4846
5043
  formatted_choice_ostructs,
4847
- source_id: "#{@delegate_object[:filename]}_vmlhf"
5044
+ source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®HstFls"
4848
5045
  )
4849
5046
  vux_publish_document_file_name_for_external_automation
4850
5047
 
@@ -4856,7 +5053,7 @@ module MarkdownExec
4856
5053
  # yield :end_of_cli, @delegate_object
4857
5054
 
4858
5055
  if @delegate_object[:list_blocks]
4859
- list_blocks(source_id: "#{@delegate_object[:filename]}_vmleoc")
5056
+ list_blocks(source_id: "#{@delegate_object[:filename]}¤VuxMainLoop®EndCLI")
4860
5057
  :exit
4861
5058
  end
4862
5059
 
@@ -5081,8 +5278,8 @@ module MarkdownExec
5081
5278
  :prompt_color_after_script_execution
5082
5279
  )
5083
5280
 
5084
- menu_items = blocks_as_menu_items(menu_blocks)
5085
- if menu_items.empty?
5281
+ tty_menu_items = blocks_as_menu_items(menu_blocks)
5282
+ if tty_menu_items.empty?
5086
5283
  return SelectedBlockMenuState.new(nil, OpenStruct.new,
5087
5284
  MenuState::EXIT)
5088
5285
  end
@@ -5117,8 +5314,9 @@ module MarkdownExec
5117
5314
  { cycle: @delegate_object[:select_page_cycle],
5118
5315
  per_page: @delegate_object[:select_page_height] }
5119
5316
  )
5120
- selected_option = select_option_with_metadata(prompt_title, menu_items,
5121
- selection_opts)
5317
+ selected_option = select_option_with_metadata(
5318
+ prompt_title, tty_menu_items, selection_opts
5319
+ )
5122
5320
  determine_block_state(selected_option)
5123
5321
  end
5124
5322
 
@@ -5192,9 +5390,13 @@ module MarkdownExec
5192
5390
  save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
5193
5391
  if save_expr.present?
5194
5392
  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]
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
5198
5400
  else
5199
5401
  link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
5200
5402
  end
@@ -5202,6 +5404,8 @@ module MarkdownExec
5202
5404
  end
5203
5405
 
5204
5406
  class HashDelegator < HashDelegatorParent
5407
+ include ::ErrorReporting
5408
+
5205
5409
  # Cleans a value, handling both Hash and Struct types.
5206
5410
  # For Structs, the cleaned version is converted to a hash.
5207
5411
  def self.clean_value(value)
@@ -5414,7 +5618,7 @@ module MarkdownExec
5414
5618
  input: MarkdownExec::FCB.new(title: '',
5415
5619
  body: ['def add(x, y)',
5416
5620
  ' x + y', 'end']),
5417
- output: "def add(x, y)\n x + y\n end\n"
5621
+ output: "def add(x, y)\n x + y\n end"
5418
5622
  },
5419
5623
  {
5420
5624
  input: MarkdownExec::FCB.new(title: 'foo', body: %w[bar baz]),