lib/hash_delegator.rb in markdown_exec-1.8.9 vs lib/hash_delegator.rb in markdown_exec-2.0.0

- old
+ new

@@ -9,10 +9,11 @@ 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' @@ -38,14 +39,10 @@ !empty? end end module HashDelegatorSelf - # def add_back_option(menu_blocks) - # append_chrome_block(menu_blocks, MenuState::BACK) - # end - # Applies an ANSI color method to a string using a specified color key. # The method retrieves the color method from the provided hash. If the color key # is not present in the hash, it uses a default color method. # @param string [String] The string to be colored. # @param color_methods [Hash] A hash where keys are color names (String/Symbol) and values are color methods. @@ -184,23 +181,23 @@ def initialize_fcb_names(fcb) fcb.oname = fcb.dname = fcb.title || '' end def join_code_lines(lines) - ((lines || [])+ ['']).join("\n") + ((lines || []) + ['']).join("\n") end def merge_lists(*args) # Filters out nil values, flattens the arrays, and ensures an empty list is returned if no valid lists are provided merged = args.compact.flatten merged.empty? ? [] : merged end - def next_link_state(block_name_from_cli, was_using_cli, block_state, block_name: nil) + 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 = block_name_from_cli ? @cli_block_name : block_name + 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 breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH # &bsp 'breaker:', breaker @@ -212,10 +209,12 @@ [block_name, block_name_from_cli, breaker] end def parse_yaml_data_from_body(body) body.any? ? YAML.load(body.join("\n")) : {} + rescue StandardError + error_handler('parse_yaml_data_from_body', { abort: true }) end # Reads required code blocks from a temporary file specified by an environment variable. # @return [Array<String>] Lines read from the temporary file, or an empty array if file is not found or path is empty. def read_required_blocks_from_temp_file(temp_blocks_file_path) @@ -266,25 +265,19 @@ # If the fcb has a body and meets certain conditions, it yields to the given block. # # @param fcb [Object] The fcb object whose attributes are to be updated. # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable. # @param block [Block] An optional block to yield to if conditions are met. - def update_menu_attrib_yield_selected(fcb, selected_messages, configuration = {}, &block) + def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, &block) initialize_fcb_names(fcb) return unless fcb.body default_block_title_from_body(fcb) - MarkdownExec::Filter.yield_to_block_if_applicable(fcb, selected_messages, configuration, + MarkdownExec::Filter.yield_to_block_if_applicable(fcb, messages, configuration, &block) end - # Writes the provided code blocks to a file. - # @param code_blocks [String] Code blocks to write into the file. - def write_code_to_file(content, path) - File.write(path, content) - end - def write_execution_output_to_file(files, filespec) FileUtils.mkdir_p File.dirname(filespec) File.write( filespec, @@ -306,11 +299,10 @@ return unless block && selected_messages.include?(:line) block.call(:line, MarkdownExec::FCB.new(body: [line])) end end -### require_relative 'hash_delegator_self' # This module provides methods for compacting and converting data structures. module CompactionHelpers # Converts an array of key-value pairs into a hash, applying compaction to the values. # Each value is processed by `compact_hash` to remove ineligible elements. @@ -410,54 +402,51 @@ # Modifies the provided menu blocks array by adding 'Back' and 'Exit' options, # along with initial and final dividers, based on the delegate object's configuration. # # @param menu_blocks [Array] The array of menu block elements to be modified. - def add_menu_chrome_blocks!(menu_blocks, link_state) + def add_menu_chrome_blocks!(menu_blocks:, link_state:) return unless @delegate_object[:menu_link_format].present? - if @delegate_object[:menu_with_inherited_lines] - add_inherited_lines(menu_blocks, - link_state) - end + add_inherited_lines(menu_blocks: menu_blocks, link_state: link_state) if @delegate_object[:menu_with_inherited_lines] # back before exit - add_back_option(menu_blocks) if should_add_back_option? + add_back_option(menu_blocks: menu_blocks) if should_add_back_option? # exit after other options - add_exit_option(menu_blocks) if @delegate_object[:menu_with_exit] + add_exit_option(menu_blocks: menu_blocks) if @delegate_object[:menu_with_exit] - add_dividers(menu_blocks) + add_dividers(menu_blocks: menu_blocks) end private - def add_back_option(menu_blocks) - append_chrome_block(menu_blocks, MenuState::BACK) + def add_back_option(menu_blocks:) + append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::BACK) end - def add_dividers(menu_blocks) - append_divider(menu_blocks, :initial) - append_divider(menu_blocks, :final) + def add_dividers(menu_blocks:) + append_divider(menu_blocks: menu_blocks, position: :initial) + append_divider(menu_blocks: menu_blocks, position: :final) end - def add_exit_option(menu_blocks) - append_chrome_block(menu_blocks, MenuState::EXIT) + def add_exit_option(menu_blocks:) + append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::EXIT) end - def add_inherited_lines(menu_blocks, link_state) - append_inherited_lines(menu_blocks, link_state) + def add_inherited_lines(menu_blocks:, link_state:) + append_inherited_lines(menu_blocks: menu_blocks, link_state: link_state) end public # Appends a chrome block, which is a menu option for Back or Exit # # @param all_blocks [Array] The current blocks in the menu # @param type [Symbol] The type of chrome block to add (:back or :exit) - def append_chrome_block(menu_blocks, type) - case type + def append_chrome_block(menu_blocks:, menu_state:) + case menu_state when MenuState::BACK history_state_partition option_name = @delegate_object[:menu_option_back_name] insert_at_top = @delegate_object[:menu_back_at_top] when MenuState::EXIT @@ -485,11 +474,11 @@ # Appends a formatted divider to the specified position in a menu block array. # The method checks for the presence of formatting options before appending. # # @param menu_blocks [Array] The array of menu block elements. # @param position [Symbol] The position to insert the divider (:initial or :final). - def append_inherited_lines(menu_blocks, link_state, position: top) + def append_inherited_lines(menu_blocks:, link_state:, position: top) return unless link_state.inherited_lines.present? insert_at_top = @delegate_object[:menu_inherited_lines_at_top] chrome_blocks = link_state.inherited_lines.map do |line| formatted = format(@delegate_object[:menu_inherited_lines_format], @@ -518,11 +507,11 @@ # Appends a formatted divider to the specified position in a menu block array. # The method checks for the presence of formatting options before appending. # # @param menu_blocks [Array] The array of menu block elements. # @param position [Symbol] The position to insert the divider (:initial or :final). - def append_divider(menu_blocks, position) + def append_divider(menu_blocks:, position:) return unless divider_formatting_present?(position) divider = create_divider(position) position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider) end @@ -540,10 +529,19 @@ else name end end + def assign_key_value_in_bash(key, value) + if value =~ /["$\\`]/ + # requiring ShellWords to write into Bash scripts + "#{key}=#{Shellwords.escape(value)}" + else + "#{key}=\"#{value}\"" + end + end + # private # Iterates through nested files to collect various types of blocks, including dividers, tasks, and others. # The method categorizes blocks based on their type and processes them accordingly. # @@ -601,13 +599,13 @@ # If the block type is VARS, it also sets environment variables based on the block's content. # # @param mdoc [YourMDocClass] An instance of the MDoc class. # @param selected [Hash] The selected block. # @return [Array<String>] Required code blocks as an array of lines. - def collect_required_code_lines(mdoc, selected, link_state = LinkState.new, block_source:) + def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new) required = mdoc.collect_recursively_required_code( - selected[:nickname] || selected[:oname], + anyname: selected[:nickname] || selected[:oname], label_format_above: @delegate_object[:shell_code_label_format_above], label_format_below: @delegate_object[:shell_code_label_format_below], block_source: block_source ) dependencies = (link_state&.inherited_dependencies || {}).merge(required[:dependencies] || {}) @@ -619,11 +617,11 @@ warn format_and_highlight_dependencies(dependencies, highlight: required[:unmet_dependencies]) runtime_exception(:runtime_exception_error_level, 'unmet_dependencies, flag: runtime_exception_error_level', required[:unmet_dependencies]) - elsif true + else warn format_and_highlight_dependencies(dependencies, highlight: [@delegate_object[:block_name]]) end code_lines = selected[:shell] == BlockType::VARS ? set_environment_variables_for_block(selected) : [] @@ -665,18 +663,18 @@ @run_state.in_own_window = false Open3.popen3(@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args) do |stdin, stdout, stderr, exec_thr| - handle_stream(stdout, ExecutionStreams::StdOut) do |line| + handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line| yield nil, line, nil, exec_thr if block_given? end - handle_stream(stderr, ExecutionStreams::StdErr) do |line| + handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line| yield nil, nil, line, exec_thr if block_given? end - in_thr = handle_stream($stdin, ExecutionStreams::StdIn) do |line| + in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line| stdin.puts(line) yield line, nil, nil, exec_thr if block_given? end wait_for_stream_processing @@ -701,11 +699,11 @@ @run_state.error = err @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message] @fout.fout "Error ENOENT: #{err.inspect}" end - def load_cli_or_user_selected_block(all_blocks, menu_blocks, default) + def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil) if @delegate_object[:block_name].present? block = all_blocks.find do |item| item[:oname] == @delegate_object[:block_name] end&.merge(block_name_from_ui: false) else @@ -725,22 +723,22 @@ # may display the code for user approval before execution. It then executes the approved block. # # @param mdoc [Object] The markdown document object containing code blocks. # @param selected [Hash] The selected item from the menu to be executed. # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one. - def compile_execute_and_trigger_reuse(mdoc, selected, link_state = nil, block_source:) - required_lines = collect_required_code_lines(mdoc, selected, link_state, + def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:, link_state: nil) + required_lines = collect_required_code_lines(mdoc: mdoc, selected: selected, link_state: link_state, block_source: block_source) output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve] - display_required_code(required_lines) if output_or_approval + display_required_code(required_lines: required_lines) if output_or_approval allow_execution = if @delegate_object[:user_must_approve] - prompt_for_user_approval(required_lines, selected) + prompt_for_user_approval(required_lines: required_lines, selected: selected) else true end - execute_required_lines(required_lines, selected) if allow_execution + execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution link_state.block_name = nil LoadFileLinkState.new(LoadFile::Reuse, link_state) end @@ -768,12 +766,12 @@ # Creates and adds a formatted block to the blocks array based on the provided match and format options. # @param blocks [Array] The array of blocks to add the new block to. # @param match_data [MatchData] The match data containing named captures for formatting. # @param format_option [String] The format string to be used for the new block. # @param color_method [Symbol] The color method to apply to the block's display name. - def create_and_add_chrome_block(blocks, match_data, format_option, - color_method) + def create_and_add_chrome_block(blocks:, match_data:, format_option:, + color_method:) oname = format(format_option, match_data.named_captures.transform_keys(&:to_sym)) blocks.push FCB.new( chrome: true, disabled: '', @@ -802,12 +800,16 @@ unless @delegate_object[criteria[:match]].present? && (mbody = fcb.body[0].match @delegate_object[criteria[:match]]) next end - create_and_add_chrome_block(blocks, mbody, @delegate_object[criteria[:format]], - @delegate_object[criteria[:color]].to_sym) + create_and_add_chrome_block( + blocks: blocks, + match_data: mbody, + format_option: @delegate_object[criteria[:format]], + color_method: @delegate_object[criteria[:color]].to_sym + ) break end end def create_divider(position) @@ -831,13 +833,11 @@ # filter block if selected in menu return true if @run_state.block_name_from_cli # return false if @prior_execution_block == @delegate_object[:block_name] - if @prior_execution_block == @delegate_object[:block_name] - return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat - end + return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat if @prior_execution_block == @delegate_object[:block_name] @prior_execution_block = @delegate_object[:block_name] @allowed_execution_block = nil true end @@ -867,11 +867,11 @@ # Displays the required lines of code with color formatting for the preview section. # It wraps the code lines between a formatted header and tail. # # @param required_lines [Array<String>] The lines of code to be displayed. - def display_required_code(required_lines) + def display_required_code(required_lines:) output_color_formatted(:script_preview_head, :script_preview_frame_color) required_lines.each { |cb| @fout.fout cb } output_color_formatted(:script_preview_tail, :script_preview_frame_color) @@ -894,14 +894,14 @@ # It sets the script block name, writes command files if required, and handles the execution # including output formatting and summarization. # # @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, selected) if @delegate_object[:save_executed_script] + 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 - format_and_execute_command(required_lines) + format_and_execute_command(code_lines: required_lines) post_execution_process end # Execute a code block after approval and provide user interaction options. # @@ -910,28 +910,49 @@ # code to the clipboard or save it to a file. # # @param opts [Hash] Options hash containing configuration settings. # @param mdoc [YourMDocClass] An instance of the MDoc class. # - def execute_shell_type(selected, mdoc, link_state = LinkState.new, - block_source:) + def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new) if selected.fetch(:shell, '') == BlockType::LINK debounce_reset - push_link_history_and_trigger_load(selected.fetch(:body, ''), mdoc, selected, - link_state) + push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''), + mdoc: mdoc, + selected: selected, + link_state: link_state, + block_source: block_source) elsif @menu_user_clicked_back_link debounce_reset pop_link_history_and_trigger_load elsif selected[:shell] == BlockType::OPTS debounce_reset - options_state = read_show_options_and_trigger_reuse(selected, link_state) + block_names = [] + code_lines = [] + dependencies = {} + options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state) + + ## apply options to current state + # @menu_base_options.merge!(options_state.options) @delegate_object.merge!(options_state.options) - options_state.load_file_link_state + ### options_state.load_file_link_state + link_state = LinkState.new + link_history_push_and_next( + curr_block_name: selected[:oname], + curr_document_filename: @delegate_object[:filename], + inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, + inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data + inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines), + 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 = {} @@ -945,11 +966,13 @@ next_document_filename: @delegate_object[:filename], next_load_file: LoadFile::Reuse ) elsif debounce_allows - compile_execute_and_trigger_reuse(mdoc, selected, link_state, + 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 @@ -966,12 +989,12 @@ color_sym: :execution_report_preview_frame_color) data_string = @delegate_object.fetch(data_sym, default).to_s string_send_color(data_string, color_sym) end - def format_and_execute_command(lines) - formatted_command = lines.flatten.join("\n") + def format_and_execute_command(code_lines:) + formatted_command = code_lines.flatten.join("\n") @fout.fout fetch_color(data_sym: :script_execution_head, color_sym: :script_execution_frame_color) command_execute(formatted_command, args: @pass_args) @fout.fout fetch_color(data_sym: :script_execution_tail, color_sym: :script_execution_frame_color) @@ -1050,11 +1073,11 @@ @delegate_object[:block_name] = block_state.block[:oname] @menu_user_clicked_back_link = block_state.state == MenuState::BACK end - def handle_stream(stream, file_type, swap: false) + def handle_stream(stream:, file_type:, swap: false) @process_mutex.synchronize do Thread.new do stream.each_line do |line| line.strip! @run_state.files[file_type] << line @@ -1104,29 +1127,29 @@ &block) end end end - def link_block_data_eval(link_state, code_lines, selected, link_block_data) + 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 = [] Open3.popen3( @delegate_object[:shell], '-c', all_code.join("\n") ) do |stdin, stdout, stderr, _exec_thr| - handle_stream(stdout, ExecutionStreams::StdOut) do |line| + handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line| output_lines.push(line) end - handle_stream(stderr, ExecutionStreams::StdErr) do |line| + handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line| output_lines.push(line) end - in_thr = handle_stream($stdin, ExecutionStreams::StdIn) do |line| + in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line| stdin.puts(line) end wait_for_stream_processing sleep 0.1 @@ -1145,17 +1168,14 @@ else output_lines = `#{all_code.join("\n")}`.split("\n") end - unless output_lines - HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true }) - 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] label_format_below = @delegate_object[:shell_code_label_format_below] - block_source = { document_filename: link_state&.document_filename } [label_format_above && format(label_format_above, block_source.merge({ block_name: selected[:oname] }))] + output_lines.map do |line| re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)')) @@ -1190,24 +1210,130 @@ inherited_lines: inherited_lines ) ) end + # format + glob + select for file in load block + # name has references to ENV vars and doc and batch vars incl. timestamp + def load_filespec_from_expression(expression) + # Process expression with embedded formatting + expanded_expression = formatted_expression(expression) + + # Handle wildcards or direct file specification + if contains_wildcards?(expanded_expression) + load_filespec_wildcard_expansion(expanded_expression) + else + expanded_expression + end + end + + def save_filespec_from_expression(expression) + # Process expression with embedded formatting + formatted = formatted_expression(expression) + + # Handle wildcards or direct file specification + if contains_wildcards?(formatted) + save_filespec_wildcard_expansion(formatted) + else + formatted + end + end + + # private + + # Expand expression if it contains format specifiers + def formatted_expression(expr) + expr.include?('%{') ? format_expression(expr) : expr + end + + # Format expression using environment variables and run state + def format_expression(expr) + data = link_load_format_data + ENV.each { |key, value| data[key] = value } + format(expr, data) + end + + # Check if the expression contains wildcard characters + def contains_wildcards?(expr) + expr.match(%r{\*|\?|\[}) + end + + # Handle expression with wildcard characters + def load_filespec_wildcard_expansion(expr) + files = find_files(expr) + case files.count + when 0 + HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true }) + when 1 + files.first + else + prompt_select_code_filename(files) + end + end + + # Handle expression with wildcard characters + # allow user to select or enter + def puts_gets_oprompt_(filespec) + puts format(@delegate_object[:prompt_show_expr_format], + { expr: filespec }) + puts @delegate_object[:prompt_enter_filespec] + gets.chomp + end + + # prompt user to enter a path (i.e. containing a path separator) + # 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) + end + + # Handle expression with wildcard characters + # allow user to select or enter + def save_filespec_wildcard_expansion(filespec) + files = find_files(filespec) + case files.count + when 0 + prompt_for_filespec_with_wildcard(filespec) + else + ## user selects from existing files or other + # input into path with wildcard for easy entry + # + name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files) + if name == @delegate_object[:prompt_filespec_other] + prompt_for_filespec_with_wildcard(filespec) + else + name + end + end + end + + def link_load_format_data + { + batch_index: @run_state.batch_index, + batch_random: @run_state.batch_random, + block_name: @delegate_object[:block_name], + document_filename: File.basename(@delegate_object[:filename]), + document_filespec: @delegate_object[:filename], + home: Dir.pwd, + started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format]) + } + 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) block_name = @delegate_object[:document_load_opts_block_name] - unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] - return - end + return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] block = HashDelegator.block_find(all_blocks, :oname, block_name) return unless block - options_state = read_show_options_and_trigger_reuse(block) + options_state = read_show_options_and_trigger_reuse(selected: block) @menu_base_options.merge!(options_state.options) @delegate_object.merge!(options_state.options) @most_recent_loaded_filename = @delegate_object[:filename] true @@ -1229,11 +1355,11 @@ # recreate menu with new options # all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_blocks(all_blocks) menu_blocks = mdoc.fcbs_per_options(@delegate_object) - add_menu_chrome_blocks!(menu_blocks, link_state) + add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state) ### compress empty lines HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true [all_blocks, menu_blocks, mdoc] end @@ -1271,17 +1397,10 @@ @delegate_object[method_name] # super end end - def shift_cli_argument - return true unless @menu_base_options[:input_cli_rest].present? - - @cli_block_name = @menu_base_options[:input_cli_rest].shift - false - end - def output_color_formatted(data_sym, color_sym) formatted_string = string_send_color(@delegate_object[data_sym], color_sym) @fout.fout formatted_string end @@ -1500,11 +1619,11 @@ # @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu. # @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu. # # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise. ## - def prompt_for_user_approval(required_lines, selected) + def prompt_for_user_approval(required_lines:, selected:) # Present a selection menu for user approval. sel = @prompt.select( string_send_color(@delegate_object[:prompt_approve_block], :prompt_color_after_script_execution), filter: true @@ -1520,11 +1639,11 @@ end if sel == MenuOptions::SCRIPT_TO_CLIPBOARD copy_to_clipboard(required_lines) elsif sel == MenuOptions::SAVE_SCRIPT - save_to_file(required_lines, selected) + save_to_file(required_lines: required_lines, selected: selected) end sel == MenuOptions::YES rescue TTY::Reader::InputInterrupt exit 1 @@ -1545,71 +1664,80 @@ exit 1 end # public + def prompt_select_code_filename(filenames) + @prompt.select( + string_send_color(@delegate_object[:prompt_select_code_file], + :prompt_color_after_script_execution), + filter: true, + quiet: true + ) do |menu| + filenames.each do |filename| + menu.choice filename + end + end + rescue TTY::Reader::InputInterrupt + exit 1 + end + # Handles the processing of a link block in Markdown Execution. # It loads YAML data from the link_block_body content, pushes the state to history, # sets environment variables, and decides on the next block to load. # # @param link_block_body [Array<String>] The body content as an array of strings. # @param mdoc [Object] Markdown document object. # @param selected [FCB] Selected code block. # @return [LoadFileLinkState] Object indicating the next action for file loading. - def push_link_history_and_trigger_load(link_block_body, mdoc, selected, - link_state = LinkState.new) + def push_link_history_and_trigger_load(link_block_body: [], mdoc: nil, selected: FCB.new, + link_state: LinkState.new, block_source: {}) link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body) ## collect blocks specified by block # if mdoc code_info = mdoc.collect_recursively_required_code( - selected[:oname], + anyname: selected[:oname], label_format_above: @delegate_object[:shell_code_label_format_above], label_format_below: @delegate_object[:shell_code_label_format_below], - block_source: { document_filename: link_state.document_filename } + block_source: block_source ) code_lines = code_info[:code] block_names = code_info[:block_names] dependencies = code_info[:dependencies] else block_names = [] code_lines = [] dependencies = {} end - next_document_filename = link_block_data[LinkKeys::File] || @delegate_object[:filename] # load key and values from link block into current environment # if link_block_data[LinkKeys::Vars] code_lines.push "# #{selected[:oname]}" (link_block_data[LinkKeys::Vars] || []).each do |(key, value)| ENV[key] = value.to_s - require 'shellwords' - code_lines.push "#{key}=\"#{Shellwords.escape(value)}\"" + code_lines.push(assign_key_value_in_bash(key, value)) end end ## append blocks loaded, apply LinkKeys::Eval # - if (load_filespec = link_block_data.fetch(LinkKeys::Load, '')).present? - code_lines += File.readlines(load_filespec, chomp: true) + if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present? + load_filespec = load_filespec_from_expression(load_expr) + code_lines += File.readlines(load_filespec, chomp: true) if load_filespec end # if an eval link block, evaluate code_lines and return its standard output # if link_block_data.fetch(LinkKeys::Eval, false) || link_block_data.fetch(LinkKeys::Exec, false) - code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data) + code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source) end - ## write variables - # - if (save_filespec = link_block_data.fetch(LinkKeys::Save, '')).present? - File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines)) - next_document_filename = @delegate_object[:filename] - end + next_document_filename = write_inherited_lines_to_file(link_state, link_block_data) if link_block_data[LinkKeys::Return] pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines, dependencies, selected) @@ -1618,17 +1746,30 @@ curr_block_name: selected[:oname], curr_document_filename: @delegate_object[:filename], inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines), - next_block_name: link_block_data.fetch(LinkKeys::NextBlock, nil) || link_block_data[LinkKeys::Block] || '', + next_block_name: link_block_data.fetch(LinkKeys::NextBlock, + nil) || link_block_data[LinkKeys::Block] || '', next_document_filename: next_document_filename, 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( @@ -1644,98 +1785,205 @@ return unless (@delegate_object[exception_sym]).positive? exit @delegate_object[exception_sym] end - def save_to_file(required_lines, selected) - write_command_file(required_lines, selected) + def save_to_file(required_lines:, selected:) + write_command_file(required_lines: required_lines, selected: selected) @fout.fout "File saved: #{@run_state.saved_filespec}" end + def block_state_for_name_from_cli(block_name) + SelectedBlockMenuState.new( + @dml_blocks_in_file.find do |item| + item[:oname] == block_name + end&.merge( + block_name_from_cli: true, + block_name_from_ui: false + ), + MenuState::CONTINUE + ) + end + # Select and execute a code block from a Markdown document. # # This method allows the user to interactively select a code block from a # Markdown document, obtain approval, and execute the chosen block of code. # # @return [Nil] Returns nil if no code block is selected or an error occurs. def document_menu_loop @menu_base_options = @delegate_object - link_state = LinkState.new( + @dml_link_state = LinkState.new( block_name: @delegate_object[:block_name], document_filename: @delegate_object[:filename] ) - @run_state.block_name_from_cli = link_state.block_name.present? - @cli_block_name = link_state.block_name - now_using_cli = @run_state.block_name_from_cli - menu_default_dname = nil + @run_state.block_name_from_cli = @dml_link_state.block_name.present? + @cli_block_name = @dml_link_state.block_name + @dml_now_using_cli = @run_state.block_name_from_cli + @dml_menu_default_dname = nil + @dml_block_state = SelectedBlockMenuState.new @run_state.batch_random = Random.new.rand @run_state.batch_index = 0 - loop do - @run_state.batch_index += 1 - @run_state.in_own_window = false + InputSequencer.new( + @delegate_object[:filename], + @delegate_object[:input_cli_rest] + ).run do |msg, data| + case msg + when :parse_document # once for each menu + # puts "@ - parse document #{data}" + ii_parse_document(data) - # &bsp 'loop', block_name_from_cli, @cli_block_name - @run_state.block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc = \ - set_delobj_menu_loop_vars(@run_state.block_name_from_cli, now_using_cli, link_state) + when :display_menu + # warn "@ - display menu:" + # ii_display_menu + @dml_block_state = SelectedBlockMenuState.new + @delegate_object[:block_name] = nil - # cli or user selection - # - block_state = load_cli_or_user_selected_block(blocks_in_file, menu_blocks, - menu_default_dname) - # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli - if !block_state - HashDelegator.error_handler('block_state missing', { abort: true }) - elsif block_state.state == MenuState::EXIT - # &bsp 'load_cli_or_user_selected_block -> break' - break - end + 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 - dump_and_warn_block_state(block_state.block) - link_state, menu_default_dname = exec_bash_next_state(block_state.block, mdoc, - link_state) - if prompt_user_exit(@run_state.block_name_from_cli, block_state.block) - # &bsp 'prompt_user_exit -> break' - break - end + # puts "! - Executing block: #{data}" + @dml_block_state.block[:oname] - ## order of block name processing - # from link block - # from cli - # from user - # - link_state.block_name, @run_state.block_name_from_cli, cli_break = \ - HashDelegator.next_link_state(!link_state.block_name && !shift_cli_argument, now_using_cli, block_state, block_name: link_state.block_name) + when :execute_block + block_name = data + if block_name == '* Back' #### + debounce_reset + @menu_user_clicked_back_link = true + load_file_link_state = pop_link_history_and_trigger_load + @dml_link_state = load_file_link_state.link_state - if !block_state.block[:block_name_from_ui] && cli_break - # &bsp '!block_name_from_ui + cli_break -> break' - break + InputSequencer.merge_link_state( + @dml_link_state, + InputSequencer.next_link_state( + block_name: @dml_link_state.block_name, + document_filename: @dml_link_state.document_filename, + prior_block_was_link: true + ) + ) + + else + @dml_block_state = block_state_for_name_from_cli(block_name) + if @dml_block_state.block[:shell] == BlockType::OPTS + debounce_reset + link_state = LinkState.new + options_state = read_show_options_and_trigger_reuse( + selected: @dml_block_state.block, + link_state: link_state + ) + + @menu_base_options.merge!(options_state.options) + @delegate_object.merge!(options_state.options) + options_state.load_file_link_state.link_state + else + ii_execute_block(block_name) + + if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli, + selected: @dml_block_state.block) + 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_state: @dml_block_state, + block_name: @dml_link_state.block_name + ) + + if !@dml_block_state.block[:block_name_from_ui] && cli_break + # &bsp '!block_name_from_ui + cli_break -> break' + return :break + end + + InputSequencer.next_link_state( + block_name: @dml_link_state.block_name, + prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH + ) + end + end + + when :exit? + data == $texit + when :stay? + data == $stay + else + raise "Invalid message: #{msg}" end end rescue StandardError HashDelegator.error_handler('document_menu_loop', { abort: true }) end - def exec_bash_next_state(block_state_block, mdoc, link_state) + def ii_parse_document(_document_filename) + @run_state.batch_index += 1 + @run_state.in_own_window = false + + # &bsp 'loop', block_name_from_cli, @cli_block_name + @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \ + set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli, + now_using_cli: @dml_now_using_cli, + link_state: @dml_link_state) + end + + def ii_user_choice + @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file, + menu_blocks: @dml_menu_blocks, + default: @dml_menu_default_dname) + # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli + if !@dml_block_state + HashDelegator.error_handler('block_state missing', { abort: true }) + elsif @dml_block_state.state == MenuState::EXIT + # &bsp 'load_cli_or_user_selected_block -> break' + :break + end + end + + def ii_execute_block(block_name) + @dml_block_state = block_state_for_name_from_cli(block_name) + + dump_and_warn_block_state(selected: @dml_block_state.block) + @dml_link_state, @dml_menu_default_dname = \ + exec_bash_next_state( + selected: @dml_block_state.block, + mdoc: @dml_mdoc, + link_state: @dml_link_state, + block_source: { + document_filename: @delegate_object[:filename], + time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format]) + } + ) + end + + def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {}) lfls = execute_shell_type( - block_state_block, - mdoc, - link_state, - block_source: { document_filename: @delegate_object[:filename] } + selected: selected, + mdoc: mdoc, + link_state: link_state, + block_source: block_source ) # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item [lfls.link_state, - lfls.load_file == LoadFile::Load ? nil : block_state_block[:dname]] + lfls.load_file == LoadFile::Load ? nil : selected[:dname]] end - def set_delobj_menu_loop_vars(block_name_from_cli, now_using_cli, link_state) + def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:) block_name_from_cli, now_using_cli = \ - manage_cli_selection_state(block_name_from_cli, now_using_cli, link_state) - set_delob_filename_block_name(link_state, block_name_from_cli) + manage_cli_selection_state(block_name_from_cli: block_name_from_cli, + now_using_cli: now_using_cli, + link_state: link_state) + set_delob_filename_block_name(link_state: link_state, + block_name_from_cli: block_name_from_cli) # update @delegate_object and @menu_base_options in auto_load # blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state) dump_delobj(blocks_in_file, menu_blocks, link_state) @@ -1743,18 +1991,18 @@ [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc] end # user prompt to exit if the menu will be displayed again # - def prompt_user_exit(block_name_from_cli, block_state_block) + def prompt_user_exit(block_name_from_cli:, selected:) !block_name_from_cli && - block_state_block[:shell] == BlockType::BASH && + selected[:shell] == BlockType::BASH && @delegate_object[:pause_after_script_execution] && prompt_select_continue == MenuState::EXIT end - def manage_cli_selection_state(block_name_from_cli, now_using_cli, link_state) + def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:) if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name] # &bsp 'pause cli control, allow user to select block' block_name_from_cli = false now_using_cli = false @menu_base_options[:block_name] = \ @@ -1773,11 +2021,11 @@ # This method updates the block name based on whether it was specified # through the CLI or derived from the link state. # # @param link_state [LinkState] The current link state object. # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI. - def set_delob_filename_block_name(link_state, block_name_from_cli) + def set_delob_filename_block_name(link_state:, block_name_from_cli:) @delegate_object[:filename] = link_state.document_filename link_state.block_name = @delegate_object[:block_name] = block_name_from_cli ? @cli_block_name : link_state.block_name end @@ -1786,13 +2034,11 @@ # @param delegate_object [Hash] The delegate object containing configuration flags. # @param blocks_in_file [Hash] Hash of blocks present in the file. # @param menu_blocks [Hash] Hash of menu blocks. # @param link_state [LinkState] Current state of the link. def dump_delobj(blocks_in_file, menu_blocks, link_state) - if @delegate_object[:dump_delegate_object] - warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') - end + warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object] if @delegate_object[:dump_blocks_in_file] warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file), label: 'blocks_in_file') end @@ -1800,24 +2046,26 @@ if @delegate_object[:dump_menu_blocks] warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks), label: 'menu_blocks') end + warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names] + warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies] return unless @delegate_object[:dump_inherited_lines] warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines') end - def dump_and_warn_block_state(block_state_block) - if block_state_block.nil? + def dump_and_warn_block_state(selected:) + if selected.nil? Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}", { abort: true }) end return unless @delegate_object[:dump_selected_block] - warn block_state_block.to_yaml.sub(/^(?:---\n)?/, "Block:\n") + warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n") end # Presents a TTY prompt to select an option or exit, returns metadata including option and selected def select_option_with_metadata(prompt_text, names, opts = {}) selection = @prompt.select(prompt_text, @@ -1948,12 +2196,16 @@ line = nested_line.to_s if line.match(@delegate_object[:fenced_start_and_end_regex]) if state[:in_fenced_block] ## end of code block # - HashDelegator.update_menu_attrib_yield_selected(state[:fcb], selected_messages, @delegate_object, - &block) + HashDelegator.update_menu_attrib_yield_selected( + fcb: state[:fcb], + messages: selected_messages, + configuration: @delegate_object, + &block + ) state[:in_fenced_block] = false else ## start of code block # state[:fcb] = @@ -1982,11 +2234,11 @@ # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output. # @param selected [Hash] Selected item from the menu containing a YAML body. # @param tgt2 [Hash, nil] An optional target hash to update with YAML data. # @return [LoadFileLinkState] An instance indicating the next action for loading files. - def read_show_options_and_trigger_reuse(selected, link_state = LinkState.new) + def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new) obj = {} data = YAML.load(selected[:body].join("\n")) (data || []).each do |key, value| sym_key = key.to_sym obj[sym_key] = value @@ -2036,11 +2288,11 @@ selection_opts) determine_block_state(selected_option) end # Handles the core logic for generating the command file's metadata and content. - def write_command_file(required_lines, selected) + def write_command_file(required_lines:, selected:) return unless @delegate_object[:save_executed_script] time_now = Time.now.utc @run_state.saved_script_filename = SavedAsset.script_name( @@ -2072,29 +2324,20 @@ ) rescue StandardError HashDelegator.error_handler('write_command_file') end - # Writes required code blocks to a temporary file and sets an environment variable with its path. - # - # @param mdoc [Object] The Markdown document object. - # @param block_name [String] The name of the block to collect code for. - def write_required_blocks_to_file(mdoc, block_name, temp_file_path, import_filename: nil) - c1 = if mdoc - mdoc.collect_recursively_required_code( - block_name, - label_format_above: @delegate_object[:shell_code_label_format_above], - label_format_below: @delegate_object[:shell_code_label_format_below] - )[:code] - else - [] - end - - code_blocks = (HashDelegator.read_required_blocks_from_temp_file(import_filename) + - c1).join("\n") - - HashDelegator.write_code_to_file(code_blocks, temp_file_path) + 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 end return if $PROGRAM_NAME != __FILE__ @@ -2103,10 +2346,15 @@ Bundler.require(:default) require 'minitest/autorun' require 'mocha/minitest' +#### +require_relative 'dev/instance_method_wrapper' +# MarkdownExec::HashDelegator.prepend(InstanceMethodWrapper) +# MarkdownExec::HashDelegator.singleton_class.prepend(ClassMethodWrapper) + module MarkdownExec class TestHashDelegator < Minitest::Test def setup @hd = HashDelegator.new @mdoc = mock('MarkdownDocument') @@ -2134,18 +2382,18 @@ end # Test case for empty body def test_push_link_history_and_trigger_load_with_empty_body assert_equal LoadFile::Reuse, - @hd.push_link_history_and_trigger_load([], nil, FCB.new).load_file + @hd.push_link_history_and_trigger_load.load_file end # Test case for non-empty body without 'file' key def test_push_link_history_and_trigger_load_without_file_key body = ["vars:\n KEY: VALUE"] assert_equal LoadFile::Reuse, - @hd.push_link_history_and_trigger_load(body, nil, FCB.new).load_file + @hd.push_link_history_and_trigger_load(link_block_body: body).load_file end # Test case for non-empty body with 'file' key def test_push_link_history_and_trigger_load_with_file_key body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"] @@ -2153,12 +2401,14 @@ LinkState.new(block_name: 'sample_block', document_filename: 'sample_file', inherited_dependencies: {}, inherited_lines: ['# ', 'KEY="VALUE"'])) assert_equal expected_result, - @hd.push_link_history_and_trigger_load(body, nil, FCB.new(block_name: 'sample_block', - filename: 'sample_file')) + @hd.push_link_history_and_trigger_load( + link_block_body: body, + selected: FCB.new(block_name: 'sample_block', filename: 'sample_file') + ) end def test_indent_all_lines_with_indent body = "Line 1\nLine 2" indent = ' ' # Two spaces @@ -2232,28 +2482,28 @@ HashDelegator.stubs(:safeval).returns('Safe Value') end def test_append_divider_initial menu_blocks = [] - @hd.append_divider(menu_blocks, :initial) + @hd.append_divider(menu_blocks: menu_blocks, position: :initial) assert_equal 1, menu_blocks.size assert_equal 'Formatted Divider', menu_blocks.first.dname end def test_append_divider_final menu_blocks = [] - @hd.append_divider(menu_blocks, :final) + @hd.append_divider(menu_blocks: menu_blocks, position: :final) assert_equal 1, menu_blocks.size assert_equal 'Formatted Divider', menu_blocks.last.dname end def test_append_divider_without_format @hd.instance_variable_set(:@delegate_object, {}) menu_blocks = [] - @hd.append_divider(menu_blocks, :initial) + @hd.append_divider(menu_blocks: menu_blocks, position: :initial) assert_empty menu_blocks end end @@ -2320,11 +2570,11 @@ end def test_collect_required_code_lines_with_vars YAML.stubs(:load).returns({ 'key' => 'value' }) @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] }) - result = @hd.collect_required_code_lines(@mdoc, @selected, block_source: {}) + result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, block_source: {}) assert_equal ['code line', 'key="value"'], result end end @@ -2339,22 +2589,22 @@ def test_command_selected_block all_blocks = [{ oname: 'block1' }, { oname: 'block2' }] @hd.instance_variable_set(:@delegate_object, { block_name: 'block1' }) - result = @hd.load_cli_or_user_selected_block(all_blocks, [], nil) + result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks) assert_equal all_blocks.first.merge(block_name_from_ui: false), result.block assert_nil result.state end def test_user_selected_block block_state = SelectedBlockMenuState.new({ oname: 'block2' }, :some_state) @hd.stubs(:wait_for_user_selected_block).returns(block_state) - result = @hd.load_cli_or_user_selected_block([], [], nil) + result = @hd.load_cli_or_user_selected_block assert_equal block_state.block.merge(block_name_from_ui: true), result.block assert_equal :some_state, result.state end end @@ -2477,11 +2727,11 @@ required_lines = %w[line1 line2] @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_head).returns('Header') @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_tail).returns('Footer') @hd.instance_variable_get(:@fout).expects(:fout).times(4) - @hd.display_required_code(required_lines) + @hd.display_required_code(required_lines: required_lines) # Verifying that fout is called for each line and for header & footer assert true # Placeholder for actual test assertions end end @@ -2687,11 +2937,11 @@ def test_handle_stream stream = StringIO.new("line 1\nline 2\n") file_type = :stdout - Thread.new { @hd.handle_stream(stream, file_type) } + Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) } @hd.wait_for_stream_processing assert_equal ['line 1', 'line 2'], @hd.instance_variable_get(:@run_state).files[:stdout] @@ -2700,11 +2950,11 @@ def test_handle_stream_with_io_error stream = StringIO.new("line 1\nline 2\n") file_type = :stdout stream.stubs(:each_line).raises(IOError) - Thread.new { @hd.handle_stream(stream, file_type) } + Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) } @hd.wait_for_stream_processing assert_equal [], @hd.instance_variable_get(:@run_state).files[:stdout] @@ -2864,17 +3114,17 @@ def test_update_menu_attrib_yield_selected_with_body HashDelegator.expects(:initialize_fcb_names).with(@fcb) HashDelegator.expects(:default_block_title_from_body).with(@fcb) Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], {}) - HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message]) + HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message]) end def test_update_menu_attrib_yield_selected_without_body @fcb.stubs(:body).returns(nil) HashDelegator.expects(:initialize_fcb_names).with(@fcb) - HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message]) + HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message]) end end class TestHashDelegatorWaitForUserSelectedBlock < Minitest::Test def setup @@ -2940,7 +3190,33 @@ Filter.yield_to_block_if_applicable(@fcb, [:non_blocks]) do |_| block_called = true 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 + + 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_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_returns_expression_unchanged_for_empty_path + path = '' + expression = 'prefix/*/suffix' + expected_result = 'prefix/*/suffix' + assert_equal expected_result, resolve_path_or_substitute(path, expression) end end # module MarkdownExec