lib/hash_delegator.rb in markdown_exec-2.0.2 vs lib/hash_delegator.rb in markdown_exec-2.0.3

- old
+ new

@@ -9,20 +9,20 @@ require 'open3' require 'optparse' require 'set' require 'shellwords' require 'tmpdir' -# require 'tty-file' require 'tty-prompt' require 'yaml' require_relative 'array' require_relative 'array_util' require_relative 'block_label' require_relative 'block_types' require_relative 'cached_nested_file_reader' require_relative 'constants' +require_relative 'std_out_err_logger' require_relative 'directory_searcher' require_relative 'exceptions' require_relative 'fcb' require_relative 'filter' require_relative 'fout' @@ -191,22 +191,19 @@ merged = args.compact.flatten merged.empty? ? [] : merged end def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, block_name: nil) - # &bsp 'next_link_state', block_name_from_cli, was_using_cli, block_state # Set block_name based on block_name_from_cli block_name = @cli_block_name if block_name_from_cli - # &bsp 'block_name:', block_name # Determine the state of breaker based on was_using_cli and the block type + # 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. breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH - # &bsp 'breaker:', breaker # Reset block_name_from_cli if the conditions are not met block_name_from_cli ||= false - # &bsp 'block_name_from_cli:', block_name_from_cli [block_name, block_name_from_cli, breaker] end def parse_yaml_data_from_body(body) @@ -350,10 +347,24 @@ def value_ineligible?(value) [nil, [], {}, ''].include?(value) end end +module PathUtils + # Determines if a given path is absolute or substitutes a placeholder in an expression with the path. + # @param path [String] The input path to check or fill in. + # @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute. + # @return [String] The absolute path or the expression with the wildcard replaced by the path. + def self.resolve_path_or_substitute(path, expression) + if path.include?('/') + path + else + expression.gsub('*', path) + end + end +end + module MarkdownExec class DebugHelper # Class-level variable to store history of printed messages @@printed_messages = Set.new @@ -366,11 +377,11 @@ warn(*str) @@printed_messages.add(str) end end - class HashDelegator + class HashDelegatorParent attr_accessor :most_recent_loaded_filename, :pass_args, :run_state extend HashDelegatorSelf include CompactionHelpers @@ -557,15 +568,15 @@ HashDelegator.error_handler('blocks_from_nested_files') end # private - def calc_logged_stdout_filename + def calc_logged_stdout_filename(block_name:) return unless @delegate_object[:saved_stdout_folder] @delegate_object[:logged_stdout_filename] = - SavedAsset.stdout_name(blockname: @delegate_object[:block_name], + SavedAsset.stdout_name(blockname: block_name, filename: File.basename(@delegate_object[:filename], '.*'), prefix: @delegate_object[:logged_stdout_filename_prefix], time: Time.now.utc) @@ -896,11 +907,11 @@ # # @param required_lines [Array<String>] The lines of code to be executed. # @param selected [FCB] The selected functional code block object. def execute_required_lines(required_lines: [], selected: FCB.new) write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script] - calc_logged_stdout_filename + calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state format_and_execute_command(code_lines: required_lines) post_execution_process end # Execute a code block after approval and provide user interaction options. @@ -948,11 +959,10 @@ next_block_name: '', next_document_filename: @delegate_object[:filename], next_load_file: LoadFile::Reuse ) - elsif selected[:shell] == BlockType::VARS debounce_reset block_names = [] code_lines = set_environment_variables_for_block(selected) dependencies = {} @@ -970,10 +980,11 @@ elsif debounce_allows compile_execute_and_trigger_reuse(mdoc: mdoc, selected: selected, link_state: link_state, block_source: block_source) + else LoadFileLinkState.new(LoadFile::Reuse, link_state) end end @@ -1130,46 +1141,51 @@ end def link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source:) all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines) - if link_block_data.fetch(LinkKeys::Exec, false) - @run_state.files = Hash.new([]) - output_lines = [] + Tempfile.open do |file| + file.write(all_code.join("\n")) + file.rewind - Open3.popen3( - @delegate_object[:shell], - '-c', all_code.join("\n") - ) do |stdin, stdout, stderr, _exec_thr| - handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line| - output_lines.push(line) - end - handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line| - output_lines.push(line) - end + if link_block_data.fetch(LinkKeys::Exec, false) + @run_state.files = Hash.new([]) + output_lines = [] - in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line| - stdin.puts(line) - end + Open3.popen3( + "#{@delegate_object[:shell]} #{file.path}" + ) do |stdin, stdout, stderr, _exec_thr| + handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line| + output_lines.push(line) + end + handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line| + output_lines.push(line) + end - wait_for_stream_processing - sleep 0.1 - in_thr.kill if in_thr&.alive? - end + in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line| + stdin.puts(line) + end - ## select output_lines that look like assignment or match other specs - # - output_lines = process_string_array( - output_lines, - begin_pattern: @delegate_object.fetch(:output_assignment_begin, nil), - end_pattern: @delegate_object.fetch(:output_assignment_end, nil), - scan1: @delegate_object.fetch(:output_assignment_match, nil), - format1: @delegate_object.fetch(:output_assignment_format, nil) - ) + wait_for_stream_processing + sleep 0.1 + in_thr.kill if in_thr&.alive? + end - else - output_lines = `#{all_code.join("\n")}`.split("\n") + ## select output_lines that look like assignment or match other specs + # + output_lines = process_string_array( + output_lines, + begin_pattern: @delegate_object.fetch(:output_assignment_begin, nil), + end_pattern: @delegate_object.fetch(:output_assignment_end, nil), + scan1: @delegate_object.fetch(:output_assignment_match, nil), + format1: @delegate_object.fetch(:output_assignment_format, nil) + ) + + else + # output_lines = `#{all_code.join("\n")}`.split("\n") + output_lines = `#{@delegate_object[:shell]} #{file.path}`.split("\n") + end end HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true }) unless output_lines label_format_above = @delegate_object[:shell_code_label_format_above] @@ -1283,11 +1299,11 @@ # or name to substitute into the wildcard expression def prompt_for_filespec_with_wildcard(filespec) puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec }) puts @delegate_object[:prompt_enter_filespec] - resolve_path_or_substitute(gets.chomp, filespec) + PathUtils.resolve_path_or_substitute(gets.chomp, filespec) end # Handle expression with wildcard characters # allow user to select or enter def save_filespec_wildcard_expansion(filespec) @@ -1318,15 +1334,38 @@ home: Dir.pwd, started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format]) } end +# # Loads auto link block. +# def load_auto_link_block(all_blocks, link_state, mdoc, block_source:) +# block_name = @delegate_object[:document_load_link_block_name] +# return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] + +# block = HashDelegator.block_find(all_blocks, :oname, block_name) +# return unless block + +# if block.fetch(:shell, '') != BlockType::LINK +# HashDelegator.error_handler('must be Link block type', { abort: true }) + +# else +# # debounce_reset +# push_link_history_and_trigger_load( +# link_block_body: block.fetch(:body, ''), +# mdoc: mdoc, +# selected: block, +# link_state: link_state, +# block_source: block_source +# ) +# end +# end + # Loads auto blocks based on delegate object settings and updates if new filename is detected. # Executes a specified block once per filename. # @param all_blocks [Array] Array of all block elements. # @return [Boolean, nil] True if values were modified, nil otherwise. - def load_auto_blocks(all_blocks) + def load_auto_opts_block(all_blocks) block_name = @delegate_object[:document_load_opts_block_name] return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] block = HashDelegator.block_find(all_blocks, :oname, block_name) return unless block @@ -1352,11 +1391,12 @@ def mdoc_menu_and_blocks_from_nested_files(link_state) all_blocks, mdoc = mdoc_and_blocks_from_nested_files # recreate menu with new options # - all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_blocks(all_blocks) + all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks) + # load_auto_link_block(all_blocks, link_state, mdoc, block_source: {}) menu_blocks = mdoc.fcbs_per_options(@delegate_object) add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state) ### compress empty lines HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true @@ -1754,22 +1794,10 @@ next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load ) end end - # Determines if a given path is absolute or substitutes a placeholder in an expression with the path. - # @param path [String] The input path to check or fill in. - # @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute. - # @return [String] The absolute path or the expression with the wildcard replaced by the path. - def resolve_path_or_substitute(path, expression) - if path.include?('/') - path - else - expression.gsub('*', path) - end - end - def runtime_exception(exception_sym, name, items) if @delegate_object[exception_sym] != 0 data = { name: name, detail: items.join(', ') } warn( format( @@ -1839,16 +1867,24 @@ # ii_display_menu @dml_block_state = SelectedBlockMenuState.new @delegate_object[:block_name] = nil when :user_choice - # puts "? - Select a block to execute (or type #{$texit} to exit):" - break if ii_user_choice == :break # into @dml_block_state - break if @dml_block_state.block.nil? # no block matched - + if @dml_link_state.block_name.present? + # @prior_block_was_link = true + @dml_block_state.block = @dml_blocks_in_file.find do |item| + item[:oname] == @dml_link_state.block_name + end + @dml_link_state.block_name = nil + else + # puts "? - Select a block to execute (or type #{$texit} to exit):" + break if ii_user_choice == :break # into @dml_block_state + break if @dml_block_state.block.nil? # no block matched + end # puts "! - Executing block: #{data}" - @dml_block_state.block[:oname] + # @dml_block_state.block[:oname] + @dml_block_state.block&.fetch(:oname, nil) when :execute_block block_name = data if block_name == '* Back' #### debounce_reset @@ -1886,17 +1922,16 @@ return :break end ## order of block name processing: link block, cli, from user # - @cli_block_name = block_name @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \ HashDelegator.next_link_state( - block_name_from_cli: !@dml_link_state.block_name, - was_using_cli: @dml_now_using_cli, + block_name: @dml_link_state.block_name, + block_name_from_cli: !@dml_link_state.block_name.present?, block_state: @dml_block_state, - block_name: @dml_link_state.block_name + was_using_cli: @dml_now_using_cli ) if !@dml_block_state.block[:block_name_from_ui] && cli_break # &bsp '!block_name_from_ui + cli_break -> break' return :break @@ -2280,11 +2315,16 @@ @delegate_object.merge(default: default) else @delegate_object end - selection_opts.merge!(per_page: @delegate_object[:select_page_height]) + sph = @delegate_object[:select_page_height] + unless sph.positive? + require 'io/console' + sph = [IO.console.winsize[0] - 3, 4].max + end + selection_opts.merge!(per_page: sph) selected_option = select_option_with_metadata(prompt_title, block_menu, selection_opts) determine_block_state(selected_option) end @@ -2329,33 +2369,91 @@ def write_inherited_lines_to_file(link_state, link_block_data) save_expr = link_block_data.fetch(LinkKeys::Save, '') if save_expr.present? save_filespec = save_filespec_from_expression(save_expr) File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines)) - # TTY::File.create_file save_filespec, HahDelegator.join_code_lines(link_state&.inherited_lines), force: true @delegate_object[:filename] else link_block_data[LinkKeys::File] || @delegate_object[:filename] end end end + + class HashDelegator < HashDelegatorParent + # Cleans a value, handling both Hash and Struct types. + # For Structs, the cleaned version is converted to a hash. + def self.clean_value(value) + case value + when Hash + clean_hash_recursively(value) + when Struct + struct_hash = value.to_h # Convert the Struct to a hash + cleaned_hash = clean_hash_recursively(struct_hash) # Clean the hash + # Return the cleaned hash instead of updating the Struct + return cleaned_hash + else + value + end + end + + # Recursively cleans the given object (hash or struct) from unwanted values. + def self.clean_hash_recursively(obj) + obj.each do |key, value| + cleaned_value = clean_value(value) # Clean and possibly convert value + obj[key] = cleaned_value if value.is_a?(Hash) || value.is_a?(Struct) + end + + if obj.is_a?(Hash) + obj.select! { |key, value| ![nil, '', [], {}, nil].include?(value) } + end + + obj + end + + def self.next_link_state(*args, **kwargs, &block) + super + # result = super + + # @logger ||= StdOutErrLogger.new + # @logger.unknown( + # HashDelegator.clean_hash_recursively( + # { "HashDelegator.next_link_state": + # { 'args': args, + # 'at': Time.now.strftime('%FT%TZ'), + # 'for': /[^\/]+:\d+/.match(caller.first)[0], + # 'kwargs': kwargs, + # 'return': result } } + # ) + # ) + + # result + end + end end return if $PROGRAM_NAME != __FILE__ require 'bundler/setup' Bundler.require(:default) require 'minitest/autorun' require 'mocha/minitest' -#### -require_relative 'instance_method_wrapper' -# MarkdownExec::HashDelegator.prepend(InstanceMethodWrapper) -# MarkdownExec::HashDelegator.singleton_class.prepend(ClassMethodWrapper) +require_relative 'std_out_err_logger' module MarkdownExec + class TestHashDelegator0 < Minitest::Test + def setup + @hd = HashDelegator.new + end + + # Test case for empty body + def test_next_link_state + @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil, block_name: nil) + end + end + class TestHashDelegator < Minitest::Test def setup @hd = HashDelegator.new @mdoc = mock('MarkdownDocument') end @@ -3192,31 +3290,35 @@ end refute block_called end end - def test_resolves_absolute_path - absolute_path = '/usr/local/bin' - assert_equal '/usr/local/bin', resolve_path_or_substitute(absolute_path, 'prefix/*/suffix') - end + class PathUtilsTest < Minitest::Test + def test_absolute_path_returns_unchanged + absolute_path = "/usr/local/bin" + expression = "path/to/*/directory" + assert_equal absolute_path, PathUtils.resolve_path_or_substitute(absolute_path, expression) + end - def test_substitutes_wildcard_with_path - path = 'bin' - expression = 'prefix/*/suffix' - expected_result = 'prefix/bin/suffix' - assert_equal expected_result, resolve_path_or_substitute(path, expression) - end + def test_relative_path_gets_substituted + relative_path = "my_folder" + expression = "path/to/*/directory" + expected_output = "path/to/my_folder/directory" + assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression) + end - def test_handles_path_with_no_separator_as_is - path = 'bin' - expression = 'prefix*suffix' - expected_result = 'prefixbinsuffix' - assert_equal expected_result, resolve_path_or_substitute(path, expression) - end + def test_path_with_no_slash_substitutes_correctly + relative_path = "data" + expression = "path/to/*/directory" + expected_output = "path/to/data/directory" + assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression) + end - def test_returns_expression_unchanged_for_empty_path - path = '' - expression = 'prefix/*/suffix' - expected_result = 'prefix/*/suffix' - assert_equal expected_result, resolve_path_or_substitute(path, expression) + def test_empty_path_substitution + empty_path = "" + expression = "path/to/*/directory" + expected_output = "path/to//directory" + assert_equal expected_output, PathUtils.resolve_path_or_substitute(empty_path, expression) + end end + end # module MarkdownExec