lib/markdown_exec.rb in markdown_exec-1.3.8 vs lib/markdown_exec.rb in markdown_exec-1.3.9

- old
+ new

@@ -35,10 +35,17 @@ $stderr.sync = true $stdout.sync = true BLOCK_SIZE = 1024 +# macros +# +BACK_OPTION = '* Back' +EXIT_OPTION = '* Exit' +LOAD_FILE = true +VN = 'MDE_MENU_HISTORY' + # custom error: file specified is missing # class FileMissingError < StandardError; end # hash with keys sorted by name @@ -56,10 +63,22 @@ transform_keys(&:to_sym) end end end +# integer value for comparison +# +def options_fetch_display_level(options) + options.fetch(:display_level, 1) +end + +# integer value for comparison +# +def options_fetch_display_level_xbase_prefix(options) + options.fetch(:level_xbase_prefix, '') +end + # stdout manager # module FOUT # standard output; not for debug # @@ -75,19 +94,19 @@ puts "# #{name}" puts data.to_yaml end def approved_fout?(level) - level <= @options[:display_level] + level <= options_fetch_display_level(@options) end # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT) # def lout(str, level: DISPLAY_LEVEL_BASE) return unless approved_fout? level - fout level == DISPLAY_LEVEL_BASE ? str : @options[:display_level_xbase_prefix] + str + fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str end end def dp(str) lout " => #{str}", level: DISPLAY_LEVEL_DEBUG @@ -119,10 +138,17 @@ module MarkdownExec # :reek:IrresponsibleModule FNR11 = '/' FNR12 = ',~' + SHELL_COLOR_OPTIONS = { + 'bash' => :menu_bash_color, + BLOCK_TYPE_LINK => :menu_link_color, + 'opts' => :menu_opts_color, + 'vars' => :menu_vars_color + }.freeze + ## # # rubocop:disable Layout/LineLength # :reek:DuplicateMethodCall { allow_calls: ['block', 'item', 'lm', 'opts', 'option', '@options', 'required_blocks'] } # rubocop:enable Layout/LineLength @@ -160,21 +186,10 @@ else argv[0..ind - 1] end end - # return arguments after `--` - # - def arguments_for_child(argv = ARGV) - case ind = argv.find_index('--') - when nil, argv.count - 1 - [] - else - argv[ind + 1..-1] - end - end - ## # options necessary to start, parse input, defaults for cli options # def base_options menu_iter do |item| @@ -206,128 +221,174 @@ # 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. # @return [String] The name of the executed code block. + # def approve_and_execute_block(opts, mdoc) - # Collect required code blocks based on the provided options. - required_blocks = mdoc.collect_recursively_required_code(opts[:block_name]) - # Display required code blocks if requested or required approval. - if opts[:output_script] || opts[:user_must_approve] - display_required_code(opts, - required_blocks) + selected = mdoc.get_block_by_name(opts[:block_name]) + if selected[:shell] == BLOCK_TYPE_LINK + handle_link_shell(opts, selected) + elsif selected[:shell] == 'opts' + handle_opts_shell(opts, selected) + else + required_lines = collect_required_code_blocks(opts, mdoc, selected) + # Display required code blocks if requested or required approval. + if opts[:output_script] || opts[:user_must_approve] + display_required_code(opts, required_lines) + end + + allow = true + allow = user_approval(opts, required_lines) if opts[:user_must_approve] + opts[:ir_approve] = allow + mdoc.get_block_by_name(opts[:block_name]) + execute_approved_block(opts, required_lines) if opts[:ir_approve] + + [!LOAD_FILE, ''] end + end - allow = true - # If user approval is required, prompt the user for approval. - if opts[:user_must_approve] - loop do - # Present a selection menu for user approval. - sel = @prompt.select(opts[:prompt_approve_block], - filter: true) do |menu| - menu.default 1 - menu.choice opts[:prompt_yes], 1 - menu.choice opts[:prompt_no], 2 - menu.choice opts[:prompt_script_to_clipboard], 3 - menu.choice opts[:prompt_save_script], 4 - end - allow = (sel == 1) - if sel == 3 - # Copy the code to the clipboard. - text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR) - Clipboard.copy(text) - fout "Clipboard updated: #{required_blocks.count} blocks," / - " #{required_blocks.flatten.count} lines," / - " #{text.length} characters" - end - if sel == 4 - # Save the code to a file. - write_command_file(opts.merge(save_executed_script: true), - required_blocks) - fout "File saved: #{@options[:saved_filespec]}" - end - break if [1, 2].include? sel - end + def handle_link_shell(opts, selected) + data = YAML.load(selected[:body].join("\n")) + + # add to front of history + # + ENV[VN] = opts[:filename] + opts[:history_document_separator] + ENV.fetch(VN, '') + + opts[:filename] = data.fetch('file', nil) + return !LOAD_FILE unless opts[:filename] + + data.fetch('vars', []).each do |var| + ENV[var[0]] = var[1].to_s end - opts[:ir_approve] = allow + [LOAD_FILE, data.fetch('block', '')] + end - # Get the selected code block by name. - selected = mdoc.get_block_by_name(opts[:block_name]) + def handle_opts_shell(opts, selected) + data = YAML.load(selected[:body].join("\n")) + data.each_key do |key| + opts[key.to_sym] = value = data[key].to_s + next unless opts[:menu_opts_set_format].present? - # If approved, write the code to a file, execute it, and provide output. - if opts[:ir_approve] - write_command_file(opts, required_blocks) - command_execute(opts, required_blocks.flatten.join("\n")) - save_execution_output - output_execution_summary - output_execution_result + print format( + opts[:menu_opts_set_format], + { key: key, + value: value } + ).send(opts[:menu_opts_set_color].to_sym) end + [!LOAD_FILE, ''] + end - selected[:name] + def user_approval(opts, required_lines) + # Present a selection menu for user approval. + sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu| + menu.default 1 + menu.choice opts[:prompt_yes], 1 + menu.choice opts[:prompt_no], 2 + menu.choice opts[:prompt_script_to_clipboard], 3 + menu.choice opts[:prompt_save_script], 4 + end + + if sel == 3 + copy_to_clipboard(required_lines) + elsif sel == 4 + save_to_file(opts, required_lines) + end + + sel == 1 end + def execute_approved_block(opts, required_lines) + write_command_file(opts, required_lines) + command_execute( + opts, + required_lines.flatten.join("\n"), + args: opts.fetch(:pass_args, []) + ) + save_execution_output + output_execution_summary + output_execution_result + end + + # Collect required code blocks based on the provided options. + # + # @param opts [Hash] Options hash containing configuration settings. + # @param mdoc [YourMDocClass] An instance of the MDoc class. + # @return [Array<String>] Required code blocks as an array of lines. + def collect_required_code_blocks(opts, mdoc, selected) + required = mdoc.collect_recursively_required_code(opts[:block_name]) + required_lines = required[:code] + required[:blocks] + + # Apply hash in opts block to environment variables + if selected[:shell] == BLOCK_TYPE_VARS + data = YAML.load(selected[:body].join("\n")) + data.each_key do |key| + ENV[key] = value = data[key].to_s + next unless opts[:menu_vars_set_format].present? + + print format( + opts[:menu_vars_set_format], + { key: key, + value: value } + ).send(opts[:menu_vars_set_color].to_sym) + end + end + + required_lines + end + def cfile @cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern)) end - # :reek:DuplicateMethodCall - # :reek:UncommunicativeVariableName { exclude: [ e ] } - # :reek:LongYieldList - def command_execute(opts, command) - #d 'execute command and yield outputs' + EF_STDOUT = :stdout + EF_STDERR = :stderr + EF_STDIN = :stdin + + # Handles reading and processing lines from a given IO stream + # + # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin). + # @param file_type [Symbol] The type of file to which the stream corresponds. + def handle_stream(opts, stream, file_type, swap: false) + Thread.new do + until (line = stream.gets).nil? + @execute_files[file_type] = @execute_files[file_type] + [line.strip] + print line if opts[:output_stdout] + yield line if block_given? + end + rescue IOError + #d 'stdout IOError, thread killed, do nothing' + end + end + + # Existing command_execute method + def command_execute(opts, command, args: []) @execute_files = Hash.new([]) @execute_options = opts @execute_started_at = Time.now.utc - args = [] - Open3.popen3(@options[:shell], '-c', - command, ARGV[0], *args) do |stdin, stdout, stderr, exec_thr| - #d 'command started' - Thread.new do - until (line = stdout.gets).nil? - @execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line] - print line if opts[:output_stdout] - yield nil, line, nil, exec_thr if block_given? - end - rescue IOError - #d 'stdout IOError, thread killed, do nothing' + Open3.popen3(opts[:shell], '-c', command, opts[:filename], + *args) do |stdin, stdout, stderr, exec_thr| + handle_stream(opts, stdout, EF_STDOUT) do |line| + yield nil, line, nil, exec_thr if block_given? end - - Thread.new do - until (line = stderr.gets).nil? - @execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line] - print line if opts[:output_stdout] - yield nil, nil, line, exec_thr if block_given? - end - rescue IOError - #d 'stderr IOError, thread killed, do nothing' + handle_stream(opts, stderr, EF_STDERR) do |line| + yield nil, nil, line, exec_thr if block_given? end - in_thr = Thread.new do - while exec_thr.alive? # reading input until the child process ends - stdin.puts(line = $stdin.gets) - @execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line] - yield line, nil, nil, exec_thr if block_given? - end - #d 'exec_thr now dead' - rescue StandardError - #d 'stdin error, thread killed, do nothing' + in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line| + stdin.puts(line) + yield line, nil, nil, exec_thr if block_given? end - #d 'join exec_thr' exec_thr.join - - #d 'wait before closing stdin' sleep 0.1 - - #d 'kill stdin thread' - in_thr.kill - # @return_code = exec_thr.value - #d 'command end' + in_thr.kill if in_thr&.alive? end - #d 'command completed' + @execute_completed_at = Time.now.utc rescue Errno::ENOENT => err #d 'command error ENOENT triggered by missing command in script' @execute_aborted_at = Time.now.utc @execute_error_message = err.message @@ -342,23 +403,23 @@ @execute_files[EF_STDERR] += [@execute_error_message] fout "Error ENOENT: #{err.inspect}" end def count_blocks_in_filename - fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match] + fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex] cnt = 0 cfile.readlines(@options[:filename]).each do |line| - cnt += 1 if line.match(fenced_start_and_end_match) + cnt += 1 if line.match(fenced_start_and_end_regex) end cnt / 2 end # :reek:DuplicateMethodCall - def display_required_code(opts, required_blocks) + def display_required_code(opts, required_lines) frame = opts[:output_divider].send(opts[:output_divider_color].to_sym) fout frame - required_blocks.each { |cb| fout cb } + required_lines.each { |cb| fout cb } fout frame end # :reek:DuplicateMethodCall def exec_block(options, _block_name = '') @@ -424,11 +485,10 @@ ## summarize blocks # def get_block_summary(call_options, fcb) opts = optsmerge call_options # return fcb.body unless opts[:struct] - return fcb unless opts[:bash] fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil) titlexcall = if fcb.call fcb.title.sub("%#{fcb.call}", '') @@ -436,111 +496,123 @@ fcb.title end bm = option_match_groups(titlexcall, opts[:block_name_match]) fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan]) fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan]) - fcb.title = fcb.name = (bm && bm[1] ? bm[:title] : titlexcall) + + shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]] + fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall + fcb.dname = if shell_color_option && opts[shell_color_option].present? + fcb.oname.send(opts[shell_color_option].to_sym) + else + fcb.oname + end fcb end # :reek:DuplicateMethodCall # :reek:LongYieldList # :reek:NestedIterators - def iter_blocks_in_file(opts = {}) - # opts = optsmerge call_options, options_block + #--- + def iter_blocks_in_file(opts = {}, &block) unless opts[:filename]&.present? fout 'No blocks found.' return end unless File.exist? opts[:filename] fout 'Document is missing.' return end - fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match] - fenced_start_ex = Regexp.new opts[:fenced_start_ex_match] + fenced_start_and_end_regex = Regexp.new opts[:fenced_start_and_end_regex] + fenced_start_extended_regex = Regexp.new opts[:fenced_start_extended_regex] fcb = FCB.new - in_block = false + in_fenced_block = false headings = [] ## get type of messages to select # selected_messages = yield :filter cfile.readlines(opts[:filename]).each.with_index do |line, _line_num| continue unless line + headings = update_headings(line, headings, opts) if opts[:menu_blocks_with_headings] - if opts[:menu_blocks_with_headings] - if (lm = line.match(Regexp.new(opts[:heading3_match]))) - headings = [headings[0], headings[1], lm[:name]] - elsif (lm = line.match(Regexp.new(opts[:heading2_match]))) - headings = [headings[0], lm[:name]] - elsif (lm = line.match(Regexp.new(opts[:heading1_match]))) - headings = [lm[:name]] + if line.match(fenced_start_and_end_regex) + if in_fenced_block + process_fenced_block(fcb, opts, selected_messages, &block) + in_fenced_block = false + else + fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex) + in_fenced_block = true end + elsif in_fenced_block && fcb.body + dp 'append line to fcb body' + fcb.body += [line.chomp] + else + process_line(line, opts, selected_messages, &block) end + end + end - if line.match(fenced_start_and_end_match) - if in_block - # end fcb - # - fcb.name = fcb.title || '' - if fcb.body - if fcb.title.nil? || fcb.title.empty? - fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64] - end + def start_fenced_block(opts, line, headings, fenced_start_extended_regex) + fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys + fcb = FCB.new + fcb.headings = headings + fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '') + fcb.shell = fcb_title_groups.fetch(:shell, '') + fcb.title = fcb_title_groups.fetch(:name, '') - if block_given? && - selected_messages.include?(:blocks) && - Filter.fcb_select?(opts, fcb) - yield :blocks, fcb - end - end - in_block = false - else - # start fcb - # - in_block = true + # selected fcb + fcb.body = [] - fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys - fcb = FCB.new - fcb.headings = headings - fcb.name = fcb_title_groups.fetch(:name, '') - fcb.shell = fcb_title_groups.fetch(:shell, '') - fcb.title = fcb_title_groups.fetch(:name, '') + rest = fcb_title_groups.fetch(:rest, '') + fcb.reqs, fcb.wraps = + split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name| + !name.match(Regexp.new(opts[:block_name_wrapper_match])) + end + fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first + fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/)) + tn.named_captures.sym_keys + end + fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/)) + tn.named_captures.sym_keys + end + fcb + end - # selected fcb - # - fcb.body = [] + def process_fenced_block(fcb, opts, selected_messages, &block) + fcb.oname = fcb.dname = fcb.title || '' + return unless fcb.body - rest = fcb_title_groups.fetch(:rest, '') - fcb.reqs, fcb.wraps = - split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name| - !name.match(Regexp.new(opts[:block_name_wrapper_match])) - end - fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first - fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/)) - tn.named_captures.sym_keys - end - fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/)) - tn.named_captures.sym_keys - end - end - elsif in_block && fcb.body - dp 'append line to fcb body' - fcb.body += [line.chomp] - elsif block_given? && selected_messages.include?(:line) - dp 'text outside of fcb' - fcb = FCB.new - fcb.body = [line] - yield :line, fcb - end + set_fcb_title(fcb) + + if block && + selected_messages.include?(:blocks) && + Filter.fcb_select?(opts, fcb) + block.call :blocks, fcb end end + def process_line(line, _opts, selected_messages, &block) + return unless block && selected_messages.include?(:line) + + # dp 'text outside of fcb' + fcb = FCB.new + fcb.body = [line] + block.call(:line, fcb) + end + + # set the title of an FCB object based on its body if it is nil or empty + def set_fcb_title(fcb) + return unless fcb.title.nil? || fcb.title.empty? + + fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64] + end + def split_array(arr) true_list = [] false_list = [] arr.each do |element| @@ -552,15 +624,21 @@ end [true_list, false_list] end - # # Example usage: - # array = [1, 2, 3, 4, 5] - # result = split_array(array) { |num| num.even? } - # puts "True List: #{result[0]}" # Output: True List: [2, 4] - # puts "False List: #{result[1]}" # Output: False List: [1, 3, 5] + def update_headings(line, headings, opts) + if (lm = line.match(Regexp.new(opts[:heading3_match]))) + [headings[0], headings[1], lm[:name]] + elsif (lm = line.match(Regexp.new(opts[:heading2_match]))) + [headings[0], lm[:name]] + elsif (lm = line.match(Regexp.new(opts[:heading1_match]))) + [lm[:name]] + else + headings + end + end # return body, title if option.struct # return body if not struct # def list_blocks_in_file(call_options = {}, &options_block) @@ -570,14 +648,15 @@ blocks = [] if opts[:menu_initial_divider].present? && use_chrome blocks.push FCB.new({ # name: '', chrome: true, - name: format( + dname: format( opts[:menu_divider_format], opts[:menu_initial_divider] ).send(opts[:menu_divider_color].to_sym), + oname: opts[:menu_initial_divider], disabled: '' # __LINE__.to_s }) end iter_blocks_in_file(opts) do |btype, fcb| @@ -594,24 +673,29 @@ (mbody = fcb.body[0].match opts[:menu_divider_match]) if use_chrome blocks.push FCB.new( { chrome: true, disabled: '', - name: format(opts[:menu_divider_format], - mbody[:name]).send(opts[:menu_divider_color].to_sym) } + dname: format(opts[:menu_divider_format], + mbody[:name]).send(opts[:menu_divider_color].to_sym), + oname: mbody[:name] } ) end elsif opts[:menu_task_match].present? && (fcb.body[0].match opts[:menu_task_match]) if use_chrome blocks.push FCB.new( { chrome: true, disabled: '', - name: format( + dname: format( opts[:menu_task_format], $~.named_captures.transform_keys(&:to_sym) - ).send(opts[:menu_task_color].to_sym) } + ).send(opts[:menu_task_color].to_sym), + oname: format( + opts[:menu_task_format], + $~.named_captures.transform_keys(&:to_sym) + ) } ) end else # line not added end @@ -624,13 +708,14 @@ if opts[:menu_divider_format].present? && opts[:menu_final_divider].present? && use_chrome && use_chrome blocks.push FCB.new( { chrome: true, disabled: '', - name: format(opts[:menu_divider_format], - opts[:menu_final_divider]) - .send(opts[:menu_divider_color].to_sym) } + dname: format(opts[:menu_divider_format], + opts[:menu_final_divider]) + .send(opts[:menu_divider_color].to_sym), + oname: opts[:menu_final_divider] } ) end blocks rescue StandardError => err warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}") @@ -707,11 +792,11 @@ if opts[:struct] blocks else # blocks.map(&:name) blocks.map do |block| - block.fetch(:text, nil) || block.fetch(:name, nil) + block.fetch(:text, nil) || block.oname end end.compact.reject(&:empty?) end ## output type (body string or full object) per option struct and bash @@ -800,14 +885,14 @@ when :filter %i[blocks line] when :line if options[:menu_divider_match] && (mbody = fcb.body[0].match(options[:menu_divider_match])) - menu.push FCB.new({ name: mbody[:name], disabled: '' }) + menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' }) end when :blocks - menu += [fcb.name] + menu += [fcb.oname] end end menu end @@ -849,11 +934,11 @@ ## post-parse options configuration # def options_finalize(rest) ## position 0: file or folder (optional) # - if (pos = rest.fetch(0, nil))&.present? + if (pos = rest.shift)&.present? if Dir.exist?(pos) @options[:path] = pos elsif File.exist?(pos) @options[:filename] = pos else @@ -861,11 +946,11 @@ end end ## position 1: block name (optional) # - block_name = rest.fetch(1, nil) + block_name = rest.shift @options[:block_name] = block_name if block_name.present? end # :reek:ControlParameter def optsmerge(call_options = {}, options_block = nil) @@ -908,29 +993,68 @@ execute_started_at: @execute_started_at, execute_script_filespec: @execute_script_filespec } end + ## insert back option at head or tail + # + ## Adds a back option at the head or tail of a menu + # + def prompt_menu_add_back(items, label = BACK_OPTION) + return items unless @options[:menu_with_back] + + history = ENV.fetch('MDE_MENU_HISTORY', '') + return items unless history.present? + + @hs_curr, @hs_rest = split_string_on_first_char( + history, + @options[:history_document_separator] + ) + + @options[:menu_back_at_top] ? [label] + items : items + [label] + end + ## insert exit option at head or tail # - def prompt_menu_add_exit(_prompt_text, items, exit_option, _opts = {}) + def prompt_menu_add_exit(items, label = EXIT_OPTION) if @options[:menu_exit_at_top] - (@options[:menu_with_exit] ? [exit_option] : []) + items + (@options[:menu_with_exit] ? [label] : []) + items else - items + (@options[:menu_with_exit] ? [exit_option] : []) + items + (@options[:menu_with_exit] ? [label] : []) end end ## tty prompt to select # insert exit option at head or tail # return selected option or nil # def prompt_with_quit(prompt_text, items, opts = {}) - exit_option = '* Exit' - sel = @prompt.select(prompt_text, prompt_menu_add_exit(prompt_text, items, exit_option, opts), + obj = prompt_with_quit2(prompt_text, items, opts) + if obj.fetch(:option, nil) + nil + else + obj[:selected] + end + end + + ## tty prompt to select + # insert exit option at head or tail + # return option:, selected option: + # + def prompt_with_quit2(prompt_text, items, opts = {}) + sel = @prompt.select(prompt_text, + prompt_menu_add_exit( + prompt_menu_add_back(items) + ), opts.merge(filter: true)) - sel == exit_option ? nil : sel + if sel == BACK_OPTION + { option: sel, curr: @hs_curr, rest: @hs_rest } + elsif sel == EXIT_OPTION + { option: sel } + else + { selected: sel } + end end # :reek:UtilityFunction ### temp def read_configuration_file!(options, configuration_path) return unless File.exist?(configuration_path) @@ -939,16 +1063,12 @@ .transform_keys(&:to_sym)) end # :reek:NestedIterators def run - ## default configuration - # @options = base_options - ## read local configuration file - # read_configuration_file! @options, ".#{MarkdownExec::APP_NAME.downcase}.yml" @option_parser = option_parser = OptionParser.new do |opts| executable_name = File.basename($PROGRAM_NAME) @@ -960,16 +1080,17 @@ menu_iter do |item| menu_option_append opts, options, item end end - option_parser.load # filename defaults to basename of the program - # without suffix in a directory ~/.options - option_parser.environment # env defaults to the basename of the program - # child_argv = arguments_for_child + option_parser.load + option_parser.environment rest = option_parser.parse!(arguments_for_mde) # (into: options) + # pass through arguments excluded from OptionParser with `--` + @options[:pass_args] = ARGV[rest.count + 1..] + begin options_finalize rest exec_block options, options[:block_name] rescue FileMissingError => err puts "File missing: #{err}" @@ -1024,65 +1145,102 @@ ol += @execute_files&.fetch(EF_STDIN, []) ol += ["\n"] File.write(@options[:logged_stdout_filespec], ol.join) end + # Prepare the blocks menu by adding labels and other necessary details. + # + # @param blocks_in_file [Array<Hash>] The list of blocks from the file. + # @param opts [Hash] The options hash. + # @return [Array<Hash>] The updated blocks menu. + def prepare_blocks_menu(blocks_in_file, opts) + # next if fcb.fetch(:disabled, false) + # next unless fcb.fetch(:name, '').present? + blocks_in_file.map do |fcb| + fcb.merge!( + name: fcb.dname, + label: BlockLabel.make( + body: fcb[:body], + filename: opts[:filename], + headings: fcb.fetch(:headings, []), + menu_blocks_with_docname: opts[:menu_blocks_with_docname], + menu_blocks_with_headings: opts[:menu_blocks_with_headings], + text: fcb[:text], + title: fcb[:title] + ) + ) + fcb.to_h + end.compact + 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. # # @param call_options [Hash] Initial options for the method. # @param options_block [Block] Block of options to be merged with call_options. # @return [Nil] Returns nil if no code block is selected or an error occurs. def select_approve_and_execute_block(call_options, &options_block) opts = optsmerge(call_options, options_block) - blocks_in_file = list_blocks_in_file(opts.merge(struct: true)) - mdoc = MDoc.new(blocks_in_file) do |nopts| - opts.merge!(nopts) - end - blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true)) - repeat_menu = true && !opts[:block_name].present? + + load_file = !LOAD_FILE loop do - unless opts[:block_name].present? - pt = opts[:prompt_select_block].to_s + # load file + # + loop do + # repeat menu + # + load_file = !LOAD_FILE + blocks_in_file = list_blocks_in_file(opts.merge(struct: true)) + mdoc = MDoc.new(blocks_in_file) do |nopts| + opts.merge!(nopts) + end + blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true)) + unless opts[:block_name].present? + pt = opts[:prompt_select_block].to_s + bm = prepare_blocks_menu(blocks_menu, opts) + return nil if bm.count.zero? - bm = blocks_menu.map do |fcb| - # next if fcb.fetch(:disabled, false) - # next unless fcb.fetch(:name, '').present? + # sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height]) + # return nil if sel.nil? + obj = prompt_with_quit2(pt, bm, per_page: opts[:select_page_height]) + case obj.fetch(:option, nil) + when EXIT_OPTION + return nil + when BACK_OPTION + opts[:filename] = obj[:curr] + opts[:block_name] = @options[:block_name] = '' + ENV['MDE_MENU_HISTORY'] = obj[:rest] + load_file = LOAD_FILE # later: exit menu, load file + else + sel = obj[:selected] - fcb.merge!( - label: BlockLabel.make( - body: fcb[:body], - filename: opts[:filename], - headings: fcb.fetch(:headings, []), - menu_blocks_with_docname: opts[:menu_blocks_with_docname], - menu_blocks_with_headings: opts[:menu_blocks_with_headings], - text: fcb[:text], - title: fcb[:title] - ) - ) + ## store selected option + # + label_block = blocks_in_file.select do |fcb| + fcb.dname == sel + end.fetch(0, nil) + opts[:block_name] = @options[:block_name] = label_block.oname + end + end + break if load_file == LOAD_FILE - fcb.to_h - end.compact - return nil if bm.count.zero? + # later: load file - sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height]) - return nil if sel.nil? + load_file, block_name = approve_and_execute_block(opts, mdoc) - ## store selected option - # - label_block = blocks_in_file.select do |fcb| - fcb[:label] == sel - end.fetch(0, nil) - opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '') - end - approve_and_execute_block(opts, mdoc) - break unless repeat_menu + opts[:block_name] = block_name + if load_file == LOAD_FILE + repeat_menu = true + break + end - opts[:block_name] = '' + break unless repeat_menu + end + break if load_file != LOAD_FILE end rescue StandardError => err warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}") warn err.backtrace binding.pry if $tap_enable @@ -1140,10 +1298,26 @@ item.delete(:procname) item end.to_yaml end + # Splits the given string on the first occurrence of the specified character. + # Returns an array containing the portion of the string before the character and the rest of the string. + # + # @param input_str [String] The string to be split. + # @param split_char [String] The character on which to split the string. + # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string. + def split_string_on_first_char(input_str, split_char) + split_index = input_str.index(split_char) + + if split_index.nil? + [input_str, ''] + else + [input_str[0...split_index], input_str[(split_index + 1)..-1]] + end + end + def tab_completions(data = menu_for_optparse) data.map do |item| "--#{item[:long_name]}" if item[:long_name] end.compact end @@ -1157,11 +1331,11 @@ @options.merge! opts end @options end - def write_command_file(call_options, required_blocks) + def write_command_file(call_options, required_lines) return unless call_options[:save_executed_script] time_now = Time.now.utc opts = optsmerge call_options opts[:saved_script_filename] = @@ -1184,14 +1358,79 @@ File.write(@options[:saved_filespec], shebang + "# file_name: #{opts[:filename]}\n" \ "# block_name: #{opts[:block_name]}\n" \ "# time: #{time_now}\n" \ - "#{required_blocks.flatten.join("\n")}\n") + "#{required_lines.flatten.join("\n")}\n") return if @options[:saved_script_chmod].zero? File.chmod @options[:saved_script_chmod], @options[:saved_filespec] end end # class MarkParse end # module MarkdownExec -require 'minitest/autorun' if $PROGRAM_NAME == __FILE__ +if $PROGRAM_NAME == __FILE__ + require 'bundler/setup' + Bundler.require(:default) + + require 'minitest/autorun' + + module MarkdownExec + class TestMarkParse < Minitest::Test + require 'mocha/minitest' + + def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value + pigeon = 'E' + obj = { pass_args: pigeon } + + c = MarkdownExec::MarkParse.new + + # Expect that method command_execute is called with argument args having value pigeon + c.expects(:command_execute).with( + obj, + '', + args: pigeon) + + # Call method execute_approved_block + c.execute_approved_block(obj, []) + end + + def setup + @mark_parse = MarkdownExec::MarkParse.new + end + + def test_set_fcb_title + # sample input and output data for testing set_fcb_title method + input_output_data = [ + { + input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]), + output: "puts 'Hello, world!'" + }, + { + input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']), + output: 'def add(x, y) x + y end' + }, + { + input: FCB.new(title: 'foo', body: %w[bar baz]), + output: 'foo' # expect the title to remain unchanged + } + ] + + # iterate over the input and output data and assert that the method sets the title as expected + input_output_data.each do |data| + input = data[:input] + output = data[:output] + @mark_parse.set_fcb_title(input) + assert_equal output, input.title + end + end + end + + ### + + # result = split_string_on_first_char("hello-world", "-") + # puts result.inspect # Output should be ["hello", "world"] + + # result = split_string_on_first_char("hello", "-") + # puts result.inspect # Output should be ["hello", ""] + end +end