markdown_exec 2.0.5 → 2.0.7
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +17 -2
- data/CHANGELOG.md +48 -0
- data/Gemfile.lock +1 -1
- data/bin/tab_completion.sh +15 -31
- data/examples/block_names.md +23 -0
- data/examples/interrupt.md +9 -0
- data/examples/line-wrapping.md +17 -0
- data/examples/linked.md +8 -0
- data/examples/linked_show.md +7 -0
- data/examples/load_code.md +10 -0
- data/examples/nickname.md +46 -9
- data/examples/pause-after-execution.md +9 -0
- data/lib/color_scheme.rb +65 -0
- data/lib/colorize.rb +25 -0
- data/lib/constants.rb +19 -19
- data/lib/fcb.rb +15 -0
- data/lib/find_files.rb +35 -39
- data/lib/hash_delegator.rb +1239 -761
- data/lib/input_sequencer.rb +11 -4
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +16 -6
- data/lib/mdoc.rb +9 -14
- data/lib/menu.src.yml +106 -24
- data/lib/menu.yml +93 -25
- data/lib/saved_assets.rb +4 -3
- metadata +9 -2
data/lib/hash_delegator.rb
CHANGED
@@ -200,7 +200,7 @@ module HashDelegatorSelf
|
|
200
200
|
|
201
201
|
# Determine the state of breaker based on was_using_cli and the block type
|
202
202
|
# true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block[:shell] equals BlockType::BASH. In all other scenarios, breaker is false.
|
203
|
-
breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block
|
203
|
+
breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
|
204
204
|
|
205
205
|
# Reset block_name_from_cli if the conditions are not met
|
206
206
|
block_name_from_cli ||= false
|
@@ -252,14 +252,16 @@ module HashDelegatorSelf
|
|
252
252
|
|
253
253
|
# # Sanitize input (example: removing potentially harmful characters)
|
254
254
|
# str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
|
255
|
-
|
256
255
|
# Evaluate the sanitized string
|
257
256
|
result = nil
|
258
257
|
binding.eval("result = #{str}")
|
259
258
|
|
260
259
|
result
|
261
260
|
rescue StandardError # catches NameError, StandardError
|
261
|
+
pp $!, $@
|
262
|
+
pp "code: #{str}"
|
262
263
|
error_handler('safeval')
|
264
|
+
exit 1
|
263
265
|
end
|
264
266
|
|
265
267
|
# # Evaluates the given string as Ruby code and rescues any StandardErrors.
|
@@ -311,11 +313,11 @@ module HashDelegatorSelf
|
|
311
313
|
File.write(
|
312
314
|
filespec,
|
313
315
|
["-STDOUT-\n",
|
314
|
-
format_execution_streams(ExecutionStreams::
|
316
|
+
format_execution_streams(ExecutionStreams::STD_OUT, files),
|
315
317
|
"-STDERR-\n",
|
316
|
-
format_execution_streams(ExecutionStreams::
|
318
|
+
format_execution_streams(ExecutionStreams::STD_ERR, files),
|
317
319
|
"-STDIN-\n",
|
318
|
-
format_execution_streams(ExecutionStreams::
|
320
|
+
format_execution_streams(ExecutionStreams::STD_IN, files),
|
319
321
|
"\n"].join
|
320
322
|
)
|
321
323
|
end
|
@@ -387,7 +389,7 @@ module PathUtils
|
|
387
389
|
# @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
|
388
390
|
# @return [String] The absolute path or the expression with the wildcard replaced by the path.
|
389
391
|
def self.resolve_path_or_substitute(path, expression)
|
390
|
-
if path.
|
392
|
+
if path.start_with?('/')
|
391
393
|
path
|
392
394
|
else
|
393
395
|
expression.gsub('*', path)
|
@@ -412,6 +414,74 @@ class BashCommentFormatter
|
|
412
414
|
# end
|
413
415
|
end
|
414
416
|
|
417
|
+
class StringWrapper
|
418
|
+
attr_reader :width, :left_margin, :right_margin, :indent, :fill_margin
|
419
|
+
|
420
|
+
# Initializes the StringWrapper with the given options.
|
421
|
+
#
|
422
|
+
# @param width [Integer] the maximum width of each line
|
423
|
+
# @param left_margin [Integer] the number of spaces for the left margin
|
424
|
+
# @param right_margin [Integer] the number of spaces for the right margin
|
425
|
+
# @param indent [Integer] the number of spaces to indent all but the first line
|
426
|
+
# @param fill_margin [Boolean] whether to fill the left margin with spaces
|
427
|
+
def initialize(
|
428
|
+
width:,
|
429
|
+
fill_margin: false,
|
430
|
+
first_indent: '',
|
431
|
+
indent_space: ' ',
|
432
|
+
left_margin: 0,
|
433
|
+
margin_char: ' ',
|
434
|
+
rest_indent: '',
|
435
|
+
right_margin: 0
|
436
|
+
)
|
437
|
+
@fill_margin = fill_margin
|
438
|
+
@first_indent = first_indent
|
439
|
+
@indent = indent
|
440
|
+
@indent_space = indent_space
|
441
|
+
@rest_indent = rest_indent
|
442
|
+
@right_margin = right_margin
|
443
|
+
@width = width
|
444
|
+
|
445
|
+
@margin_space = fill_margin ? (margin_char * left_margin) : ''
|
446
|
+
@left_margin = @margin_space.length
|
447
|
+
end
|
448
|
+
|
449
|
+
# Wraps the given text according to the specified options.
|
450
|
+
#
|
451
|
+
# @param text [String] the text to wrap
|
452
|
+
# @return [String] the wrapped text
|
453
|
+
def wrap(text)
|
454
|
+
text = text.dup if text.frozen?
|
455
|
+
max_line_length = width - left_margin - right_margin - @indent_space.length
|
456
|
+
lines = []
|
457
|
+
current_line = String.new
|
458
|
+
|
459
|
+
words = text.split
|
460
|
+
words.each.with_index do |word, index|
|
461
|
+
trial_length = word.length
|
462
|
+
trial_length += @first_indent.length if index.zero?
|
463
|
+
trial_length += current_line.length + 1 + @rest_indent.length if index != 0
|
464
|
+
if trial_length > max_line_length && (words.count != 0)
|
465
|
+
lines << current_line
|
466
|
+
current_line = word
|
467
|
+
current_line = current_line.dup if current_line.frozen?
|
468
|
+
else
|
469
|
+
current_line << ' ' unless current_line.empty?
|
470
|
+
current_line << word
|
471
|
+
end
|
472
|
+
end
|
473
|
+
lines << current_line unless current_line.empty?
|
474
|
+
|
475
|
+
lines.map.with_index do |line, index|
|
476
|
+
@margin_space + if index.zero?
|
477
|
+
@first_indent
|
478
|
+
else
|
479
|
+
@rest_indent
|
480
|
+
end + line
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
415
485
|
module MarkdownExec
|
416
486
|
class DebugHelper
|
417
487
|
# Class-level variable to store history of printed messages
|
@@ -509,9 +579,24 @@ module MarkdownExec
|
|
509
579
|
history_state_partition
|
510
580
|
option_name = @delegate_object[:menu_option_back_name]
|
511
581
|
insert_at_top = @delegate_object[:menu_back_at_top]
|
582
|
+
when MenuState::EDIT
|
583
|
+
option_name = @delegate_object[:menu_option_edit_name]
|
584
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
512
585
|
when MenuState::EXIT
|
513
586
|
option_name = @delegate_object[:menu_option_exit_name]
|
514
587
|
insert_at_top = @delegate_object[:menu_exit_at_top]
|
588
|
+
when MenuState::LOAD
|
589
|
+
option_name = @delegate_object[:menu_option_load_name]
|
590
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
591
|
+
when MenuState::SAVE
|
592
|
+
option_name = @delegate_object[:menu_option_save_name]
|
593
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
594
|
+
when MenuState::SHELL
|
595
|
+
option_name = @delegate_object[:menu_option_shell_name]
|
596
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
597
|
+
when MenuState::VIEW
|
598
|
+
option_name = @delegate_object[:menu_option_view_name]
|
599
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
515
600
|
end
|
516
601
|
|
517
602
|
formatted_name = format(@delegate_object[:menu_link_format],
|
@@ -519,7 +604,7 @@ module MarkdownExec
|
|
519
604
|
chrome_block = FCB.new(
|
520
605
|
chrome: true,
|
521
606
|
dname: HashDelegator.new(@delegate_object).string_send_color(
|
522
|
-
formatted_name, :
|
607
|
+
formatted_name, :menu_chrome_color
|
523
608
|
),
|
524
609
|
oname: formatted_name
|
525
610
|
)
|
@@ -607,6 +692,8 @@ module MarkdownExec
|
|
607
692
|
#
|
608
693
|
# @return [Array<FCB>] An array of FCB objects representing the blocks.
|
609
694
|
def blocks_from_nested_files
|
695
|
+
register_console_attributes(@delegate_object)
|
696
|
+
|
610
697
|
blocks = []
|
611
698
|
iter_blocks_from_nested_files do |btype, fcb|
|
612
699
|
process_block_based_on_type(blocks, btype, fcb)
|
@@ -617,6 +704,20 @@ module MarkdownExec
|
|
617
704
|
HashDelegator.error_handler('blocks_from_nested_files')
|
618
705
|
end
|
619
706
|
|
707
|
+
# find a block by its original (undecorated) name or nickname (not visible in menu)
|
708
|
+
# if matched, the block returned has properties that it is from cli and not ui
|
709
|
+
def block_state_for_name_from_cli(block_name)
|
710
|
+
SelectedBlockMenuState.new(
|
711
|
+
@dml_blocks_in_file.find do |item|
|
712
|
+
block_name == item.pub_name
|
713
|
+
end&.merge(
|
714
|
+
block_name_from_cli: true,
|
715
|
+
block_name_from_ui: false
|
716
|
+
),
|
717
|
+
MenuState::CONTINUE
|
718
|
+
)
|
719
|
+
end
|
720
|
+
|
620
721
|
# private
|
621
722
|
|
622
723
|
def calc_logged_stdout_filename(block_name:)
|
@@ -663,7 +764,7 @@ module MarkdownExec
|
|
663
764
|
# @return [Array<String>] Required code blocks as an array of lines.
|
664
765
|
def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new)
|
665
766
|
required = mdoc.collect_recursively_required_code(
|
666
|
-
anyname: selected
|
767
|
+
anyname: selected.pub_name,
|
667
768
|
label_format_above: @delegate_object[:shell_code_label_format_above],
|
668
769
|
label_format_below: @delegate_object[:shell_code_label_format_below],
|
669
770
|
block_source: block_source
|
@@ -701,47 +802,15 @@ module MarkdownExec
|
|
701
802
|
system(
|
702
803
|
format(
|
703
804
|
@delegate_object[:execute_command_format],
|
704
|
-
|
705
|
-
batch_index: @run_state.batch_index,
|
706
|
-
batch_random: @run_state.batch_random,
|
707
|
-
block_name: @delegate_object[:block_name],
|
708
|
-
document_filename: File.basename(@delegate_object[:filename]),
|
709
|
-
document_filespec: @delegate_object[:filename],
|
710
|
-
home: Dir.pwd,
|
711
|
-
output_filename: File.basename(@delegate_object[:logged_stdout_filespec]),
|
712
|
-
output_filespec: @delegate_object[:logged_stdout_filespec],
|
713
|
-
script_filename: @run_state.saved_filespec,
|
714
|
-
script_filespec: File.join(Dir.pwd, @run_state.saved_filespec),
|
715
|
-
started_at: @run_state.started_at.strftime(
|
716
|
-
@delegate_object[:execute_command_title_time_format]
|
717
|
-
)
|
718
|
-
}
|
805
|
+
command_execute_in_own_window_format_arguments
|
719
806
|
)
|
720
807
|
)
|
721
808
|
|
722
809
|
else
|
723
810
|
@run_state.in_own_window = false
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
*args) do |stdin, stdout, stderr, exec_thr|
|
728
|
-
handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
|
729
|
-
yield nil, line, nil, exec_thr if block_given?
|
730
|
-
end
|
731
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
|
732
|
-
yield nil, nil, line, exec_thr if block_given?
|
733
|
-
end
|
734
|
-
|
735
|
-
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
|
736
|
-
stdin.puts(line)
|
737
|
-
yield line, nil, nil, exec_thr if block_given?
|
738
|
-
end
|
739
|
-
|
740
|
-
wait_for_stream_processing
|
741
|
-
exec_thr.join
|
742
|
-
sleep 0.1
|
743
|
-
in_thr.kill if in_thr&.alive?
|
744
|
-
end
|
811
|
+
execute_command_with_streams(
|
812
|
+
[@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args]
|
813
|
+
)
|
745
814
|
end
|
746
815
|
|
747
816
|
@run_state.completed_at = Time.now.utc
|
@@ -750,32 +819,33 @@ module MarkdownExec
|
|
750
819
|
@run_state.aborted_at = Time.now.utc
|
751
820
|
@run_state.error_message = err.message
|
752
821
|
@run_state.error = err
|
753
|
-
@run_state.files[ExecutionStreams::
|
822
|
+
@run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
|
754
823
|
@fout.fout "Error ENOENT: #{err.inspect}"
|
755
824
|
rescue SignalException => err
|
756
825
|
# Handle SignalException
|
757
826
|
@run_state.aborted_at = Time.now.utc
|
758
827
|
@run_state.error_message = 'SIGTERM'
|
759
828
|
@run_state.error = err
|
760
|
-
@run_state.files[ExecutionStreams::
|
829
|
+
@run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
|
761
830
|
@fout.fout "Error ENOENT: #{err.inspect}"
|
762
831
|
end
|
763
832
|
|
764
|
-
def
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
833
|
+
def command_execute_in_own_window_format_arguments(home: Dir.pwd)
|
834
|
+
{
|
835
|
+
batch_index: @run_state.batch_index,
|
836
|
+
batch_random: @run_state.batch_random,
|
837
|
+
block_name: @delegate_object[:block_name],
|
838
|
+
document_filename: File.basename(@delegate_object[:filename]),
|
839
|
+
document_filespec: @delegate_object[:filename],
|
840
|
+
home: home,
|
841
|
+
output_filename: File.basename(@delegate_object[:logged_stdout_filespec]),
|
842
|
+
output_filespec: @delegate_object[:logged_stdout_filespec],
|
843
|
+
script_filename: @run_state.saved_filespec,
|
844
|
+
script_filespec: File.join(home, @run_state.saved_filespec),
|
845
|
+
started_at: @run_state.started_at.strftime(
|
846
|
+
@delegate_object[:execute_command_title_time_format]
|
847
|
+
)
|
848
|
+
}
|
779
849
|
end
|
780
850
|
|
781
851
|
# This method is responsible for handling the execution of generic blocks in a markdown document.
|
@@ -799,7 +869,12 @@ module MarkdownExec
|
|
799
869
|
execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
|
800
870
|
|
801
871
|
link_state.block_name = nil
|
802
|
-
LoadFileLinkState.new(LoadFile::
|
872
|
+
LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
873
|
+
end
|
874
|
+
|
875
|
+
# Check if the expression contains wildcard characters
|
876
|
+
def contains_wildcards?(expr)
|
877
|
+
expr.match(%r{\*|\?|\[})
|
803
878
|
end
|
804
879
|
|
805
880
|
def copy_to_clipboard(required_lines)
|
@@ -828,16 +903,70 @@ module MarkdownExec
|
|
828
903
|
# @param match_data [MatchData] The match data containing named captures for formatting.
|
829
904
|
# @param format_option [String] The format string to be used for the new block.
|
830
905
|
# @param color_method [Symbol] The color method to apply to the block's display name.
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
906
|
+
# return number of lines added
|
907
|
+
def create_and_add_chrome_block(blocks:, match_data:,
|
908
|
+
format_option:, color_method:,
|
909
|
+
case_conversion: nil,
|
910
|
+
center: nil,
|
911
|
+
wrap: nil)
|
912
|
+
line_cap = match_data.named_captures.transform_keys(&:to_sym)
|
913
|
+
|
914
|
+
# replace tabs in indent
|
915
|
+
line_cap[:indent] ||= ''
|
916
|
+
line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen?
|
917
|
+
line_cap[:indent].gsub!("\t", ' ')
|
918
|
+
# replace tabs in text
|
919
|
+
line_cap[:text] ||= ''
|
920
|
+
line_cap[:text] = line_cap[:text].dup if line_cap[:text].frozen?
|
921
|
+
line_cap[:text].gsub!("\t", ' ')
|
922
|
+
# missing capture
|
923
|
+
line_cap[:line] ||= ''
|
924
|
+
|
925
|
+
accepted_width = @delegate_object[:console_width] - 2
|
926
|
+
line_caps = if wrap
|
927
|
+
if line_cap[:text].length > accepted_width
|
928
|
+
wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length)
|
929
|
+
wrapper.wrap(line_cap[:text]).map do |line|
|
930
|
+
line_cap.dup.merge(text: line)
|
931
|
+
end
|
932
|
+
else
|
933
|
+
[line_cap]
|
934
|
+
end
|
935
|
+
else
|
936
|
+
[line_cap]
|
937
|
+
end
|
938
|
+
if center
|
939
|
+
line_caps.each do |line_obj|
|
940
|
+
line_obj[:indent] = if line_obj[:text].length < accepted_width
|
941
|
+
' ' * ((accepted_width - line_obj[:text].length) / 2)
|
942
|
+
else
|
943
|
+
''
|
944
|
+
end
|
945
|
+
end
|
946
|
+
end
|
947
|
+
|
948
|
+
line_caps.each do |line_obj|
|
949
|
+
next if line_obj[:text].nil?
|
950
|
+
|
951
|
+
case case_conversion
|
952
|
+
when :upcase
|
953
|
+
line_obj[:text].upcase!
|
954
|
+
when :downcase
|
955
|
+
line_obj[:text].downcase!
|
956
|
+
end
|
957
|
+
|
958
|
+
# format expects :line to be text only
|
959
|
+
line_obj[:line] = line_obj[:text]
|
960
|
+
oname = format(format_option, line_obj)
|
961
|
+
line_obj[:line] = line_obj[:indent] + line_obj[:text]
|
962
|
+
blocks.push FCB.new(
|
963
|
+
chrome: true,
|
964
|
+
disabled: '',
|
965
|
+
dname: line_obj[:indent] + oname.send(color_method),
|
966
|
+
oname: line_obj[:text]
|
967
|
+
)
|
968
|
+
end
|
969
|
+
line_caps.count
|
841
970
|
end
|
842
971
|
|
843
972
|
##
|
@@ -848,12 +977,12 @@ module MarkdownExec
|
|
848
977
|
# @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
|
849
978
|
def create_and_add_chrome_blocks(blocks, fcb)
|
850
979
|
match_criteria = [
|
851
|
-
{ color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match },
|
852
|
-
{ color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match },
|
853
|
-
{ color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match },
|
980
|
+
{ color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, center: true, case_conversion: :upcase, wrap: true },
|
981
|
+
{ color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, center: true, wrap: true },
|
982
|
+
{ color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, center: true, case_conversion: :downcase, wrap: true },
|
854
983
|
{ color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match },
|
855
|
-
{ color: :menu_note_color, format: :menu_note_format, match: :menu_note_match },
|
856
|
-
{ color: :menu_task_color, format: :menu_task_format, match: :menu_task_match }
|
984
|
+
{ color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true },
|
985
|
+
{ color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true }
|
857
986
|
]
|
858
987
|
# rubocop:enable Style/UnlessElse
|
859
988
|
match_criteria.each do |criteria|
|
@@ -864,9 +993,12 @@ module MarkdownExec
|
|
864
993
|
|
865
994
|
create_and_add_chrome_block(
|
866
995
|
blocks: blocks,
|
867
|
-
|
996
|
+
case_conversion: criteria[:case_conversion],
|
997
|
+
center: criteria[:center],
|
998
|
+
color_method: @delegate_object[criteria[:color]].to_sym,
|
868
999
|
format_option: @delegate_object[criteria[:format]],
|
869
|
-
|
1000
|
+
match_data: mbody,
|
1001
|
+
wrap: criteria[:wrap]
|
870
1002
|
)
|
871
1003
|
break
|
872
1004
|
end
|
@@ -950,164 +1082,495 @@ module MarkdownExec
|
|
950
1082
|
@delegate_object[:logged_stdout_filespec])
|
951
1083
|
end
|
952
1084
|
|
953
|
-
#
|
954
|
-
# It sets the script block name, writes command files if required, and handles the execution
|
955
|
-
# including output formatting and summarization.
|
956
|
-
#
|
957
|
-
# @param required_lines [Array<String>] The lines of code to be executed.
|
958
|
-
# @param selected [FCB] The selected functional code block object.
|
959
|
-
def execute_required_lines(required_lines: [], selected: FCB.new)
|
960
|
-
write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script]
|
961
|
-
calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state
|
962
|
-
format_and_execute_command(code_lines: required_lines)
|
963
|
-
post_execution_process
|
964
|
-
end
|
965
|
-
|
966
|
-
# Execute a code block after approval and provide user interaction options.
|
967
|
-
#
|
968
|
-
# This method displays required code blocks, asks for user approval, and
|
969
|
-
# executes the code block if approved. It also allows users to copy the
|
970
|
-
# code to the clipboard or save it to a file.
|
1085
|
+
# Select and execute a code block from a Markdown document.
|
971
1086
|
#
|
972
|
-
#
|
973
|
-
#
|
1087
|
+
# This method allows the user to interactively select a code block from a
|
1088
|
+
# Markdown document, obtain approval, and execute the chosen block of code.
|
974
1089
|
#
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
1090
|
+
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1091
|
+
def document_inpseq
|
1092
|
+
@menu_base_options = @delegate_object
|
1093
|
+
@dml_link_state = LinkState.new(
|
1094
|
+
block_name: @delegate_object[:block_name],
|
1095
|
+
document_filename: @delegate_object[:filename]
|
1096
|
+
)
|
1097
|
+
# @dml_link_state_block_name_from_cli = @dml_link_state.block_name.present? ###
|
1098
|
+
@run_state.block_name_from_cli = @dml_link_state.block_name.present?
|
1099
|
+
@cli_block_name = @dml_link_state.block_name
|
1100
|
+
@dml_now_using_cli = @run_state.block_name_from_cli
|
1101
|
+
@dml_menu_default_dname = nil
|
1102
|
+
@dml_block_state = SelectedBlockMenuState.new
|
1103
|
+
@doc_saved_lines_files = []
|
983
1104
|
|
984
|
-
|
985
|
-
|
986
|
-
|
1105
|
+
## load file with code lines per options
|
1106
|
+
#
|
1107
|
+
if @menu_base_options[:load_code].present?
|
1108
|
+
@dml_link_state.inherited_lines = []
|
1109
|
+
@menu_base_options[:load_code].split(':').map do |path|
|
1110
|
+
@dml_link_state.inherited_lines += File.readlines(path, chomp: true)
|
1111
|
+
end
|
987
1112
|
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
code_lines
|
992
|
-
|
993
|
-
options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state)
|
1113
|
+
inherited_block_names = []
|
1114
|
+
inherited_dependencies = {}
|
1115
|
+
selected = { oname: 'load_code' }
|
1116
|
+
pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
|
1117
|
+
end
|
994
1118
|
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
1119
|
+
item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
|
1120
|
+
item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
|
1121
|
+
item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
|
1122
|
+
item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
|
1123
|
+
item_shell = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name]))
|
1124
|
+
item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
|
999
1125
|
|
1000
|
-
|
1001
|
-
|
1002
|
-
link_history_push_and_next(
|
1003
|
-
curr_block_name: selected[:oname],
|
1004
|
-
curr_document_filename: @delegate_object[:filename],
|
1005
|
-
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1006
|
-
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1007
|
-
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1008
|
-
next_block_name: '',
|
1009
|
-
next_document_filename: @delegate_object[:filename],
|
1010
|
-
next_load_file: LoadFile::Reuse
|
1011
|
-
)
|
1126
|
+
@run_state.batch_random = Random.new.rand
|
1127
|
+
@run_state.batch_index = 0
|
1012
1128
|
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1022
|
-
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1023
|
-
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1024
|
-
next_block_name: '',
|
1025
|
-
next_document_filename: @delegate_object[:filename],
|
1026
|
-
next_load_file: LoadFile::Reuse
|
1027
|
-
)
|
1129
|
+
InputSequencer.new(
|
1130
|
+
@delegate_object[:filename],
|
1131
|
+
@delegate_object[:input_cli_rest]
|
1132
|
+
).run do |msg, data|
|
1133
|
+
case msg
|
1134
|
+
when :parse_document # once for each menu
|
1135
|
+
# puts "@ - parse document #{data}"
|
1136
|
+
inpseq_parse_document(data)
|
1028
1137
|
|
1029
|
-
|
1030
|
-
compile_execute_and_trigger_reuse(mdoc: mdoc,
|
1031
|
-
selected: selected,
|
1032
|
-
link_state: link_state,
|
1033
|
-
block_source: block_source)
|
1138
|
+
if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
|
1034
1139
|
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
end
|
1140
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
1141
|
+
files = sf ? Dir.glob(sf) : []
|
1142
|
+
@doc_saved_lines_files = files.count.positive? ? files : []
|
1039
1143
|
|
1040
|
-
|
1041
|
-
# and applies a color style based on the specified color symbol.
|
1042
|
-
#
|
1043
|
-
# @param default [String] The default value if the data symbol is not found.
|
1044
|
-
# @param data_sym [Symbol] The symbol key to fetch data from the delegate object.
|
1045
|
-
# @param color_sym [Symbol] The symbol key to fetch the color option for styling.
|
1046
|
-
# @return [String] The color-styled string.
|
1047
|
-
def fetch_color(default: '',
|
1048
|
-
data_sym: :execution_report_preview_head,
|
1049
|
-
color_sym: :execution_report_preview_frame_color)
|
1050
|
-
data_string = @delegate_object.fetch(data_sym, default).to_s
|
1051
|
-
string_send_color(data_string, color_sym)
|
1052
|
-
end
|
1144
|
+
lines_count = @dml_link_state.inherited_lines&.count || 0
|
1053
1145
|
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1146
|
+
# add menu items (glob, load, save) and enable selectively
|
1147
|
+
menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
|
1148
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD) if files.count.positive?
|
1149
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT) if lines_count.positive?
|
1150
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), 1, '', menu_state: MenuState::SAVE) if lines_count.positive?
|
1151
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), 1, '', menu_state: MenuState::VIEW) if lines_count.positive?
|
1152
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name])), 1, '', menu_state: MenuState::SHELL) if @delegate_object[:menu_with_shell]
|
1153
|
+
end
|
1062
1154
|
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
# @param format_sym [Symbol] Symbol key to fetch the format string from the delegate object.
|
1069
|
-
# @param color_sym [Symbol] Symbol key to fetch the color option for string styling.
|
1070
|
-
# @return [String] The formatted and color-styled string.
|
1071
|
-
def format_references_send_color(default: '', context: {},
|
1072
|
-
format_sym: :output_execution_label_format,
|
1073
|
-
color_sym: :execution_report_preview_frame_color)
|
1074
|
-
formatted_string = format(@delegate_object.fetch(format_sym, ''),
|
1075
|
-
context).to_s
|
1076
|
-
string_send_color(formatted_string, color_sym)
|
1077
|
-
end
|
1155
|
+
when :display_menu
|
1156
|
+
# warn "@ - display menu:"
|
1157
|
+
# ii_display_menu
|
1158
|
+
@dml_block_state = SelectedBlockMenuState.new
|
1159
|
+
@delegate_object[:block_name] = nil
|
1078
1160
|
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1161
|
+
when :user_choice
|
1162
|
+
if @dml_link_state.block_name.present?
|
1163
|
+
# @prior_block_was_link = true
|
1164
|
+
@dml_block_state.block = @dml_blocks_in_file.find do |item|
|
1165
|
+
item.pub_name == @dml_link_state.block_name
|
1166
|
+
end
|
1167
|
+
@dml_link_state.block_name = nil
|
1168
|
+
else
|
1169
|
+
# puts "? - Select a block to execute (or type #{$texit} to exit):"
|
1170
|
+
break if inpseq_user_choice == :break # into @dml_block_state
|
1171
|
+
break if @dml_block_state.block.nil? # no block matched
|
1172
|
+
end
|
1173
|
+
# puts "! - Executing block: #{data}"
|
1174
|
+
@dml_block_state.block&.pub_name
|
1086
1175
|
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1176
|
+
when :execute_block
|
1177
|
+
case (block_name = data)
|
1178
|
+
when item_back
|
1179
|
+
debounce_reset
|
1180
|
+
@menu_user_clicked_back_link = true
|
1181
|
+
load_file_link_state = pop_link_history_and_trigger_load
|
1182
|
+
@dml_link_state = load_file_link_state.link_state
|
1091
1183
|
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1184
|
+
InputSequencer.merge_link_state(
|
1185
|
+
@dml_link_state,
|
1186
|
+
InputSequencer.next_link_state(
|
1187
|
+
block_name: @dml_link_state.block_name,
|
1188
|
+
document_filename: @dml_link_state.document_filename,
|
1189
|
+
prior_block_was_link: true
|
1190
|
+
)
|
1191
|
+
)
|
1096
1192
|
|
1097
|
-
|
1193
|
+
when item_edit
|
1194
|
+
debounce_reset
|
1195
|
+
edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
|
1196
|
+
@dml_link_state.inherited_lines = edited.split("\n") if edited
|
1197
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1098
1198
|
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1199
|
+
when item_load
|
1200
|
+
debounce_reset
|
1201
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
1202
|
+
load_filespec = load_filespec_from_expression(sf)
|
1203
|
+
if load_filespec
|
1204
|
+
@dml_link_state.inherited_lines ||= []
|
1205
|
+
@dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
|
1206
|
+
end
|
1207
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1208
|
+
|
1209
|
+
when item_save
|
1210
|
+
debounce_reset
|
1211
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
1212
|
+
save_filespec = save_filespec_from_expression(sf)
|
1213
|
+
if save_filespec && !write_file_with_directory_creation(
|
1214
|
+
save_filespec,
|
1215
|
+
HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
|
1216
|
+
)
|
1217
|
+
return :break
|
1218
|
+
|
1219
|
+
end
|
1220
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1221
|
+
|
1222
|
+
when item_shell
|
1223
|
+
debounce_reset
|
1224
|
+
loop do
|
1225
|
+
command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen)
|
1226
|
+
break if !command.present? || command == 'exit'
|
1227
|
+
|
1228
|
+
exit_status = execute_command_with_streams(
|
1229
|
+
[@delegate_object[:shell], '-c', command]
|
1230
|
+
)
|
1231
|
+
case exit_status
|
1232
|
+
when 0
|
1233
|
+
warn "#{'OK'.green} #{exit_status}"
|
1234
|
+
else
|
1235
|
+
warn "#{'ERR'.bred} #{exit_status}"
|
1236
|
+
end
|
1237
|
+
end
|
1238
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1239
|
+
|
1240
|
+
when item_view
|
1241
|
+
debounce_reset
|
1242
|
+
warn @dml_link_state.inherited_lines.join("\n")
|
1243
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1244
|
+
|
1245
|
+
else
|
1246
|
+
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1247
|
+
if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
|
1248
|
+
debounce_reset
|
1249
|
+
link_state = LinkState.new
|
1250
|
+
options_state = read_show_options_and_trigger_reuse(
|
1251
|
+
selected: @dml_block_state.block,
|
1252
|
+
link_state: link_state
|
1253
|
+
)
|
1254
|
+
|
1255
|
+
@menu_base_options.merge!(options_state.options)
|
1256
|
+
@delegate_object.merge!(options_state.options)
|
1257
|
+
options_state.load_file_link_state.link_state
|
1258
|
+
else
|
1259
|
+
inpseq_execute_block(block_name)
|
1260
|
+
|
1261
|
+
if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
|
1262
|
+
selected: @dml_block_state.block)
|
1263
|
+
return :break
|
1264
|
+
end
|
1265
|
+
|
1266
|
+
## order of block name processing: link block, cli, from user
|
1267
|
+
#
|
1268
|
+
@dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
|
1269
|
+
HashDelegator.next_link_state(
|
1270
|
+
block_name: @dml_link_state.block_name,
|
1271
|
+
block_name_from_cli: @dml_now_using_cli,
|
1272
|
+
block_state: @dml_block_state,
|
1273
|
+
was_using_cli: @dml_now_using_cli
|
1274
|
+
)
|
1275
|
+
|
1276
|
+
if !@dml_block_state.block[:block_name_from_ui] && cli_break
|
1277
|
+
# &bsp '!block_name_from_ui + cli_break -> break'
|
1278
|
+
return :break
|
1279
|
+
end
|
1280
|
+
|
1281
|
+
InputSequencer.next_link_state(
|
1282
|
+
block_name: @dml_link_state.block_name,
|
1283
|
+
prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
|
1284
|
+
)
|
1285
|
+
end
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
when :exit?
|
1289
|
+
data == $texit
|
1290
|
+
when :stay?
|
1291
|
+
data == $stay
|
1292
|
+
else
|
1293
|
+
raise "Invalid message: #{msg}"
|
1294
|
+
end
|
1295
|
+
end
|
1296
|
+
rescue StandardError
|
1297
|
+
HashDelegator.error_handler('document_inpseq',
|
1298
|
+
{ abort: true })
|
1299
|
+
end
|
1300
|
+
|
1301
|
+
# remove leading "./"
|
1302
|
+
# replace characters: / : . * (space) with: (underscore)
|
1303
|
+
def document_name_in_glob_as_file_name(document_filename, glob)
|
1304
|
+
return document_filename if document_filename.nil? || document_filename.empty?
|
1305
|
+
|
1306
|
+
format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
|
1307
|
+
end
|
1308
|
+
|
1309
|
+
def dump_and_warn_block_state(selected:)
|
1310
|
+
if selected.nil?
|
1311
|
+
Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
|
1312
|
+
{ abort: true })
|
1104
1313
|
end
|
1105
1314
|
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1315
|
+
return unless @delegate_object[:dump_selected_block]
|
1316
|
+
|
1317
|
+
warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
|
1318
|
+
end
|
1319
|
+
|
1320
|
+
# Outputs warnings based on the delegate object's configuration
|
1321
|
+
#
|
1322
|
+
# @param delegate_object [Hash] The delegate object containing configuration flags.
|
1323
|
+
# @param blocks_in_file [Hash] Hash of blocks present in the file.
|
1324
|
+
# @param menu_blocks [Hash] Hash of menu blocks.
|
1325
|
+
# @param link_state [LinkState] Current state of the link.
|
1326
|
+
def dump_delobj(blocks_in_file, menu_blocks, link_state)
|
1327
|
+
warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
|
1328
|
+
|
1329
|
+
if @delegate_object[:dump_blocks_in_file]
|
1330
|
+
warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
|
1331
|
+
label: 'blocks_in_file')
|
1332
|
+
end
|
1333
|
+
|
1334
|
+
if @delegate_object[:dump_menu_blocks]
|
1335
|
+
warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
|
1336
|
+
label: 'menu_blocks')
|
1337
|
+
end
|
1338
|
+
|
1339
|
+
warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
|
1340
|
+
warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
|
1341
|
+
return unless @delegate_object[:dump_inherited_lines]
|
1342
|
+
|
1343
|
+
warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
|
1344
|
+
end
|
1345
|
+
|
1346
|
+
# Opens text in an editor for user modification and returns the modified text.
|
1347
|
+
#
|
1348
|
+
# This method reads the provided text, opens it in the default editor,
|
1349
|
+
# and allows the user to modify it. If the user makes changes, the
|
1350
|
+
# modified text is returned. If the user exits the editor without
|
1351
|
+
# making changes or the editor is closed abruptly, appropriate messages
|
1352
|
+
# are displayed.
|
1353
|
+
#
|
1354
|
+
# @param [String] initial_text The initial text to be edited.
|
1355
|
+
# @param [String] temp_name The base name for the temporary file (default: 'edit_text').
|
1356
|
+
# @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly.
|
1357
|
+
def edit_text(initial_text, temp_name: 'edit_text')
|
1358
|
+
# Create a temporary file to store the initial text
|
1359
|
+
temp_file = Tempfile.new(temp_name)
|
1360
|
+
temp_file.write(initial_text)
|
1361
|
+
temp_file.rewind
|
1362
|
+
|
1363
|
+
# Capture the modification time of the temporary file before editing
|
1364
|
+
before_mtime = temp_file.mtime
|
1365
|
+
|
1366
|
+
# Open the temporary file in the default editor
|
1367
|
+
system("#{ENV['EDITOR'] || 'vi'} #{temp_file.path}")
|
1368
|
+
|
1369
|
+
# Capture the exit status of the editor
|
1370
|
+
editor_exit_status = $?.exitstatus
|
1371
|
+
|
1372
|
+
# Reopen the file to ensure the updated modification time is read
|
1373
|
+
temp_file.open
|
1374
|
+
after_mtime = temp_file.mtime
|
1375
|
+
|
1376
|
+
# Check if the editor was exited normally or was interrupted
|
1377
|
+
if editor_exit_status != 0
|
1378
|
+
warn 'The editor was closed abruptly. No changes were made.'
|
1379
|
+
temp_file.close
|
1380
|
+
temp_file.unlink
|
1381
|
+
return
|
1382
|
+
end
|
1383
|
+
|
1384
|
+
result_text = nil
|
1385
|
+
# Read the file if it was modified
|
1386
|
+
if before_mtime != after_mtime
|
1387
|
+
temp_file.rewind
|
1388
|
+
result_text = temp_file.read
|
1389
|
+
end
|
1390
|
+
|
1391
|
+
# Remove the temporary file
|
1392
|
+
temp_file.close
|
1393
|
+
temp_file.unlink
|
1394
|
+
|
1395
|
+
result_text
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
|
1399
|
+
lfls = execute_shell_type(
|
1400
|
+
selected: selected,
|
1401
|
+
mdoc: mdoc,
|
1402
|
+
link_state: link_state,
|
1403
|
+
block_source: block_source
|
1109
1404
|
)
|
1110
|
-
|
1405
|
+
|
1406
|
+
# if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
|
1407
|
+
[lfls.link_state,
|
1408
|
+
lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
|
1409
|
+
#.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
# Executes a given command and processes its input, output, and error streams.
|
1413
|
+
#
|
1414
|
+
# @param [Array<String>] command the command to execute along with its arguments.
|
1415
|
+
# @yield [stdin, stdout, stderr, thread] if a block is provided, it yields input, output, error lines, and the execution thread.
|
1416
|
+
# @return [Integer] the exit status of the executed command (0 to 255).
|
1417
|
+
#
|
1418
|
+
# @example
|
1419
|
+
# status = execute_command_with_streams(['ls', '-la']) do |stdin, stdout, stderr, thread|
|
1420
|
+
# puts "STDOUT: #{stdout}" if stdout
|
1421
|
+
# puts "STDERR: #{stderr}" if stderr
|
1422
|
+
# end
|
1423
|
+
# puts "Command exited with status: #{status}"
|
1424
|
+
def execute_command_with_streams(command)
|
1425
|
+
exit_status = nil
|
1426
|
+
|
1427
|
+
Open3.popen3(*command) do |stdin, stdout, stderr, exec_thread|
|
1428
|
+
# Handle stdout stream
|
1429
|
+
handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
|
1430
|
+
yield nil, line, nil, exec_thread if block_given?
|
1431
|
+
end
|
1432
|
+
|
1433
|
+
# Handle stderr stream
|
1434
|
+
handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
|
1435
|
+
yield nil, nil, line, exec_thread if block_given?
|
1436
|
+
end
|
1437
|
+
|
1438
|
+
# Handle stdin stream
|
1439
|
+
input_thread = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
|
1440
|
+
stdin.puts(line)
|
1441
|
+
yield line, nil, nil, exec_thread if block_given?
|
1442
|
+
end
|
1443
|
+
|
1444
|
+
# Wait for all streams to be processed
|
1445
|
+
wait_for_stream_processing
|
1446
|
+
exec_thread.join
|
1447
|
+
|
1448
|
+
# Ensure the input thread is killed if it's still alive
|
1449
|
+
sleep 0.1
|
1450
|
+
input_thread.kill if input_thread&.alive?
|
1451
|
+
|
1452
|
+
# Retrieve the exit status
|
1453
|
+
exit_status = exec_thread.value.exitstatus
|
1454
|
+
end
|
1455
|
+
|
1456
|
+
exit_status
|
1457
|
+
end
|
1458
|
+
|
1459
|
+
# Executes a block of code that has been approved for execution.
|
1460
|
+
# It sets the script block name, writes command files if required, and handles the execution
|
1461
|
+
# including output formatting and summarization.
|
1462
|
+
#
|
1463
|
+
# @param required_lines [Array<String>] The lines of code to be executed.
|
1464
|
+
# @param selected [FCB] The selected functional code block object.
|
1465
|
+
def execute_required_lines(required_lines: [], selected: FCB.new)
|
1466
|
+
write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script]
|
1467
|
+
calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state
|
1468
|
+
format_and_execute_command(code_lines: required_lines)
|
1469
|
+
post_execution_process
|
1470
|
+
end
|
1471
|
+
|
1472
|
+
# Execute a code block after approval and provide user interaction options.
|
1473
|
+
#
|
1474
|
+
# This method displays required code blocks, asks for user approval, and
|
1475
|
+
# executes the code block if approved. It also allows users to copy the
|
1476
|
+
# code to the clipboard or save it to a file.
|
1477
|
+
#
|
1478
|
+
# @param opts [Hash] Options hash containing configuration settings.
|
1479
|
+
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
1480
|
+
#
|
1481
|
+
def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new)
|
1482
|
+
if selected.fetch(:shell, '') == BlockType::LINK
|
1483
|
+
debounce_reset
|
1484
|
+
push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''),
|
1485
|
+
mdoc: mdoc,
|
1486
|
+
selected: selected,
|
1487
|
+
link_state: link_state,
|
1488
|
+
block_source: block_source)
|
1489
|
+
|
1490
|
+
elsif @menu_user_clicked_back_link
|
1491
|
+
debounce_reset
|
1492
|
+
pop_link_history_and_trigger_load
|
1493
|
+
|
1494
|
+
elsif selected[:shell] == BlockType::OPTS
|
1495
|
+
debounce_reset
|
1496
|
+
block_names = []
|
1497
|
+
code_lines = []
|
1498
|
+
dependencies = {}
|
1499
|
+
options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state)
|
1500
|
+
|
1501
|
+
## apply options to current state
|
1502
|
+
#
|
1503
|
+
@menu_base_options.merge!(options_state.options)
|
1504
|
+
@delegate_object.merge!(options_state.options)
|
1505
|
+
|
1506
|
+
### options_state.load_file_link_state
|
1507
|
+
link_state = LinkState.new
|
1508
|
+
link_history_push_and_next(
|
1509
|
+
curr_block_name: selected.pub_name,
|
1510
|
+
curr_document_filename: @delegate_object[:filename],
|
1511
|
+
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1512
|
+
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1513
|
+
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1514
|
+
next_block_name: '',
|
1515
|
+
next_document_filename: @delegate_object[:filename],
|
1516
|
+
next_load_file: LoadFile::REUSE
|
1517
|
+
)
|
1518
|
+
|
1519
|
+
elsif selected[:shell] == BlockType::VARS
|
1520
|
+
debounce_reset
|
1521
|
+
block_names = []
|
1522
|
+
code_lines = set_environment_variables_for_block(selected)
|
1523
|
+
dependencies = {}
|
1524
|
+
link_history_push_and_next(
|
1525
|
+
curr_block_name: selected.pub_name,
|
1526
|
+
curr_document_filename: @delegate_object[:filename],
|
1527
|
+
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1528
|
+
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1529
|
+
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1530
|
+
next_block_name: '',
|
1531
|
+
next_document_filename: @delegate_object[:filename],
|
1532
|
+
next_load_file: LoadFile::REUSE
|
1533
|
+
)
|
1534
|
+
|
1535
|
+
elsif debounce_allows
|
1536
|
+
compile_execute_and_trigger_reuse(mdoc: mdoc,
|
1537
|
+
selected: selected,
|
1538
|
+
link_state: link_state,
|
1539
|
+
block_source: block_source)
|
1540
|
+
|
1541
|
+
else
|
1542
|
+
LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
1543
|
+
end
|
1544
|
+
end
|
1545
|
+
|
1546
|
+
# Retrieves a specific data symbol from the delegate object, converts it to a string,
|
1547
|
+
# and applies a color style based on the specified color symbol.
|
1548
|
+
#
|
1549
|
+
# @param default [String] The default value if the data symbol is not found.
|
1550
|
+
# @param data_sym [Symbol] The symbol key to fetch data from the delegate object.
|
1551
|
+
# @param color_sym [Symbol] The symbol key to fetch the color option for styling.
|
1552
|
+
# @return [String] The color-styled string.
|
1553
|
+
def fetch_color(default: '',
|
1554
|
+
data_sym: :execution_report_preview_head,
|
1555
|
+
color_sym: :execution_report_preview_frame_color)
|
1556
|
+
data_string = @delegate_object.fetch(data_sym, default).to_s
|
1557
|
+
string_send_color(data_string, color_sym)
|
1558
|
+
end
|
1559
|
+
|
1560
|
+
def format_and_execute_command(code_lines:)
|
1561
|
+
formatted_command = code_lines.flatten.join("\n")
|
1562
|
+
@fout.fout fetch_color(data_sym: :script_execution_head,
|
1563
|
+
color_sym: :script_execution_frame_color)
|
1564
|
+
command_execute(formatted_command, args: @pass_args)
|
1565
|
+
@fout.fout fetch_color(data_sym: :script_execution_tail,
|
1566
|
+
color_sym: :script_execution_frame_color)
|
1567
|
+
end
|
1568
|
+
|
1569
|
+
# Format expression using environment variables and run state
|
1570
|
+
def format_expression(expr)
|
1571
|
+
data = link_load_format_data
|
1572
|
+
ENV.each { |key, value| data[key] = value }
|
1573
|
+
format(expr, data)
|
1111
1574
|
end
|
1112
1575
|
|
1113
1576
|
# Formats multiline body content as a title string.
|
@@ -1120,6 +1583,61 @@ module MarkdownExec
|
|
1120
1583
|
end.join("\n") + "\n"
|
1121
1584
|
end
|
1122
1585
|
|
1586
|
+
# Formats a string based on a given context and applies color styling to it.
|
1587
|
+
# It retrieves format and color information from the delegate object and processes accordingly.
|
1588
|
+
#
|
1589
|
+
# @param default [String] The default value if the format symbol is not found (unused in current implementation).
|
1590
|
+
# @param context [Hash] Contextual data used for string formatting.
|
1591
|
+
# @param format_sym [Symbol] Symbol key to fetch the format string from the delegate object.
|
1592
|
+
# @param color_sym [Symbol] Symbol key to fetch the color option for string styling.
|
1593
|
+
# @return [String] The formatted and color-styled string.
|
1594
|
+
def format_references_send_color(default: '', context: {},
|
1595
|
+
format_sym: :output_execution_label_format,
|
1596
|
+
color_sym: :execution_report_preview_frame_color)
|
1597
|
+
formatted_string = format(@delegate_object.fetch(format_sym, ''),
|
1598
|
+
context).to_s
|
1599
|
+
string_send_color(formatted_string, color_sym)
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
# Expand expression if it contains format specifiers
|
1603
|
+
def formatted_expression(expr)
|
1604
|
+
expr.include?('%{') ? format_expression(expr) : expr
|
1605
|
+
end
|
1606
|
+
|
1607
|
+
# Processes a block to generate its summary, modifying its attributes based on various matching criteria.
|
1608
|
+
# It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
|
1609
|
+
#
|
1610
|
+
# @param fcb [Object] An object representing a functional code block.
|
1611
|
+
# @return [Object] The modified functional code block with updated summary attributes.
|
1612
|
+
def get_block_summary(fcb)
|
1613
|
+
return fcb unless @delegate_object[:bash]
|
1614
|
+
|
1615
|
+
fcb.call = fcb.title.match(Regexp.new(@delegate_object[:block_calls_scan]))&.fetch(1, nil)
|
1616
|
+
titlexcall = fcb.call ? fcb.title.sub("%#{fcb.call}", '') : fcb.title
|
1617
|
+
bm = extract_named_captures_from_option(titlexcall,
|
1618
|
+
@delegate_object[:block_name_match])
|
1619
|
+
|
1620
|
+
fcb.stdin = extract_named_captures_from_option(titlexcall,
|
1621
|
+
@delegate_object[:block_stdin_scan])
|
1622
|
+
fcb.stdout = extract_named_captures_from_option(titlexcall,
|
1623
|
+
@delegate_object[:block_stdout_scan])
|
1624
|
+
|
1625
|
+
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
1626
|
+
|
1627
|
+
if @delegate_object[:block_name_nick_match].present? && fcb.oname =~ Regexp.new(@delegate_object[:block_name_nick_match])
|
1628
|
+
fcb.nickname = $~[0]
|
1629
|
+
fcb.title = fcb.oname = format_multiline_body_as_title(fcb.body)
|
1630
|
+
else
|
1631
|
+
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
1632
|
+
end
|
1633
|
+
|
1634
|
+
fcb.dname = HashDelegator.indent_all_lines(
|
1635
|
+
apply_shell_color_option(fcb.oname, shell_color_option),
|
1636
|
+
fcb.fetch(:indent, nil)
|
1637
|
+
)
|
1638
|
+
fcb
|
1639
|
+
end
|
1640
|
+
|
1123
1641
|
# Updates the delegate object's state based on the provided block state.
|
1124
1642
|
# It sets the block name and determines if the user clicked the back link in the menu.
|
1125
1643
|
#
|
@@ -1131,7 +1649,7 @@ module MarkdownExec
|
|
1131
1649
|
return
|
1132
1650
|
end
|
1133
1651
|
|
1134
|
-
@delegate_object[:block_name] = block_state.block
|
1652
|
+
@delegate_object[:block_name] = block_state.block.pub_name
|
1135
1653
|
@menu_user_clicked_back_link = block_state.state == MenuState::BACK
|
1136
1654
|
end
|
1137
1655
|
|
@@ -1140,7 +1658,7 @@ module MarkdownExec
|
|
1140
1658
|
Thread.new do
|
1141
1659
|
stream.each_line do |line|
|
1142
1660
|
line.strip!
|
1143
|
-
@run_state.files[file_type] << line
|
1661
|
+
@run_state.files[file_type] << line if @run_state.files
|
1144
1662
|
|
1145
1663
|
if @delegate_object[:output_stdout]
|
1146
1664
|
# print line
|
@@ -1157,21 +1675,61 @@ module MarkdownExec
|
|
1157
1675
|
end
|
1158
1676
|
end
|
1159
1677
|
|
1160
|
-
# Initializes variables for regex and other states
|
1161
|
-
def initial_state
|
1162
|
-
{
|
1163
|
-
fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch(
|
1164
|
-
:fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
|
1165
|
-
)),
|
1166
|
-
fenced_start_extended_regex: Regexp.new(@delegate_object.fetch(
|
1167
|
-
:fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
|
1168
|
-
)),
|
1169
|
-
fcb: MarkdownExec::FCB.new,
|
1170
|
-
in_fenced_block: false,
|
1171
|
-
headings: []
|
1172
|
-
}
|
1173
|
-
end
|
1174
|
-
|
1678
|
+
# Initializes variables for regex and other states
|
1679
|
+
def initial_state
|
1680
|
+
{
|
1681
|
+
fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch(
|
1682
|
+
:fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
|
1683
|
+
)),
|
1684
|
+
fenced_start_extended_regex: Regexp.new(@delegate_object.fetch(
|
1685
|
+
:fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
|
1686
|
+
)),
|
1687
|
+
fcb: MarkdownExec::FCB.new,
|
1688
|
+
in_fenced_block: false,
|
1689
|
+
headings: []
|
1690
|
+
}
|
1691
|
+
end
|
1692
|
+
|
1693
|
+
def inpseq_execute_block(block_name)
|
1694
|
+
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1695
|
+
|
1696
|
+
dump_and_warn_block_state(selected: @dml_block_state.block)
|
1697
|
+
@dml_link_state, @dml_menu_default_dname = \
|
1698
|
+
exec_bash_next_state(
|
1699
|
+
selected: @dml_block_state.block,
|
1700
|
+
mdoc: @dml_mdoc,
|
1701
|
+
link_state: @dml_link_state,
|
1702
|
+
block_source: {
|
1703
|
+
document_filename: @delegate_object[:filename],
|
1704
|
+
time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
|
1705
|
+
}
|
1706
|
+
)
|
1707
|
+
end
|
1708
|
+
|
1709
|
+
def inpseq_parse_document(_document_filename)
|
1710
|
+
@run_state.batch_index += 1
|
1711
|
+
@run_state.in_own_window = false
|
1712
|
+
|
1713
|
+
# &bsp 'loop', block_name_from_cli, @cli_block_name
|
1714
|
+
@run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
|
1715
|
+
set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
|
1716
|
+
now_using_cli: @dml_now_using_cli,
|
1717
|
+
link_state: @dml_link_state)
|
1718
|
+
end
|
1719
|
+
|
1720
|
+
def inpseq_user_choice
|
1721
|
+
@dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
|
1722
|
+
menu_blocks: @dml_menu_blocks,
|
1723
|
+
default: @dml_menu_default_dname)
|
1724
|
+
# &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
|
1725
|
+
if !@dml_block_state
|
1726
|
+
HashDelegator.error_handler('block_state missing', { abort: true })
|
1727
|
+
elsif @dml_block_state.state == MenuState::EXIT
|
1728
|
+
# &bsp 'load_cli_or_user_selected_block -> break'
|
1729
|
+
:break
|
1730
|
+
end
|
1731
|
+
end
|
1732
|
+
|
1175
1733
|
# Iterates through blocks in a file, applying the provided block to each line.
|
1176
1734
|
# The iteration only occurs if the file exists.
|
1177
1735
|
# @yield [Symbol] :filter Yields to obtain selected messages for processing.
|
@@ -1198,25 +1756,9 @@ module MarkdownExec
|
|
1198
1756
|
file.write(all_code.join("\n"))
|
1199
1757
|
file.rewind
|
1200
1758
|
|
1201
|
-
if link_block_data.fetch(LinkKeys::
|
1759
|
+
if link_block_data.fetch(LinkKeys::EXEC, false)
|
1202
1760
|
@run_state.files = Hash.new([])
|
1203
|
-
|
1204
|
-
Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
|
1205
|
-
handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
|
1206
|
-
output_lines.push(line)
|
1207
|
-
end
|
1208
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
|
1209
|
-
output_lines.push(line)
|
1210
|
-
end
|
1211
|
-
|
1212
|
-
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
|
1213
|
-
stdin.puts(line)
|
1214
|
-
end
|
1215
|
-
|
1216
|
-
wait_for_stream_processing
|
1217
|
-
sleep 0.1
|
1218
|
-
in_thr.kill if in_thr&.alive?
|
1219
|
-
end
|
1761
|
+
execute_command_with_streams([cmd])
|
1220
1762
|
|
1221
1763
|
## select output_lines that look like assignment or match other specs
|
1222
1764
|
#
|
@@ -1239,13 +1781,13 @@ module MarkdownExec
|
|
1239
1781
|
label_format_below = @delegate_object[:shell_code_label_format_below]
|
1240
1782
|
|
1241
1783
|
[label_format_above && format(label_format_above,
|
1242
|
-
block_source.merge({ block_name: selected
|
1784
|
+
block_source.merge({ block_name: selected.pub_name }))] +
|
1243
1785
|
output_lines.map do |line|
|
1244
1786
|
re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
|
1245
1787
|
re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line
|
1246
1788
|
end.compact +
|
1247
1789
|
[label_format_below && format(label_format_below,
|
1248
|
-
block_source.merge({ block_name: selected
|
1790
|
+
block_source.merge({ block_name: selected.pub_name }))]
|
1249
1791
|
end
|
1250
1792
|
|
1251
1793
|
def link_history_push_and_next(
|
@@ -1275,6 +1817,54 @@ module MarkdownExec
|
|
1275
1817
|
)
|
1276
1818
|
end
|
1277
1819
|
|
1820
|
+
def link_load_format_data
|
1821
|
+
{
|
1822
|
+
batch_index: @run_state.batch_index,
|
1823
|
+
batch_random: @run_state.batch_random,
|
1824
|
+
block_name: @delegate_object[:block_name],
|
1825
|
+
document_filename: File.basename(@delegate_object[:filename]),
|
1826
|
+
document_filespec: @delegate_object[:filename],
|
1827
|
+
home: Dir.pwd,
|
1828
|
+
started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format])
|
1829
|
+
}
|
1830
|
+
end
|
1831
|
+
|
1832
|
+
# Loads auto blocks based on delegate object settings and updates if new filename is detected.
|
1833
|
+
# Executes a specified block once per filename.
|
1834
|
+
# @param all_blocks [Array] Array of all block elements.
|
1835
|
+
# @return [Boolean, nil] True if values were modified, nil otherwise.
|
1836
|
+
def load_auto_opts_block(all_blocks)
|
1837
|
+
block_name = @delegate_object[:document_load_opts_block_name]
|
1838
|
+
return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
|
1839
|
+
|
1840
|
+
block = HashDelegator.block_find(all_blocks, :oname, block_name)
|
1841
|
+
return unless block
|
1842
|
+
|
1843
|
+
options_state = read_show_options_and_trigger_reuse(selected: block)
|
1844
|
+
@menu_base_options.merge!(options_state.options)
|
1845
|
+
@delegate_object.merge!(options_state.options)
|
1846
|
+
|
1847
|
+
@most_recent_loaded_filename = @delegate_object[:filename]
|
1848
|
+
true
|
1849
|
+
end
|
1850
|
+
|
1851
|
+
def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
|
1852
|
+
if @delegate_object[:block_name].present?
|
1853
|
+
block = all_blocks.find do |item|
|
1854
|
+
item.pub_name == @delegate_object[:block_name]
|
1855
|
+
end&.merge(block_name_from_ui: false)
|
1856
|
+
else
|
1857
|
+
block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
|
1858
|
+
default)
|
1859
|
+
block = block_state.block&.merge(block_name_from_ui: true)
|
1860
|
+
state = block_state.state
|
1861
|
+
end
|
1862
|
+
|
1863
|
+
SelectedBlockMenuState.new(block, state)
|
1864
|
+
rescue StandardError
|
1865
|
+
HashDelegator.error_handler('load_cli_or_user_selected_block')
|
1866
|
+
end
|
1867
|
+
|
1278
1868
|
# format + glob + select for file in load block
|
1279
1869
|
# name has references to ENV vars and doc and batch vars incl. timestamp
|
1280
1870
|
def load_filespec_from_expression(expression)
|
@@ -1289,68 +1879,8 @@ module MarkdownExec
|
|
1289
1879
|
end
|
1290
1880
|
end
|
1291
1881
|
|
1292
|
-
def save_filespec_from_expression(expression)
|
1293
|
-
# Process expression with embedded formatting
|
1294
|
-
formatted = formatted_expression(expression)
|
1295
|
-
|
1296
|
-
# Handle wildcards or direct file specification
|
1297
|
-
if contains_wildcards?(formatted)
|
1298
|
-
save_filespec_wildcard_expansion(formatted)
|
1299
|
-
else
|
1300
|
-
formatted
|
1301
|
-
end
|
1302
|
-
end
|
1303
|
-
|
1304
1882
|
# private
|
1305
1883
|
|
1306
|
-
# Expand expression if it contains format specifiers
|
1307
|
-
def formatted_expression(expr)
|
1308
|
-
expr.include?('%{') ? format_expression(expr) : expr
|
1309
|
-
end
|
1310
|
-
|
1311
|
-
# Format expression using environment variables and run state
|
1312
|
-
def format_expression(expr)
|
1313
|
-
data = link_load_format_data
|
1314
|
-
ENV.each { |key, value| data[key] = value }
|
1315
|
-
format(expr, data)
|
1316
|
-
end
|
1317
|
-
|
1318
|
-
# Check if the expression contains wildcard characters
|
1319
|
-
def contains_wildcards?(expr)
|
1320
|
-
expr.match(%r{\*|\?|\[})
|
1321
|
-
end
|
1322
|
-
|
1323
|
-
# Handle expression with wildcard characters
|
1324
|
-
def load_filespec_wildcard_expansion(expr)
|
1325
|
-
files = find_files(expr)
|
1326
|
-
case files.count
|
1327
|
-
when 0
|
1328
|
-
HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
|
1329
|
-
when 1
|
1330
|
-
files.first
|
1331
|
-
else
|
1332
|
-
prompt_select_code_filename(files)
|
1333
|
-
end
|
1334
|
-
end
|
1335
|
-
|
1336
|
-
# Handle expression with wildcard characters
|
1337
|
-
# allow user to select or enter
|
1338
|
-
def puts_gets_oprompt_(filespec)
|
1339
|
-
puts format(@delegate_object[:prompt_show_expr_format],
|
1340
|
-
{ expr: filespec })
|
1341
|
-
puts @delegate_object[:prompt_enter_filespec]
|
1342
|
-
gets.chomp
|
1343
|
-
end
|
1344
|
-
|
1345
|
-
# prompt user to enter a path (i.e. containing a path separator)
|
1346
|
-
# or name to substitute into the wildcard expression
|
1347
|
-
def prompt_for_filespec_with_wildcard(filespec)
|
1348
|
-
puts format(@delegate_object[:prompt_show_expr_format],
|
1349
|
-
{ expr: filespec })
|
1350
|
-
puts @delegate_object[:prompt_enter_filespec]
|
1351
|
-
PathUtils.resolve_path_or_substitute(gets.chomp, filespec)
|
1352
|
-
end
|
1353
|
-
|
1354
1884
|
# def read_block_name(line)
|
1355
1885
|
# bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
|
1356
1886
|
# name = bm[:title]
|
@@ -1363,38 +1893,6 @@ module MarkdownExec
|
|
1363
1893
|
# name
|
1364
1894
|
# end
|
1365
1895
|
|
1366
|
-
# Handle expression with wildcard characters
|
1367
|
-
# allow user to select or enter
|
1368
|
-
def save_filespec_wildcard_expansion(filespec)
|
1369
|
-
files = find_files(filespec)
|
1370
|
-
case files.count
|
1371
|
-
when 0
|
1372
|
-
prompt_for_filespec_with_wildcard(filespec)
|
1373
|
-
else
|
1374
|
-
## user selects from existing files or other
|
1375
|
-
# input into path with wildcard for easy entry
|
1376
|
-
#
|
1377
|
-
name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
|
1378
|
-
if name == @delegate_object[:prompt_filespec_other]
|
1379
|
-
prompt_for_filespec_with_wildcard(filespec)
|
1380
|
-
else
|
1381
|
-
name
|
1382
|
-
end
|
1383
|
-
end
|
1384
|
-
end
|
1385
|
-
|
1386
|
-
def link_load_format_data
|
1387
|
-
{
|
1388
|
-
batch_index: @run_state.batch_index,
|
1389
|
-
batch_random: @run_state.batch_random,
|
1390
|
-
block_name: @delegate_object[:block_name],
|
1391
|
-
document_filename: File.basename(@delegate_object[:filename]),
|
1392
|
-
document_filespec: @delegate_object[:filename],
|
1393
|
-
home: Dir.pwd,
|
1394
|
-
started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format])
|
1395
|
-
}
|
1396
|
-
end
|
1397
|
-
|
1398
1896
|
# # Loads auto link block.
|
1399
1897
|
# def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
|
1400
1898
|
# block_name = @delegate_object[:document_load_link_block_name]
|
@@ -1418,23 +1916,23 @@ module MarkdownExec
|
|
1418
1916
|
# end
|
1419
1917
|
# end
|
1420
1918
|
|
1421
|
-
#
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1919
|
+
# Handle expression with wildcard characters
|
1920
|
+
def load_filespec_wildcard_expansion(expr, auto_load_single: false)
|
1921
|
+
files = find_files(expr)
|
1922
|
+
if files.count.zero?
|
1923
|
+
HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
|
1924
|
+
elsif auto_load_single && files.count == 1
|
1925
|
+
files.first
|
1926
|
+
else
|
1927
|
+
## user selects from existing files or other
|
1928
|
+
#
|
1929
|
+
case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
|
1930
|
+
when @delegate_object[:prompt_filespec_back]
|
1931
|
+
# do nothing
|
1932
|
+
else
|
1933
|
+
name
|
1934
|
+
end
|
1935
|
+
end
|
1438
1936
|
end
|
1439
1937
|
|
1440
1938
|
def mdoc_and_blocks_from_nested_files
|
@@ -1458,10 +1956,37 @@ module MarkdownExec
|
|
1458
1956
|
menu_blocks = mdoc.fcbs_per_options(@delegate_object)
|
1459
1957
|
add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
|
1460
1958
|
### compress empty lines
|
1461
|
-
HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
|
1959
|
+
HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
|
1462
1960
|
[all_blocks, menu_blocks, mdoc]
|
1463
1961
|
end
|
1464
1962
|
|
1963
|
+
def menu_add_disabled_option(name)
|
1964
|
+
raise unless name.present?
|
1965
|
+
raise if @dml_menu_blocks.nil?
|
1966
|
+
|
1967
|
+
block = @dml_menu_blocks.find { |item| item[:oname] == name }
|
1968
|
+
|
1969
|
+
# create menu item when it is needed (count > 0)
|
1970
|
+
#
|
1971
|
+
return unless block.nil?
|
1972
|
+
|
1973
|
+
# append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD)
|
1974
|
+
chrome_block = FCB.new(
|
1975
|
+
chrome: true,
|
1976
|
+
disabled: '',
|
1977
|
+
dname: HashDelegator.new(@delegate_object).string_send_color(
|
1978
|
+
name, :menu_inherited_lines_color
|
1979
|
+
),
|
1980
|
+
oname: formatted_name
|
1981
|
+
)
|
1982
|
+
|
1983
|
+
if insert_at_top
|
1984
|
+
@dml_menu_blocks.unshift(chrome_block)
|
1985
|
+
else
|
1986
|
+
@dml_menu_blocks.push(chrome_block)
|
1987
|
+
end
|
1988
|
+
end
|
1989
|
+
|
1465
1990
|
# Formats and optionally colors a menu option based on delegate object's configuration.
|
1466
1991
|
# @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
|
1467
1992
|
# @return [String] The formatted and possibly colored value of the menu option.
|
@@ -1486,6 +2011,47 @@ module MarkdownExec
|
|
1486
2011
|
end
|
1487
2012
|
end
|
1488
2013
|
|
2014
|
+
def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
|
2015
|
+
raise unless name.present?
|
2016
|
+
raise if @dml_menu_blocks.nil?
|
2017
|
+
|
2018
|
+
item = @dml_menu_blocks.find { |block| block[:oname] == name }
|
2019
|
+
|
2020
|
+
# create menu item when it is needed (count > 0)
|
2021
|
+
#
|
2022
|
+
if item.nil? && count.positive?
|
2023
|
+
append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
|
2024
|
+
item = @dml_menu_blocks.find { |block| block[:oname] == name }
|
2025
|
+
end
|
2026
|
+
|
2027
|
+
# update item if it exists
|
2028
|
+
#
|
2029
|
+
return unless item
|
2030
|
+
|
2031
|
+
item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name
|
2032
|
+
if count.positive?
|
2033
|
+
item.delete(:disabled)
|
2034
|
+
else
|
2035
|
+
item[:disabled] = ''
|
2036
|
+
end
|
2037
|
+
end
|
2038
|
+
|
2039
|
+
def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
|
2040
|
+
if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
|
2041
|
+
# &bsp 'pause cli control, allow user to select block'
|
2042
|
+
block_name_from_cli = false
|
2043
|
+
now_using_cli = false
|
2044
|
+
@menu_base_options[:block_name] = \
|
2045
|
+
@delegate_object[:block_name] = \
|
2046
|
+
link_state.block_name = \
|
2047
|
+
@cli_block_name = nil
|
2048
|
+
end
|
2049
|
+
|
2050
|
+
@delegate_object = @menu_base_options.dup
|
2051
|
+
@menu_user_clicked_back_link = false
|
2052
|
+
[block_name_from_cli, now_using_cli]
|
2053
|
+
end
|
2054
|
+
|
1489
2055
|
# If a method is missing, treat it as a key for the @delegate_object.
|
1490
2056
|
def method_missing(method_name, *args, &block)
|
1491
2057
|
if @delegate_object.respond_to?(method_name)
|
@@ -1567,20 +2133,20 @@ module MarkdownExec
|
|
1567
2133
|
@link_history.push(next_state)
|
1568
2134
|
|
1569
2135
|
next_state.block_name = nil
|
1570
|
-
LoadFileLinkState.new(LoadFile::
|
2136
|
+
LoadFileLinkState.new(LoadFile::LOAD, next_state)
|
1571
2137
|
else
|
1572
2138
|
# no history exists; must have been called independently => retain script
|
1573
2139
|
link_history_push_and_next(
|
1574
|
-
curr_block_name: selected
|
2140
|
+
curr_block_name: selected.pub_name,
|
1575
2141
|
curr_document_filename: @delegate_object[:filename],
|
1576
2142
|
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1577
2143
|
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1578
2144
|
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1579
|
-
next_block_name: '', # not link_block_data[LinkKeys::
|
2145
|
+
next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || ''
|
1580
2146
|
next_document_filename: @delegate_object[:filename], # not next_document_filename
|
1581
|
-
next_load_file: LoadFile::
|
2147
|
+
next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
|
1582
2148
|
)
|
1583
|
-
# LoadFileLinkState.new(LoadFile::
|
2149
|
+
# LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
1584
2150
|
end
|
1585
2151
|
end
|
1586
2152
|
|
@@ -1591,7 +2157,7 @@ module MarkdownExec
|
|
1591
2157
|
def pop_link_history_and_trigger_load
|
1592
2158
|
pop = @link_history.pop
|
1593
2159
|
peek = @link_history.peek
|
1594
|
-
LoadFileLinkState.new(LoadFile::
|
2160
|
+
LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
|
1595
2161
|
document_filename: pop.document_filename,
|
1596
2162
|
inherited_block_names: peek.inherited_block_names,
|
1597
2163
|
inherited_dependencies: peek.inherited_dependencies,
|
@@ -1705,6 +2271,32 @@ module MarkdownExec
|
|
1705
2271
|
exit 1
|
1706
2272
|
end
|
1707
2273
|
|
2274
|
+
def prompt_for_command(prompt)
|
2275
|
+
print prompt
|
2276
|
+
|
2277
|
+
gets.chomp
|
2278
|
+
rescue Interrupt
|
2279
|
+
nil
|
2280
|
+
end
|
2281
|
+
|
2282
|
+
# Prompts the user to enter a path or name to substitute into the wildcard expression.
|
2283
|
+
# If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
|
2284
|
+
#
|
2285
|
+
# @param filespec [String] the wildcard expression to be substituted
|
2286
|
+
# @return [String, nil] the resolved path or substituted expression, or nil if interrupted
|
2287
|
+
def prompt_for_filespec_with_wildcard(filespec)
|
2288
|
+
puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
|
2289
|
+
puts @delegate_object[:prompt_enter_filespec]
|
2290
|
+
|
2291
|
+
begin
|
2292
|
+
input = gets.chomp
|
2293
|
+
PathUtils.resolve_path_or_substitute(input, filespec)
|
2294
|
+
rescue Interrupt
|
2295
|
+
puts "\nOperation interrupted. Returning nil."
|
2296
|
+
nil
|
2297
|
+
end
|
2298
|
+
end
|
2299
|
+
|
1708
2300
|
##
|
1709
2301
|
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
1710
2302
|
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
@@ -1748,38 +2340,46 @@ module MarkdownExec
|
|
1748
2340
|
exit 1
|
1749
2341
|
end
|
1750
2342
|
|
1751
|
-
|
1752
|
-
|
1753
|
-
|
2343
|
+
# public
|
2344
|
+
|
2345
|
+
def prompt_select_code_filename(filenames)
|
2346
|
+
@prompt.select(
|
2347
|
+
string_send_color(@delegate_object[:prompt_select_code_file],
|
1754
2348
|
:prompt_color_after_script_execution),
|
1755
2349
|
filter: true,
|
1756
2350
|
quiet: true
|
1757
2351
|
) do |menu|
|
1758
|
-
|
1759
|
-
|
2352
|
+
filenames.each do |filename|
|
2353
|
+
menu.choice filename
|
2354
|
+
end
|
1760
2355
|
end
|
1761
|
-
sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1762
2356
|
rescue TTY::Reader::InputInterrupt
|
1763
2357
|
exit 1
|
1764
2358
|
end
|
1765
|
-
|
1766
|
-
|
1767
|
-
|
1768
|
-
|
1769
|
-
@prompt.select(
|
1770
|
-
string_send_color(@delegate_object[:prompt_select_code_file],
|
2359
|
+
|
2360
|
+
def prompt_select_continue
|
2361
|
+
sel = @prompt.select(
|
2362
|
+
string_send_color(@delegate_object[:prompt_after_script_execution],
|
1771
2363
|
:prompt_color_after_script_execution),
|
1772
2364
|
filter: true,
|
1773
2365
|
quiet: true
|
1774
2366
|
) do |menu|
|
1775
|
-
|
1776
|
-
|
1777
|
-
end
|
2367
|
+
menu.choice @delegate_object[:prompt_yes]
|
2368
|
+
menu.choice @delegate_object[:prompt_exit]
|
1778
2369
|
end
|
2370
|
+
sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1779
2371
|
rescue TTY::Reader::InputInterrupt
|
1780
2372
|
exit 1
|
1781
2373
|
end
|
1782
2374
|
|
2375
|
+
# user prompt to exit if the menu will be displayed again
|
2376
|
+
#
|
2377
|
+
def prompt_user_exit(block_name_from_cli:, selected:)
|
2378
|
+
selected[:shell] == BlockType::BASH &&
|
2379
|
+
@delegate_object[:pause_after_script_execution] &&
|
2380
|
+
prompt_select_continue == MenuState::EXIT
|
2381
|
+
end
|
2382
|
+
|
1783
2383
|
# Handles the processing of a link block in Markdown Execution.
|
1784
2384
|
# It loads YAML data from the link_block_body content, pushes the state to history,
|
1785
2385
|
# sets environment variables, and decides on the next block to load.
|
@@ -1796,7 +2396,7 @@ module MarkdownExec
|
|
1796
2396
|
#
|
1797
2397
|
if mdoc
|
1798
2398
|
code_info = mdoc.collect_recursively_required_code(
|
1799
|
-
anyname: selected
|
2399
|
+
anyname: selected.pub_name,
|
1800
2400
|
label_format_above: @delegate_object[:shell_code_label_format_above],
|
1801
2401
|
label_format_below: @delegate_object[:shell_code_label_format_below],
|
1802
2402
|
block_source: block_source
|
@@ -1812,397 +2412,189 @@ module MarkdownExec
|
|
1812
2412
|
|
1813
2413
|
# load key and values from link block into current environment
|
1814
2414
|
#
|
1815
|
-
if link_block_data[LinkKeys::
|
1816
|
-
code_lines.push BashCommentFormatter.format_comment(selected
|
1817
|
-
(link_block_data[LinkKeys::
|
2415
|
+
if link_block_data[LinkKeys::VARS]
|
2416
|
+
code_lines.push BashCommentFormatter.format_comment(selected.pub_name)
|
2417
|
+
(link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
|
1818
2418
|
ENV[key] = value.to_s
|
1819
2419
|
code_lines.push(assign_key_value_in_bash(key, value))
|
1820
2420
|
end
|
1821
|
-
end
|
1822
|
-
|
1823
|
-
## append blocks loaded, apply LinkKeys::Eval
|
1824
|
-
#
|
1825
|
-
if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
|
1826
|
-
load_filespec = load_filespec_from_expression(load_expr)
|
1827
|
-
code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
|
1828
|
-
end
|
1829
|
-
|
1830
|
-
# if an eval link block, evaluate code_lines and return its standard output
|
1831
|
-
#
|
1832
|
-
if link_block_data.fetch(LinkKeys::Eval,
|
1833
|
-
false) || link_block_data.fetch(LinkKeys::Exec, false)
|
1834
|
-
code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
|
1835
|
-
end
|
1836
|
-
|
1837
|
-
next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
|
1838
|
-
|
1839
|
-
if link_block_data[LinkKeys::Return]
|
1840
|
-
pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
|
1841
|
-
dependencies, selected)
|
1842
|
-
|
1843
|
-
else
|
1844
|
-
link_history_push_and_next(
|
1845
|
-
curr_block_name: selected[:oname],
|
1846
|
-
curr_document_filename: @delegate_object[:filename],
|
1847
|
-
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1848
|
-
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1849
|
-
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1850
|
-
next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
|
1851
|
-
nil) || link_block_data[LinkKeys::Block] || '',
|
1852
|
-
next_document_filename: next_document_filename,
|
1853
|
-
next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
|
1854
|
-
)
|
1855
|
-
end
|
1856
|
-
end
|
1857
|
-
|
1858
|
-
# Check if the delegate object responds to a given method.
|
1859
|
-
# @param method_name [Symbol] The name of the method to check.
|
1860
|
-
# @param include_private [Boolean] Whether to include private methods in the check.
|
1861
|
-
# @return [Boolean] true if the delegate object responds to the method, false otherwise.
|
1862
|
-
def respond_to?(method_name, include_private = false)
|
1863
|
-
if super
|
1864
|
-
true
|
1865
|
-
elsif @delegate_object.respond_to?(method_name, include_private)
|
1866
|
-
true
|
1867
|
-
elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
|
1868
|
-
true
|
1869
|
-
else
|
1870
|
-
@delegate_object.respond_to?(method_name, include_private)
|
1871
|
-
end
|
1872
|
-
end
|
1873
|
-
|
1874
|
-
def runtime_exception(exception_sym, name, items)
|
1875
|
-
if @delegate_object[exception_sym] != 0
|
1876
|
-
data = { name: name, detail: items.join(', ') }
|
1877
|
-
warn(
|
1878
|
-
format(
|
1879
|
-
@delegate_object.fetch(:exception_format_name, "\n%{name}"),
|
1880
|
-
data
|
1881
|
-
).send(@delegate_object.fetch(:exception_color_name, :red)) +
|
1882
|
-
format(
|
1883
|
-
@delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
|
1884
|
-
data
|
1885
|
-
).send(@delegate_object.fetch(:exception_color_detail, :yellow))
|
1886
|
-
)
|
1887
|
-
end
|
1888
|
-
return unless (@delegate_object[exception_sym]).positive?
|
1889
|
-
|
1890
|
-
exit @delegate_object[exception_sym]
|
1891
|
-
end
|
1892
|
-
|
1893
|
-
def save_to_file(required_lines:, selected:)
|
1894
|
-
write_command_file(required_lines: required_lines, selected: selected)
|
1895
|
-
@fout.fout "File saved: #{@run_state.saved_filespec}"
|
1896
|
-
end
|
1897
|
-
|
1898
|
-
def block_state_for_name_from_cli(block_name)
|
1899
|
-
SelectedBlockMenuState.new(
|
1900
|
-
@dml_blocks_in_file.find do |item|
|
1901
|
-
item[:oname] == block_name
|
1902
|
-
end&.merge(
|
1903
|
-
block_name_from_cli: true,
|
1904
|
-
block_name_from_ui: false
|
1905
|
-
),
|
1906
|
-
MenuState::CONTINUE
|
1907
|
-
)
|
1908
|
-
end
|
1909
|
-
|
1910
|
-
# Select and execute a code block from a Markdown document.
|
1911
|
-
#
|
1912
|
-
# This method allows the user to interactively select a code block from a
|
1913
|
-
# Markdown document, obtain approval, and execute the chosen block of code.
|
1914
|
-
#
|
1915
|
-
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1916
|
-
def document_menu_loop
|
1917
|
-
@menu_base_options = @delegate_object
|
1918
|
-
@dml_link_state = LinkState.new(
|
1919
|
-
block_name: @delegate_object[:block_name],
|
1920
|
-
document_filename: @delegate_object[:filename]
|
1921
|
-
)
|
1922
|
-
@run_state.block_name_from_cli = @dml_link_state.block_name.present?
|
1923
|
-
@cli_block_name = @dml_link_state.block_name
|
1924
|
-
@dml_now_using_cli = @run_state.block_name_from_cli
|
1925
|
-
@dml_menu_default_dname = nil
|
1926
|
-
@dml_block_state = SelectedBlockMenuState.new
|
1927
|
-
|
1928
|
-
@run_state.batch_random = Random.new.rand
|
1929
|
-
@run_state.batch_index = 0
|
1930
|
-
|
1931
|
-
InputSequencer.new(
|
1932
|
-
@delegate_object[:filename],
|
1933
|
-
@delegate_object[:input_cli_rest]
|
1934
|
-
).run do |msg, data|
|
1935
|
-
case msg
|
1936
|
-
when :parse_document # once for each menu
|
1937
|
-
# puts "@ - parse document #{data}"
|
1938
|
-
ii_parse_document(data)
|
1939
|
-
|
1940
|
-
when :display_menu
|
1941
|
-
# warn "@ - display menu:"
|
1942
|
-
# ii_display_menu
|
1943
|
-
@dml_block_state = SelectedBlockMenuState.new
|
1944
|
-
@delegate_object[:block_name] = nil
|
1945
|
-
|
1946
|
-
when :user_choice
|
1947
|
-
if @dml_link_state.block_name.present?
|
1948
|
-
# @prior_block_was_link = true
|
1949
|
-
@dml_block_state.block = @dml_blocks_in_file.find do |item|
|
1950
|
-
item[:oname] == @dml_link_state.block_name
|
1951
|
-
end
|
1952
|
-
@dml_link_state.block_name = nil
|
1953
|
-
else
|
1954
|
-
# puts "? - Select a block to execute (or type #{$texit} to exit):"
|
1955
|
-
break if ii_user_choice == :break # into @dml_block_state
|
1956
|
-
break if @dml_block_state.block.nil? # no block matched
|
1957
|
-
end
|
1958
|
-
# puts "! - Executing block: #{data}"
|
1959
|
-
# @dml_block_state.block[:oname]
|
1960
|
-
@dml_block_state.block&.fetch(:oname, nil)
|
1961
|
-
|
1962
|
-
when :execute_block
|
1963
|
-
block_name = data
|
1964
|
-
if block_name == '* Back' ####
|
1965
|
-
debounce_reset
|
1966
|
-
@menu_user_clicked_back_link = true
|
1967
|
-
load_file_link_state = pop_link_history_and_trigger_load
|
1968
|
-
@dml_link_state = load_file_link_state.link_state
|
1969
|
-
|
1970
|
-
InputSequencer.merge_link_state(
|
1971
|
-
@dml_link_state,
|
1972
|
-
InputSequencer.next_link_state(
|
1973
|
-
block_name: @dml_link_state.block_name,
|
1974
|
-
document_filename: @dml_link_state.document_filename,
|
1975
|
-
prior_block_was_link: true
|
1976
|
-
)
|
1977
|
-
)
|
1978
|
-
|
1979
|
-
else
|
1980
|
-
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1981
|
-
if @dml_block_state.block[:shell] == BlockType::OPTS
|
1982
|
-
debounce_reset
|
1983
|
-
link_state = LinkState.new
|
1984
|
-
options_state = read_show_options_and_trigger_reuse(
|
1985
|
-
selected: @dml_block_state.block,
|
1986
|
-
link_state: link_state
|
1987
|
-
)
|
1988
|
-
|
1989
|
-
@menu_base_options.merge!(options_state.options)
|
1990
|
-
@delegate_object.merge!(options_state.options)
|
1991
|
-
options_state.load_file_link_state.link_state
|
1992
|
-
else
|
1993
|
-
ii_execute_block(block_name)
|
1994
|
-
|
1995
|
-
if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
|
1996
|
-
selected: @dml_block_state.block)
|
1997
|
-
return :break
|
1998
|
-
end
|
1999
|
-
|
2000
|
-
## order of block name processing: link block, cli, from user
|
2001
|
-
#
|
2002
|
-
@dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
|
2003
|
-
HashDelegator.next_link_state(
|
2004
|
-
block_name: @dml_link_state.block_name,
|
2005
|
-
block_name_from_cli: !@dml_link_state.block_name.present?,
|
2006
|
-
block_state: @dml_block_state,
|
2007
|
-
was_using_cli: @dml_now_using_cli
|
2008
|
-
)
|
2009
|
-
|
2010
|
-
if !@dml_block_state.block[:block_name_from_ui] && cli_break
|
2011
|
-
# &bsp '!block_name_from_ui + cli_break -> break'
|
2012
|
-
return :break
|
2013
|
-
end
|
2014
|
-
|
2015
|
-
InputSequencer.next_link_state(
|
2016
|
-
block_name: @dml_link_state.block_name,
|
2017
|
-
prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
|
2018
|
-
)
|
2019
|
-
end
|
2020
|
-
end
|
2021
|
-
|
2022
|
-
when :exit?
|
2023
|
-
data == $texit
|
2024
|
-
when :stay?
|
2025
|
-
data == $stay
|
2026
|
-
else
|
2027
|
-
raise "Invalid message: #{msg}"
|
2028
|
-
end
|
2029
|
-
end
|
2030
|
-
rescue StandardError
|
2031
|
-
HashDelegator.error_handler('document_menu_loop',
|
2032
|
-
{ abort: true })
|
2033
|
-
end
|
2034
|
-
|
2035
|
-
def ii_parse_document(_document_filename)
|
2036
|
-
@run_state.batch_index += 1
|
2037
|
-
@run_state.in_own_window = false
|
2038
|
-
|
2039
|
-
# &bsp 'loop', block_name_from_cli, @cli_block_name
|
2040
|
-
@run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
|
2041
|
-
set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
|
2042
|
-
now_using_cli: @dml_now_using_cli,
|
2043
|
-
link_state: @dml_link_state)
|
2044
|
-
end
|
2045
|
-
|
2046
|
-
def ii_user_choice
|
2047
|
-
@dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
|
2048
|
-
menu_blocks: @dml_menu_blocks,
|
2049
|
-
default: @dml_menu_default_dname)
|
2050
|
-
# &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
|
2051
|
-
if !@dml_block_state
|
2052
|
-
HashDelegator.error_handler('block_state missing', { abort: true })
|
2053
|
-
elsif @dml_block_state.state == MenuState::EXIT
|
2054
|
-
# &bsp 'load_cli_or_user_selected_block -> break'
|
2055
|
-
:break
|
2056
|
-
end
|
2057
|
-
end
|
2058
|
-
|
2059
|
-
def ii_execute_block(block_name)
|
2060
|
-
@dml_block_state = block_state_for_name_from_cli(block_name)
|
2061
|
-
|
2062
|
-
dump_and_warn_block_state(selected: @dml_block_state.block)
|
2063
|
-
@dml_link_state, @dml_menu_default_dname = \
|
2064
|
-
exec_bash_next_state(
|
2065
|
-
selected: @dml_block_state.block,
|
2066
|
-
mdoc: @dml_mdoc,
|
2067
|
-
link_state: @dml_link_state,
|
2068
|
-
block_source: {
|
2069
|
-
document_filename: @delegate_object[:filename],
|
2070
|
-
time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
|
2071
|
-
}
|
2072
|
-
)
|
2073
|
-
end
|
2074
|
-
|
2075
|
-
def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
|
2076
|
-
lfls = execute_shell_type(
|
2077
|
-
selected: selected,
|
2078
|
-
mdoc: mdoc,
|
2079
|
-
link_state: link_state,
|
2080
|
-
block_source: block_source
|
2081
|
-
)
|
2082
|
-
|
2083
|
-
# if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
|
2084
|
-
[lfls.link_state,
|
2085
|
-
lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
|
2086
|
-
end
|
2421
|
+
end
|
2087
2422
|
|
2088
|
-
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
block_name_from_cli: block_name_from_cli)
|
2423
|
+
## append blocks loaded, apply LinkKeys::EVAL
|
2424
|
+
#
|
2425
|
+
if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
|
2426
|
+
load_filespec = load_filespec_from_expression(load_expr)
|
2427
|
+
code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
|
2428
|
+
end
|
2095
2429
|
|
2096
|
-
#
|
2430
|
+
# if an eval link block, evaluate code_lines and return its standard output
|
2097
2431
|
#
|
2098
|
-
|
2099
|
-
|
2432
|
+
if link_block_data.fetch(LinkKeys::EVAL,
|
2433
|
+
false) || link_block_data.fetch(LinkKeys::EXEC, false)
|
2434
|
+
code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
|
2435
|
+
end
|
2100
2436
|
|
2101
|
-
|
2437
|
+
next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
|
2438
|
+
|
2439
|
+
if link_block_data[LinkKeys::RETURN]
|
2440
|
+
pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
|
2441
|
+
dependencies, selected)
|
2442
|
+
|
2443
|
+
else
|
2444
|
+
link_history_push_and_next(
|
2445
|
+
curr_block_name: selected.pub_name,
|
2446
|
+
curr_document_filename: @delegate_object[:filename],
|
2447
|
+
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
2448
|
+
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
2449
|
+
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
2450
|
+
next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK,
|
2451
|
+
nil) || link_block_data[LinkKeys::BLOCK] || '',
|
2452
|
+
next_document_filename: next_document_filename,
|
2453
|
+
next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
|
2454
|
+
)
|
2455
|
+
end
|
2102
2456
|
end
|
2103
2457
|
|
2104
|
-
#
|
2105
|
-
#
|
2106
|
-
def
|
2107
|
-
|
2108
|
-
|
2109
|
-
|
2110
|
-
|
2458
|
+
# Handle expression with wildcard characters
|
2459
|
+
# allow user to select or enter
|
2460
|
+
def puts_gets_oprompt_(filespec)
|
2461
|
+
puts format(@delegate_object[:prompt_show_expr_format],
|
2462
|
+
{ expr: filespec })
|
2463
|
+
puts @delegate_object[:prompt_enter_filespec]
|
2464
|
+
gets.chomp
|
2111
2465
|
end
|
2112
2466
|
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2467
|
+
# Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
|
2468
|
+
# @param selected [Hash] Selected item from the menu containing a YAML body.
|
2469
|
+
# @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
|
2470
|
+
# @return [LoadFileLinkState] An instance indicating the next action for loading files.
|
2471
|
+
def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
|
2472
|
+
obj = {}
|
2473
|
+
data = YAML.load(selected[:body].join("\n"))
|
2474
|
+
(data || []).each do |key, value|
|
2475
|
+
sym_key = key.to_sym
|
2476
|
+
obj[sym_key] = value
|
2477
|
+
|
2478
|
+
print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
|
2122
2479
|
end
|
2123
2480
|
|
2124
|
-
|
2125
|
-
|
2126
|
-
|
2481
|
+
link_state.block_name = nil
|
2482
|
+
OpenStruct.new(options: obj,
|
2483
|
+
load_file_link_state: LoadFileLinkState.new(
|
2484
|
+
LoadFile::REUSE, link_state
|
2485
|
+
))
|
2127
2486
|
end
|
2128
2487
|
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
|
2135
|
-
|
2136
|
-
|
2137
|
-
@delegate_object[:filename] = link_state.document_filename
|
2138
|
-
link_state.block_name = @delegate_object[:block_name] =
|
2139
|
-
block_name_from_cli ? @cli_block_name : link_state.block_name
|
2488
|
+
def register_console_attributes(opts)
|
2489
|
+
unless opts[:console_width]
|
2490
|
+
require 'io/console'
|
2491
|
+
opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize
|
2492
|
+
end
|
2493
|
+
opts[:per_page] = opts[:select_page_height] = [opts[:console_height] - 3, 4].max unless opts[:select_page_height]&.positive?
|
2494
|
+
rescue StandardError
|
2495
|
+
HashDelegator.error_handler('register_console_attributes', { abort: true })
|
2140
2496
|
end
|
2141
2497
|
|
2142
|
-
#
|
2143
|
-
#
|
2144
|
-
# @param
|
2145
|
-
# @
|
2146
|
-
|
2147
|
-
|
2148
|
-
|
2149
|
-
|
2150
|
-
|
2151
|
-
|
2152
|
-
|
2153
|
-
|
2498
|
+
# Check if the delegate object responds to a given method.
|
2499
|
+
# @param method_name [Symbol] The name of the method to check.
|
2500
|
+
# @param include_private [Boolean] Whether to include private methods in the check.
|
2501
|
+
# @return [Boolean] true if the delegate object responds to the method, false otherwise.
|
2502
|
+
def respond_to?(method_name, include_private = false)
|
2503
|
+
if super
|
2504
|
+
true
|
2505
|
+
elsif @delegate_object.respond_to?(method_name, include_private)
|
2506
|
+
true
|
2507
|
+
elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
|
2508
|
+
true
|
2509
|
+
else
|
2510
|
+
@delegate_object.respond_to?(method_name, include_private)
|
2154
2511
|
end
|
2512
|
+
end
|
2155
2513
|
|
2156
|
-
|
2157
|
-
|
2158
|
-
|
2514
|
+
def runtime_exception(exception_sym, name, items)
|
2515
|
+
if @delegate_object[exception_sym] != 0
|
2516
|
+
data = { name: name, detail: items.join(', ') }
|
2517
|
+
warn(
|
2518
|
+
format(
|
2519
|
+
@delegate_object.fetch(:exception_format_name, "\n%{name}"),
|
2520
|
+
data
|
2521
|
+
).send(@delegate_object.fetch(:exception_color_name, :red)) +
|
2522
|
+
format(
|
2523
|
+
@delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
|
2524
|
+
data
|
2525
|
+
).send(@delegate_object.fetch(:exception_color_detail, :yellow))
|
2526
|
+
)
|
2159
2527
|
end
|
2528
|
+
return unless (@delegate_object[exception_sym]).positive?
|
2160
2529
|
|
2161
|
-
|
2162
|
-
warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
|
2163
|
-
return unless @delegate_object[:dump_inherited_lines]
|
2164
|
-
|
2165
|
-
warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
|
2530
|
+
exit @delegate_object[exception_sym]
|
2166
2531
|
end
|
2167
2532
|
|
2168
|
-
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2533
|
+
# allow user to select or enter
|
2534
|
+
def save_filespec_from_expression(expression)
|
2535
|
+
# Process expression with embedded formatting
|
2536
|
+
formatted = formatted_expression(expression)
|
2537
|
+
|
2538
|
+
# Handle wildcards or direct file specification
|
2539
|
+
if contains_wildcards?(formatted)
|
2540
|
+
save_filespec_wildcard_expansion(formatted)
|
2541
|
+
else
|
2542
|
+
formatted
|
2172
2543
|
end
|
2544
|
+
end
|
2173
2545
|
|
2174
|
-
|
2546
|
+
# Handle expression with wildcard characters
|
2547
|
+
# allow user to select or enter
|
2548
|
+
def save_filespec_wildcard_expansion(filespec)
|
2549
|
+
files = find_files(filespec)
|
2550
|
+
case files.count
|
2551
|
+
when 0
|
2552
|
+
prompt_for_filespec_with_wildcard(filespec)
|
2553
|
+
else
|
2554
|
+
## user selects from existing files or other
|
2555
|
+
# input into path with wildcard for easy entry
|
2556
|
+
#
|
2557
|
+
name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
|
2558
|
+
case name
|
2559
|
+
when @delegate_object[:prompt_filespec_back]
|
2560
|
+
# do nothing
|
2561
|
+
when @delegate_object[:prompt_filespec_other]
|
2562
|
+
prompt_for_filespec_with_wildcard(filespec)
|
2563
|
+
else
|
2564
|
+
name
|
2565
|
+
end
|
2566
|
+
end
|
2567
|
+
end
|
2175
2568
|
|
2176
|
-
|
2569
|
+
def save_to_file(required_lines:, selected:)
|
2570
|
+
write_command_file(required_lines: required_lines, selected: selected)
|
2571
|
+
@fout.fout "File saved: #{@run_state.saved_filespec}"
|
2177
2572
|
end
|
2178
2573
|
|
2179
2574
|
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
2180
2575
|
def select_option_with_metadata(prompt_text, names, opts = {})
|
2181
2576
|
## configure to environment
|
2182
2577
|
#
|
2183
|
-
|
2184
|
-
require 'io/console'
|
2185
|
-
opts[:per_page] = opts[:select_page_height] = [IO.console.winsize[0] - 3, 4].max
|
2186
|
-
end
|
2578
|
+
register_console_attributes(opts)
|
2187
2579
|
|
2188
2580
|
# crashes if all menu options are disabled
|
2189
2581
|
selection = @prompt.select(prompt_text,
|
2190
2582
|
names,
|
2191
2583
|
opts.merge(filter: true))
|
2192
|
-
|
2584
|
+
selected_name = names.find do |item|
|
2193
2585
|
if item.instance_of?(Hash)
|
2194
2586
|
item[:dname] == selection
|
2195
2587
|
else
|
2196
2588
|
item == selection
|
2197
2589
|
end
|
2198
2590
|
end
|
2199
|
-
|
2200
|
-
unless
|
2591
|
+
selected_name = { dname: selected_name } if selected_name.instance_of?(String)
|
2592
|
+
unless selected_name
|
2201
2593
|
HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
|
2202
2594
|
exit 1
|
2203
2595
|
end
|
2204
2596
|
|
2205
|
-
|
2597
|
+
selected_name.merge(
|
2206
2598
|
if selection == menu_chrome_colored_option(:menu_option_back_name)
|
2207
2599
|
{ option: selection, shell: BlockType::LINK }
|
2208
2600
|
elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
|
@@ -2217,6 +2609,35 @@ module MarkdownExec
|
|
2217
2609
|
HashDelegator.error_handler('select_option_with_metadata')
|
2218
2610
|
end
|
2219
2611
|
|
2612
|
+
# Update the block name in the link state and delegate object.
|
2613
|
+
#
|
2614
|
+
# This method updates the block name based on whether it was specified
|
2615
|
+
# through the CLI or derived from the link state.
|
2616
|
+
#
|
2617
|
+
# @param link_state [LinkState] The current link state object.
|
2618
|
+
# @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
|
2619
|
+
def set_delob_filename_block_name(link_state:, block_name_from_cli:)
|
2620
|
+
@delegate_object[:filename] = link_state.document_filename
|
2621
|
+
link_state.block_name = @delegate_object[:block_name] =
|
2622
|
+
block_name_from_cli ? @cli_block_name : link_state.block_name
|
2623
|
+
end
|
2624
|
+
|
2625
|
+
def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
|
2626
|
+
block_name_from_cli, now_using_cli = \
|
2627
|
+
manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
|
2628
|
+
now_using_cli: now_using_cli,
|
2629
|
+
link_state: link_state)
|
2630
|
+
set_delob_filename_block_name(link_state: link_state,
|
2631
|
+
block_name_from_cli: block_name_from_cli)
|
2632
|
+
|
2633
|
+
# update @delegate_object and @menu_base_options in auto_load
|
2634
|
+
#
|
2635
|
+
blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
|
2636
|
+
dump_delobj(blocks_in_file, menu_blocks, link_state)
|
2637
|
+
|
2638
|
+
[block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
|
2639
|
+
end
|
2640
|
+
|
2220
2641
|
def set_environment_variables_for_block(selected)
|
2221
2642
|
code_lines = []
|
2222
2643
|
YAML.load(selected[:body].join("\n"))&.each do |key, value|
|
@@ -2352,31 +2773,12 @@ module MarkdownExec
|
|
2352
2773
|
end
|
2353
2774
|
end
|
2354
2775
|
|
2355
|
-
# Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
|
2356
|
-
# @param selected [Hash] Selected item from the menu containing a YAML body.
|
2357
|
-
# @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
|
2358
|
-
# @return [LoadFileLinkState] An instance indicating the next action for loading files.
|
2359
|
-
def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
|
2360
|
-
obj = {}
|
2361
|
-
data = YAML.load(selected[:body].join("\n"))
|
2362
|
-
(data || []).each do |key, value|
|
2363
|
-
sym_key = key.to_sym
|
2364
|
-
obj[sym_key] = value
|
2365
|
-
|
2366
|
-
print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
|
2367
|
-
end
|
2368
|
-
|
2369
|
-
link_state.block_name = nil
|
2370
|
-
OpenStruct.new(options: obj,
|
2371
|
-
load_file_link_state: LoadFileLinkState.new(
|
2372
|
-
LoadFile::Reuse, link_state
|
2373
|
-
))
|
2374
|
-
end
|
2375
|
-
|
2376
2776
|
def wait_for_stream_processing
|
2377
2777
|
@process_mutex.synchronize do
|
2378
2778
|
@process_cv.wait(@process_mutex)
|
2379
2779
|
end
|
2780
|
+
rescue Interrupt
|
2781
|
+
# user interrupts process
|
2380
2782
|
end
|
2381
2783
|
|
2382
2784
|
def wait_for_user_selected_block(all_blocks, menu_blocks, default)
|
@@ -2417,7 +2819,7 @@ module MarkdownExec
|
|
2417
2819
|
time_now = Time.now.utc
|
2418
2820
|
@run_state.saved_script_filename =
|
2419
2821
|
SavedAsset.script_name(
|
2420
|
-
blockname: selected
|
2822
|
+
blockname: selected.pub_name,
|
2421
2823
|
filename: @delegate_object[:filename],
|
2422
2824
|
prefix: @delegate_object[:saved_script_filename_prefix],
|
2423
2825
|
time: time_now
|
@@ -2447,14 +2849,34 @@ module MarkdownExec
|
|
2447
2849
|
HashDelegator.error_handler('write_command_file')
|
2448
2850
|
end
|
2449
2851
|
|
2852
|
+
# Ensure the directory exists before writing the file
|
2853
|
+
def write_file_with_directory_creation(save_filespec, content)
|
2854
|
+
directory = File.dirname(save_filespec)
|
2855
|
+
|
2856
|
+
begin
|
2857
|
+
FileUtils.mkdir_p(directory)
|
2858
|
+
File.write(save_filespec, content)
|
2859
|
+
rescue Errno::EACCES
|
2860
|
+
warn "Permission denied: Unable to write to file '#{save_filespec}'"
|
2861
|
+
nil
|
2862
|
+
rescue Errno::EROFS
|
2863
|
+
warn "Read-only file system: Unable to write to file '#{save_filespec}'"
|
2864
|
+
nil
|
2865
|
+
rescue StandardError => err
|
2866
|
+
warn "An error occurred while writing to file '#{save_filespec}': #{err.message}"
|
2867
|
+
nil
|
2868
|
+
end
|
2869
|
+
end
|
2870
|
+
|
2871
|
+
# return next document file name
|
2450
2872
|
def write_inherited_lines_to_file(link_state, link_block_data)
|
2451
|
-
save_expr = link_block_data.fetch(LinkKeys::
|
2873
|
+
save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
|
2452
2874
|
if save_expr.present?
|
2453
2875
|
save_filespec = save_filespec_from_expression(save_expr)
|
2454
2876
|
File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
|
2455
2877
|
@delegate_object[:filename]
|
2456
2878
|
else
|
2457
|
-
link_block_data[LinkKeys::
|
2879
|
+
link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
|
2458
2880
|
end
|
2459
2881
|
end
|
2460
2882
|
end
|
@@ -2595,21 +3017,21 @@ module MarkdownExec
|
|
2595
3017
|
|
2596
3018
|
# Test case for empty body
|
2597
3019
|
def test_push_link_history_and_trigger_load_with_empty_body
|
2598
|
-
assert_equal LoadFile::
|
3020
|
+
assert_equal LoadFile::REUSE,
|
2599
3021
|
@hd.push_link_history_and_trigger_load.load_file
|
2600
3022
|
end
|
2601
3023
|
|
2602
3024
|
# Test case for non-empty body without 'file' key
|
2603
3025
|
def test_push_link_history_and_trigger_load_without_file_key
|
2604
3026
|
body = ["vars:\n KEY: VALUE"]
|
2605
|
-
assert_equal LoadFile::
|
3027
|
+
assert_equal LoadFile::REUSE,
|
2606
3028
|
@hd.push_link_history_and_trigger_load(link_block_body: body).load_file
|
2607
3029
|
end
|
2608
3030
|
|
2609
3031
|
# Test case for non-empty body with 'file' key
|
2610
3032
|
def test_push_link_history_and_trigger_load_with_file_key
|
2611
3033
|
body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
|
2612
|
-
expected_result = LoadFileLinkState.new(LoadFile::
|
3034
|
+
expected_result = LoadFileLinkState.new(LoadFile::LOAD,
|
2613
3035
|
LinkState.new(block_name: 'sample_block',
|
2614
3036
|
document_filename: 'sample_file',
|
2615
3037
|
inherited_dependencies: {},
|
@@ -2647,8 +3069,9 @@ module MarkdownExec
|
|
2647
3069
|
end
|
2648
3070
|
|
2649
3071
|
def test_safeval_rescue_from_error
|
2650
|
-
|
2651
|
-
|
3072
|
+
assert_raises(SystemExit) do
|
3073
|
+
HashDelegator.safeval('invalid_code_raises_exception')
|
3074
|
+
end
|
2652
3075
|
end
|
2653
3076
|
|
2654
3077
|
def test_set_fcb_title
|
@@ -2755,7 +3178,6 @@ module MarkdownExec
|
|
2755
3178
|
|
2756
3179
|
def test_blocks_from_nested_files
|
2757
3180
|
result = @hd.blocks_from_nested_files
|
2758
|
-
|
2759
3181
|
assert_kind_of Array, result
|
2760
3182
|
assert_kind_of FCB, result.first
|
2761
3183
|
end
|
@@ -3054,7 +3476,7 @@ module MarkdownExec
|
|
3054
3476
|
|
3055
3477
|
# Asserting the result is an instance of LoadFileLinkState
|
3056
3478
|
assert_instance_of LoadFileLinkState, result
|
3057
|
-
assert_equal LoadFile::
|
3479
|
+
assert_equal LoadFile::LOAD, result.load_file
|
3058
3480
|
assert_nil result.link_state.block_name
|
3059
3481
|
end
|
3060
3482
|
end
|
@@ -3441,4 +3863,60 @@ module MarkdownExec
|
|
3441
3863
|
assert_equal expected, BashCommentFormatter.format_comment(input)
|
3442
3864
|
end
|
3443
3865
|
end
|
3866
|
+
|
3867
|
+
class PromptForFilespecWithWildcardTest < Minitest::Test
|
3868
|
+
def setup
|
3869
|
+
@delegate_object = {
|
3870
|
+
prompt_show_expr_format: 'Current expression: %{expr}',
|
3871
|
+
prompt_enter_filespec: 'Please enter a filespec:'
|
3872
|
+
}
|
3873
|
+
@original_stdin = $stdin
|
3874
|
+
end
|
3875
|
+
|
3876
|
+
def teardown
|
3877
|
+
$stdin = @original_stdin
|
3878
|
+
end
|
3879
|
+
|
3880
|
+
def test_prompt_for_filespec_with_normal_input
|
3881
|
+
$stdin = StringIO.new("test_input\n")
|
3882
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3883
|
+
assert_equal 'resolved_path_or_substituted_value', result
|
3884
|
+
end
|
3885
|
+
|
3886
|
+
def test_prompt_for_filespec_with_interruption
|
3887
|
+
$stdin = StringIO.new
|
3888
|
+
# rubocop disable:Lint/NestedMethodDefinition
|
3889
|
+
def $stdin.gets; raise Interrupt; end
|
3890
|
+
# rubocop enable:Lint/NestedMethodDefinition
|
3891
|
+
|
3892
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3893
|
+
assert_nil result
|
3894
|
+
end
|
3895
|
+
|
3896
|
+
def test_prompt_for_filespec_with_empty_input
|
3897
|
+
$stdin = StringIO.new("\n")
|
3898
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3899
|
+
assert_equal 'resolved_path_or_substituted_value', result
|
3900
|
+
end
|
3901
|
+
|
3902
|
+
private
|
3903
|
+
|
3904
|
+
def prompt_for_filespec_with_wildcard(filespec)
|
3905
|
+
puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
|
3906
|
+
puts @delegate_object[:prompt_enter_filespec]
|
3907
|
+
|
3908
|
+
begin
|
3909
|
+
input = gets.chomp
|
3910
|
+
PathUtils.resolve_path_or_substitute(input, filespec)
|
3911
|
+
rescue Interrupt
|
3912
|
+
nil
|
3913
|
+
end
|
3914
|
+
end
|
3915
|
+
|
3916
|
+
module PathUtils
|
3917
|
+
def self.resolve_path_or_substitute(input, filespec)
|
3918
|
+
'resolved_path_or_substituted_value' # Placeholder implementation
|
3919
|
+
end
|
3920
|
+
end
|
3921
|
+
end
|
3444
3922
|
end # module MarkdownExec
|