markdown_exec 2.0.6 → 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 +10 -2
- data/CHANGELOG.md +27 -0
- data/Gemfile.lock +1 -1
- data/bin/tab_completion.sh +2 -2
- data/examples/interrupt.md +9 -0
- data/examples/line-wrapping.md +17 -0
- data/examples/nickname.md +46 -9
- data/examples/pause-after-execution.md +9 -0
- data/lib/colorize.rb +25 -0
- data/lib/constants.rb +1 -0
- data/lib/fcb.rb +15 -0
- data/lib/hash_delegator.rb +365 -100
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +1 -0
- data/lib/mdoc.rb +9 -14
- data/lib/menu.src.yml +38 -13
- data/lib/menu.yml +26 -14
- data/lib/saved_assets.rb +4 -3
- metadata +5 -2
data/lib/hash_delegator.rb
CHANGED
@@ -237,6 +237,22 @@ module HashDelegatorSelf
|
|
237
237
|
# @param str [String] The string to be evaluated.
|
238
238
|
# @return [Object] The result of evaluating the string.
|
239
239
|
def safeval(str)
|
240
|
+
# # Restricting to evaluate only expressions
|
241
|
+
# unless str.match?(/\A\s*\w+\s*[\+\-\*\/\=\%\&\|\<\>\!]+\s*\w+\s*\z/)
|
242
|
+
# error_handler('safeval') # 'Invalid expression'
|
243
|
+
# return
|
244
|
+
# end
|
245
|
+
|
246
|
+
# # Whitelisting allowed operations
|
247
|
+
# allowed_methods = %w[+ - * / == != < > <= >= && || % & |]
|
248
|
+
# unless allowed_methods.any? { |op| str.include?(op) }
|
249
|
+
# error_handler('safeval', 'Operation not allowed')
|
250
|
+
# return
|
251
|
+
# end
|
252
|
+
|
253
|
+
# # Sanitize input (example: removing potentially harmful characters)
|
254
|
+
# str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
|
255
|
+
# Evaluate the sanitized string
|
240
256
|
result = nil
|
241
257
|
binding.eval("result = #{str}")
|
242
258
|
|
@@ -248,6 +264,16 @@ module HashDelegatorSelf
|
|
248
264
|
exit 1
|
249
265
|
end
|
250
266
|
|
267
|
+
# # Evaluates the given string as Ruby code and rescues any StandardErrors.
|
268
|
+
# # If an error occurs, it calls the error_handler method with 'safeval'.
|
269
|
+
# # @param str [String] The string to be evaluated.
|
270
|
+
# # @return [Object] The result of evaluating the string.
|
271
|
+
# def safeval(str)
|
272
|
+
# eval(str)
|
273
|
+
# rescue StandardError # catches NameError, StandardError
|
274
|
+
# error_handler('safeval')
|
275
|
+
# end
|
276
|
+
|
251
277
|
def set_file_permissions(file_path, chmod_value)
|
252
278
|
File.chmod(chmod_value, file_path)
|
253
279
|
end
|
@@ -388,6 +414,74 @@ class BashCommentFormatter
|
|
388
414
|
# end
|
389
415
|
end
|
390
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
|
+
|
391
485
|
module MarkdownExec
|
392
486
|
class DebugHelper
|
393
487
|
# Class-level variable to store history of printed messages
|
@@ -497,6 +591,9 @@ module MarkdownExec
|
|
497
591
|
when MenuState::SAVE
|
498
592
|
option_name = @delegate_object[:menu_option_save_name]
|
499
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]
|
500
597
|
when MenuState::VIEW
|
501
598
|
option_name = @delegate_object[:menu_option_view_name]
|
502
599
|
insert_at_top = @delegate_object[:menu_load_at_top]
|
@@ -595,6 +692,8 @@ module MarkdownExec
|
|
595
692
|
#
|
596
693
|
# @return [Array<FCB>] An array of FCB objects representing the blocks.
|
597
694
|
def blocks_from_nested_files
|
695
|
+
register_console_attributes(@delegate_object)
|
696
|
+
|
598
697
|
blocks = []
|
599
698
|
iter_blocks_from_nested_files do |btype, fcb|
|
600
699
|
process_block_based_on_type(blocks, btype, fcb)
|
@@ -605,10 +704,12 @@ module MarkdownExec
|
|
605
704
|
HashDelegator.error_handler('blocks_from_nested_files')
|
606
705
|
end
|
607
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
|
608
709
|
def block_state_for_name_from_cli(block_name)
|
609
710
|
SelectedBlockMenuState.new(
|
610
711
|
@dml_blocks_in_file.find do |item|
|
611
|
-
|
712
|
+
block_name == item.pub_name
|
612
713
|
end&.merge(
|
613
714
|
block_name_from_cli: true,
|
614
715
|
block_name_from_ui: false
|
@@ -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::STD_OUT) do |line|
|
729
|
-
yield nil, line, nil, exec_thr if block_given?
|
730
|
-
end
|
731
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) 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::STD_IN) 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
|
@@ -761,6 +830,24 @@ module MarkdownExec
|
|
761
830
|
@fout.fout "Error ENOENT: #{err.inspect}"
|
762
831
|
end
|
763
832
|
|
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
|
+
}
|
849
|
+
end
|
850
|
+
|
764
851
|
# This method is responsible for handling the execution of generic blocks in a markdown document.
|
765
852
|
# It collects the required code lines from the document and, depending on the configuration,
|
766
853
|
# may display the code for user approval before execution. It then executes the approved block.
|
@@ -816,16 +903,70 @@ module MarkdownExec
|
|
816
903
|
# @param match_data [MatchData] The match data containing named captures for formatting.
|
817
904
|
# @param format_option [String] The format string to be used for the new block.
|
818
905
|
# @param color_method [Symbol] The color method to apply to the block's display name.
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
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
|
829
970
|
end
|
830
971
|
|
831
972
|
##
|
@@ -836,12 +977,12 @@ module MarkdownExec
|
|
836
977
|
# @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
|
837
978
|
def create_and_add_chrome_blocks(blocks, fcb)
|
838
979
|
match_criteria = [
|
839
|
-
{ color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match },
|
840
|
-
{ color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match },
|
841
|
-
{ 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 },
|
842
983
|
{ color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match },
|
843
|
-
{ color: :menu_note_color, format: :menu_note_format, match: :menu_note_match },
|
844
|
-
{ 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 }
|
845
986
|
]
|
846
987
|
# rubocop:enable Style/UnlessElse
|
847
988
|
match_criteria.each do |criteria|
|
@@ -852,9 +993,12 @@ module MarkdownExec
|
|
852
993
|
|
853
994
|
create_and_add_chrome_block(
|
854
995
|
blocks: blocks,
|
855
|
-
|
996
|
+
case_conversion: criteria[:case_conversion],
|
997
|
+
center: criteria[:center],
|
998
|
+
color_method: @delegate_object[criteria[:color]].to_sym,
|
856
999
|
format_option: @delegate_object[criteria[:format]],
|
857
|
-
|
1000
|
+
match_data: mbody,
|
1001
|
+
wrap: criteria[:wrap]
|
858
1002
|
)
|
859
1003
|
break
|
860
1004
|
end
|
@@ -950,6 +1094,7 @@ module MarkdownExec
|
|
950
1094
|
block_name: @delegate_object[:block_name],
|
951
1095
|
document_filename: @delegate_object[:filename]
|
952
1096
|
)
|
1097
|
+
# @dml_link_state_block_name_from_cli = @dml_link_state.block_name.present? ###
|
953
1098
|
@run_state.block_name_from_cli = @dml_link_state.block_name.present?
|
954
1099
|
@cli_block_name = @dml_link_state.block_name
|
955
1100
|
@dml_now_using_cli = @run_state.block_name_from_cli
|
@@ -975,6 +1120,7 @@ module MarkdownExec
|
|
975
1120
|
item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
|
976
1121
|
item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
|
977
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]))
|
978
1124
|
item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
|
979
1125
|
|
980
1126
|
@run_state.batch_random = Random.new.rand
|
@@ -999,10 +1145,11 @@ module MarkdownExec
|
|
999
1145
|
|
1000
1146
|
# add menu items (glob, load, save) and enable selectively
|
1001
1147
|
menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
|
1002
|
-
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD)
|
1003
|
-
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT)
|
1004
|
-
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])),
|
1005
|
-
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])),
|
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]
|
1006
1153
|
end
|
1007
1154
|
|
1008
1155
|
when :display_menu
|
@@ -1015,7 +1162,7 @@ module MarkdownExec
|
|
1015
1162
|
if @dml_link_state.block_name.present?
|
1016
1163
|
# @prior_block_was_link = true
|
1017
1164
|
@dml_block_state.block = @dml_blocks_in_file.find do |item|
|
1018
|
-
item
|
1165
|
+
item.pub_name == @dml_link_state.block_name
|
1019
1166
|
end
|
1020
1167
|
@dml_link_state.block_name = nil
|
1021
1168
|
else
|
@@ -1024,8 +1171,7 @@ module MarkdownExec
|
|
1024
1171
|
break if @dml_block_state.block.nil? # no block matched
|
1025
1172
|
end
|
1026
1173
|
# puts "! - Executing block: #{data}"
|
1027
|
-
|
1028
|
-
@dml_block_state.block&.fetch(:oname, nil)
|
1174
|
+
@dml_block_state.block&.pub_name
|
1029
1175
|
|
1030
1176
|
when :execute_block
|
1031
1177
|
case (block_name = data)
|
@@ -1045,6 +1191,7 @@ module MarkdownExec
|
|
1045
1191
|
)
|
1046
1192
|
|
1047
1193
|
when item_edit
|
1194
|
+
debounce_reset
|
1048
1195
|
edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
|
1049
1196
|
@dml_link_state.inherited_lines = edited.split("\n") if edited
|
1050
1197
|
InputSequencer.next_link_state(prior_block_was_link: true)
|
@@ -1070,10 +1217,28 @@ module MarkdownExec
|
|
1070
1217
|
return :break
|
1071
1218
|
|
1072
1219
|
end
|
1220
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1073
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
|
1074
1238
|
InputSequencer.next_link_state(prior_block_was_link: true)
|
1075
1239
|
|
1076
1240
|
when item_view
|
1241
|
+
debounce_reset
|
1077
1242
|
warn @dml_link_state.inherited_lines.join("\n")
|
1078
1243
|
InputSequencer.next_link_state(prior_block_was_link: true)
|
1079
1244
|
|
@@ -1103,7 +1268,7 @@ module MarkdownExec
|
|
1103
1268
|
@dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
|
1104
1269
|
HashDelegator.next_link_state(
|
1105
1270
|
block_name: @dml_link_state.block_name,
|
1106
|
-
block_name_from_cli:
|
1271
|
+
block_name_from_cli: @dml_now_using_cli,
|
1107
1272
|
block_state: @dml_block_state,
|
1108
1273
|
was_using_cli: @dml_now_using_cli
|
1109
1274
|
)
|
@@ -1244,6 +1409,53 @@ module MarkdownExec
|
|
1244
1409
|
#.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
|
1245
1410
|
end
|
1246
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
|
+
|
1247
1459
|
# Executes a block of code that has been approved for execution.
|
1248
1460
|
# It sets the script block name, writes command files if required, and handles the execution
|
1249
1461
|
# including output formatting and summarization.
|
@@ -1294,7 +1506,7 @@ module MarkdownExec
|
|
1294
1506
|
### options_state.load_file_link_state
|
1295
1507
|
link_state = LinkState.new
|
1296
1508
|
link_history_push_and_next(
|
1297
|
-
curr_block_name: selected
|
1509
|
+
curr_block_name: selected.pub_name,
|
1298
1510
|
curr_document_filename: @delegate_object[:filename],
|
1299
1511
|
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1300
1512
|
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
@@ -1310,7 +1522,7 @@ module MarkdownExec
|
|
1310
1522
|
code_lines = set_environment_variables_for_block(selected)
|
1311
1523
|
dependencies = {}
|
1312
1524
|
link_history_push_and_next(
|
1313
|
-
curr_block_name: selected
|
1525
|
+
curr_block_name: selected.pub_name,
|
1314
1526
|
curr_document_filename: @delegate_object[:filename],
|
1315
1527
|
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1316
1528
|
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
@@ -1437,7 +1649,7 @@ module MarkdownExec
|
|
1437
1649
|
return
|
1438
1650
|
end
|
1439
1651
|
|
1440
|
-
@delegate_object[:block_name] = block_state.block
|
1652
|
+
@delegate_object[:block_name] = block_state.block.pub_name
|
1441
1653
|
@menu_user_clicked_back_link = block_state.state == MenuState::BACK
|
1442
1654
|
end
|
1443
1655
|
|
@@ -1446,7 +1658,7 @@ module MarkdownExec
|
|
1446
1658
|
Thread.new do
|
1447
1659
|
stream.each_line do |line|
|
1448
1660
|
line.strip!
|
1449
|
-
@run_state.files[file_type] << line
|
1661
|
+
@run_state.files[file_type] << line if @run_state.files
|
1450
1662
|
|
1451
1663
|
if @delegate_object[:output_stdout]
|
1452
1664
|
# print line
|
@@ -1546,23 +1758,7 @@ module MarkdownExec
|
|
1546
1758
|
|
1547
1759
|
if link_block_data.fetch(LinkKeys::EXEC, false)
|
1548
1760
|
@run_state.files = Hash.new([])
|
1549
|
-
|
1550
|
-
Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
|
1551
|
-
handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
|
1552
|
-
output_lines.push(line)
|
1553
|
-
end
|
1554
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
|
1555
|
-
output_lines.push(line)
|
1556
|
-
end
|
1557
|
-
|
1558
|
-
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
|
1559
|
-
stdin.puts(line)
|
1560
|
-
end
|
1561
|
-
|
1562
|
-
wait_for_stream_processing
|
1563
|
-
sleep 0.1
|
1564
|
-
in_thr.kill if in_thr&.alive?
|
1565
|
-
end
|
1761
|
+
execute_command_with_streams([cmd])
|
1566
1762
|
|
1567
1763
|
## select output_lines that look like assignment or match other specs
|
1568
1764
|
#
|
@@ -1585,13 +1781,13 @@ module MarkdownExec
|
|
1585
1781
|
label_format_below = @delegate_object[:shell_code_label_format_below]
|
1586
1782
|
|
1587
1783
|
[label_format_above && format(label_format_above,
|
1588
|
-
block_source.merge({ block_name: selected
|
1784
|
+
block_source.merge({ block_name: selected.pub_name }))] +
|
1589
1785
|
output_lines.map do |line|
|
1590
1786
|
re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
|
1591
1787
|
re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line
|
1592
1788
|
end.compact +
|
1593
1789
|
[label_format_below && format(label_format_below,
|
1594
|
-
block_source.merge({ block_name: selected
|
1790
|
+
block_source.merge({ block_name: selected.pub_name }))]
|
1595
1791
|
end
|
1596
1792
|
|
1597
1793
|
def link_history_push_and_next(
|
@@ -1655,7 +1851,7 @@ module MarkdownExec
|
|
1655
1851
|
def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
|
1656
1852
|
if @delegate_object[:block_name].present?
|
1657
1853
|
block = all_blocks.find do |item|
|
1658
|
-
item
|
1854
|
+
item.pub_name == @delegate_object[:block_name]
|
1659
1855
|
end&.merge(block_name_from_ui: false)
|
1660
1856
|
else
|
1661
1857
|
block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
|
@@ -1682,6 +1878,44 @@ module MarkdownExec
|
|
1682
1878
|
expanded_expression
|
1683
1879
|
end
|
1684
1880
|
end
|
1881
|
+
|
1882
|
+
# private
|
1883
|
+
|
1884
|
+
# def read_block_name(line)
|
1885
|
+
# bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
|
1886
|
+
# name = bm[:title]
|
1887
|
+
|
1888
|
+
# if @delegate_object[:block_name_nick_match].present? && line =~ Regexp.new(@delegate_object[:block_name_nick_match])
|
1889
|
+
# name = $~[0]
|
1890
|
+
# else
|
1891
|
+
# name = bm && bm[1] ? bm[:title] : name
|
1892
|
+
# end
|
1893
|
+
# name
|
1894
|
+
# end
|
1895
|
+
|
1896
|
+
# # Loads auto link block.
|
1897
|
+
# def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
|
1898
|
+
# block_name = @delegate_object[:document_load_link_block_name]
|
1899
|
+
# return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
|
1900
|
+
|
1901
|
+
# block = HashDelegator.block_find(all_blocks, :oname, block_name)
|
1902
|
+
# return unless block
|
1903
|
+
|
1904
|
+
# if block.fetch(:shell, '') != BlockType::LINK
|
1905
|
+
# HashDelegator.error_handler('must be Link block type', { abort: true })
|
1906
|
+
|
1907
|
+
# else
|
1908
|
+
# # debounce_reset
|
1909
|
+
# push_link_history_and_trigger_load(
|
1910
|
+
# link_block_body: block.fetch(:body, ''),
|
1911
|
+
# mdoc: mdoc,
|
1912
|
+
# selected: block,
|
1913
|
+
# link_state: link_state,
|
1914
|
+
# block_source: block_source
|
1915
|
+
# )
|
1916
|
+
# end
|
1917
|
+
# end
|
1918
|
+
|
1685
1919
|
# Handle expression with wildcard characters
|
1686
1920
|
def load_filespec_wildcard_expansion(expr, auto_load_single: false)
|
1687
1921
|
files = find_files(expr)
|
@@ -1717,6 +1951,7 @@ module MarkdownExec
|
|
1717
1951
|
# recreate menu with new options
|
1718
1952
|
#
|
1719
1953
|
all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
|
1954
|
+
# load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
|
1720
1955
|
|
1721
1956
|
menu_blocks = mdoc.fcbs_per_options(@delegate_object)
|
1722
1957
|
add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
|
@@ -1793,7 +2028,7 @@ module MarkdownExec
|
|
1793
2028
|
#
|
1794
2029
|
return unless item
|
1795
2030
|
|
1796
|
-
item[:dname] = "#{name} (#{count} #{type})"
|
2031
|
+
item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name
|
1797
2032
|
if count.positive?
|
1798
2033
|
item.delete(:disabled)
|
1799
2034
|
else
|
@@ -1902,7 +2137,7 @@ module MarkdownExec
|
|
1902
2137
|
else
|
1903
2138
|
# no history exists; must have been called independently => retain script
|
1904
2139
|
link_history_push_and_next(
|
1905
|
-
curr_block_name: selected
|
2140
|
+
curr_block_name: selected.pub_name,
|
1906
2141
|
curr_document_filename: @delegate_object[:filename],
|
1907
2142
|
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1908
2143
|
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
@@ -2036,6 +2271,14 @@ module MarkdownExec
|
|
2036
2271
|
exit 1
|
2037
2272
|
end
|
2038
2273
|
|
2274
|
+
def prompt_for_command(prompt)
|
2275
|
+
print prompt
|
2276
|
+
|
2277
|
+
gets.chomp
|
2278
|
+
rescue Interrupt
|
2279
|
+
nil
|
2280
|
+
end
|
2281
|
+
|
2039
2282
|
# Prompts the user to enter a path or name to substitute into the wildcard expression.
|
2040
2283
|
# If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
|
2041
2284
|
#
|
@@ -2132,8 +2375,7 @@ module MarkdownExec
|
|
2132
2375
|
# user prompt to exit if the menu will be displayed again
|
2133
2376
|
#
|
2134
2377
|
def prompt_user_exit(block_name_from_cli:, selected:)
|
2135
|
-
|
2136
|
-
selected[:shell] == BlockType::BASH &&
|
2378
|
+
selected[:shell] == BlockType::BASH &&
|
2137
2379
|
@delegate_object[:pause_after_script_execution] &&
|
2138
2380
|
prompt_select_continue == MenuState::EXIT
|
2139
2381
|
end
|
@@ -2154,7 +2396,7 @@ module MarkdownExec
|
|
2154
2396
|
#
|
2155
2397
|
if mdoc
|
2156
2398
|
code_info = mdoc.collect_recursively_required_code(
|
2157
|
-
anyname: selected
|
2399
|
+
anyname: selected.pub_name,
|
2158
2400
|
label_format_above: @delegate_object[:shell_code_label_format_above],
|
2159
2401
|
label_format_below: @delegate_object[:shell_code_label_format_below],
|
2160
2402
|
block_source: block_source
|
@@ -2171,7 +2413,7 @@ module MarkdownExec
|
|
2171
2413
|
# load key and values from link block into current environment
|
2172
2414
|
#
|
2173
2415
|
if link_block_data[LinkKeys::VARS]
|
2174
|
-
code_lines.push BashCommentFormatter.format_comment(selected
|
2416
|
+
code_lines.push BashCommentFormatter.format_comment(selected.pub_name)
|
2175
2417
|
(link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
|
2176
2418
|
ENV[key] = value.to_s
|
2177
2419
|
code_lines.push(assign_key_value_in_bash(key, value))
|
@@ -2200,7 +2442,7 @@ module MarkdownExec
|
|
2200
2442
|
|
2201
2443
|
else
|
2202
2444
|
link_history_push_and_next(
|
2203
|
-
curr_block_name: selected
|
2445
|
+
curr_block_name: selected.pub_name,
|
2204
2446
|
curr_document_filename: @delegate_object[:filename],
|
2205
2447
|
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
2206
2448
|
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
@@ -2243,6 +2485,16 @@ module MarkdownExec
|
|
2243
2485
|
))
|
2244
2486
|
end
|
2245
2487
|
|
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 })
|
2496
|
+
end
|
2497
|
+
|
2246
2498
|
# Check if the delegate object responds to a given method.
|
2247
2499
|
# @param method_name [Symbol] The name of the method to check.
|
2248
2500
|
# @param include_private [Boolean] Whether to include private methods in the check.
|
@@ -2323,10 +2575,7 @@ module MarkdownExec
|
|
2323
2575
|
def select_option_with_metadata(prompt_text, names, opts = {})
|
2324
2576
|
## configure to environment
|
2325
2577
|
#
|
2326
|
-
|
2327
|
-
require 'io/console'
|
2328
|
-
opts[:per_page] = opts[:select_page_height] = [IO.console.winsize[0] - 3, 4].max
|
2329
|
-
end
|
2578
|
+
register_console_attributes(opts)
|
2330
2579
|
|
2331
2580
|
# crashes if all menu options are disabled
|
2332
2581
|
selection = @prompt.select(prompt_text,
|
@@ -2528,6 +2777,8 @@ module MarkdownExec
|
|
2528
2777
|
@process_mutex.synchronize do
|
2529
2778
|
@process_cv.wait(@process_mutex)
|
2530
2779
|
end
|
2780
|
+
rescue Interrupt
|
2781
|
+
# user interrupts process
|
2531
2782
|
end
|
2532
2783
|
|
2533
2784
|
def wait_for_user_selected_block(all_blocks, menu_blocks, default)
|
@@ -2568,7 +2819,7 @@ module MarkdownExec
|
|
2568
2819
|
time_now = Time.now.utc
|
2569
2820
|
@run_state.saved_script_filename =
|
2570
2821
|
SavedAsset.script_name(
|
2571
|
-
blockname: selected
|
2822
|
+
blockname: selected.pub_name,
|
2572
2823
|
filename: @delegate_object[:filename],
|
2573
2824
|
prefix: @delegate_object[:saved_script_filename_prefix],
|
2574
2825
|
time: time_now
|
@@ -2661,6 +2912,21 @@ module MarkdownExec
|
|
2661
2912
|
|
2662
2913
|
def self.next_link_state(*args, **kwargs, &block)
|
2663
2914
|
super
|
2915
|
+
# result = super
|
2916
|
+
|
2917
|
+
# @logger ||= StdOutErrLogger.new
|
2918
|
+
# @logger.unknown(
|
2919
|
+
# HashDelegator.clean_hash_recursively(
|
2920
|
+
# { "HashDelegator.next_link_state":
|
2921
|
+
# { 'args': args,
|
2922
|
+
# 'at': Time.now.strftime('%FT%TZ'),
|
2923
|
+
# 'for': /[^\/]+:\d+/.match(caller.first)[0],
|
2924
|
+
# 'kwargs': kwargs,
|
2925
|
+
# 'return': result } }
|
2926
|
+
# )
|
2927
|
+
# )
|
2928
|
+
|
2929
|
+
# result
|
2664
2930
|
end
|
2665
2931
|
end
|
2666
2932
|
end
|
@@ -2912,7 +3178,6 @@ module MarkdownExec
|
|
2912
3178
|
|
2913
3179
|
def test_blocks_from_nested_files
|
2914
3180
|
result = @hd.blocks_from_nested_files
|
2915
|
-
|
2916
3181
|
assert_kind_of Array, result
|
2917
3182
|
assert_kind_of FCB, result.first
|
2918
3183
|
end
|