lib/markdown_exec.rb in markdown_exec-0.2.1 vs lib/markdown_exec.rb in markdown_exec-0.2.3

- old
+ new

@@ -1,24 +1,46 @@ #!/usr/bin/env ruby # frozen_string_literal: true # encoding=utf-8 -$pdebug = !(ENV['MARKDOWN_EXEC_DEBUG'] || '').empty? - require 'open3' require 'optparse' require 'tty-prompt' require 'yaml' +## +# default if nil +# false if empty or '0' +# else true + +def env_bool(name, default: false) + return default if (val = ENV[name]).nil? + return false if val.empty? || val == '0' + + true +end + +def env_int(name, default: 0) + return default if (val = ENV[name]).nil? + return default if val.empty? + + val.to_i +end + +def env_str(name, default: '') + ENV[name] || default +end + +$pdebug = env_bool 'MDE_DEBUG' + require_relative 'markdown_exec/version' $stderr.sync = true $stdout.sync = true BLOCK_SIZE = 1024 -SELECT_PAGE_HEIGHT = 12 class Object # rubocop:disable Style/Documentation def present? self && !blank? end @@ -29,54 +51,241 @@ def blank? empty? || BLANK_RE.match?(self) end end +public + +# debug output +# +def tap_inspect(format: nil, name: 'return') + return self unless $pdebug + + fn = case format + when :json + :to_json + when :string + :to_s + when :yaml + :to_yaml + else + :inspect + end + + puts "-> #{caller[0].scan(/in `?(\S+)'$/)[0][0]}()" \ + " #{name}: #{method(fn).call}" + + self +end + module MarkdownExec class Error < StandardError; end ## # class MarkParse attr_accessor :options def initialize(options = {}) @options = options + @prompt = TTY::Prompt.new(interrupt: :exit) end + ## + # options necessary to start, parse input, defaults for cli options + + def base_options + { + # commands + list_blocks: false, # command + list_docs: false, # command + + # command options + filename: env_str('MDE_FILENAME', default: nil), # option Filename to open + output_execution_summary: env_bool('MDE_OUTPUT_EXECUTION_SUMMARY', default: false), # option + output_script: env_bool('MDE_OUTPUT_SCRIPT', default: false), # option + output_stdout: env_bool('MDE_OUTPUT_STDOUT', default: true), # option + path: env_str('MDE_PATH', default: nil), # option Folder to search for files + save_executed_script: env_bool('MDE_SAVE_EXECUTED_SCRIPT', default: false), # option + saved_script_folder: env_str('MDE_SAVED_SCRIPT_FOLDER', default: 'logs'), # option + user_must_approve: env_bool('MDE_USER_MUST_APPROVE', default: true), # option Pause for user to approve script + + # configuration options + block_name_excluded_match: env_str('MDE_BLOCK_NAME_EXCLUDED_MATCH', default: '^\(.+\)$'), + block_name_match: env_str('MDE_BLOCK_NAME_MATCH', default: ':(?<title>\S+)( |$)'), + block_required_scan: env_str('MDE_BLOCK_REQUIRED_SCAN', default: '\+\S+'), + fenced_start_and_end_match: env_str('MDE_FENCED_START_AND_END_MATCH', default: '^`{3,}'), + fenced_start_ex_match: env_str('MDE_FENCED_START_EX_MATCH', default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$'), + heading1_match: env_str('MDE_HEADING1_MATCH', default: '^# *(?<name>[^#]*?) *$'), + heading2_match: env_str('MDE_HEADING2_MATCH', default: '^## *(?<name>[^#]*?) *$'), + heading3_match: env_str('MDE_HEADING3_MATCH', default: '^### *(?<name>.+?) *$'), + md_filename_glob: env_str('MDE_MD_FILENAME_GLOB', default: '*.[Mm][Dd]'), + md_filename_match: env_str('MDE_MD_FILENAME_MATCH', default: '.+\\.md'), + mdheadings: true, # use headings (levels 1,2,3) in block lable + select_page_height: env_int('MDE_SELECT_PAGE_HEIGHT', default: 12) + } + end + + def default_options + { + bash: true, # bash block parsing in get_block_summary() + exclude_expect_blocks: true, + exclude_matching_block_names: true, # exclude hidden blocks + output_saved_script_filename: false, + prompt_select_block: 'Choose a block:', # in select_and_approve_block() + prompt_select_md: 'Choose a file:', # in select_md_file() + saved_script_filename: nil, # calculated + struct: true # allow get_block_summary() + } + end + # Returns true if all files are EOF # def all_at_eof(files) files.find { |f| !f.eof }.nil? end + def approve_block(opts, blocks_in_file) + required_blocks = list_recursively_required_blocks(blocks_in_file, opts[:block_name]) + display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve] + + allow = true + allow = @prompt.yes? 'Process?' if opts[:user_must_approve] + opts[:ir_approve] = allow + selected = get_block_by_name blocks_in_file, opts[:block_name] + + if opts[:ir_approve] + write_command_file(opts, required_blocks) if opts[:save_executed_script] + command_execute opts, required_blocks.flatten.join("\n") + end + + selected[:name] + end + + def code(table, block) + all = [block[:name]] + recursively_required(table, block[:reqs]) + all.reverse.map do |req| + get_block_by_name(table, req).fetch(:body, '') + end + .flatten(1) + .tap_inspect + end + + def command_execute(opts, cmd2) + @execute_options = opts + @execute_started_at = Time.now.utc + Open3.popen3(cmd2) do |stdin, stdout, stderr| + stdin.close_write + begin + files = [stdout, stderr] + + until all_at_eof(files) + ready = IO.select(files) + + next unless ready + + # readable = ready[0] + # # writable = ready[1] + # # exceptions = ready[2] + @execute_files = Hash.new([]) + ready.each.with_index do |readable, ind| + readable.each do |f| + block = f.read_nonblock(BLOCK_SIZE) + @execute_files[ind] = @execute_files[ind] + [block] + print block if opts[:output_stdout] + rescue EOFError #=> e + # do nothing at EOF + end + end + end + rescue IOError => e + fout "IOError: #{e}" + end + @execute_completed_at = Time.now.utc + end + rescue Errno::ENOENT => e + @execute_aborted_at = Time.now.utc + @execute_error_message = e.message + @execute_error = e + fout "Error ENOENT: #{e.inspect}" + end + def count_blocks_in_filename + fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match] cnt = 0 - File.readlines(options[:filename]).each do |line| - cnt += 1 if line.match(/^```/) + File.readlines(@options[:filename]).each do |line| + cnt += 1 if line.match(fenced_start_and_end_match) end cnt / 2 end + def display_command(_opts, required_blocks) + required_blocks.each { |cb| fout cb } + end + + def exec_block(options, block_name = '') + options = default_options.merge options + update_options options, over: false + + # document and block reports + # + files = list_files_per_options(options) + if @options[:list_docs] + fout_list files + return + end + + if @options[:list_blocks] + fout_list (files.map do |file| + make_block_labels(filename: file, struct: true) + end).flatten(1) + return + end + + # process + # + select_and_approve_block( + bash: true, + block_name: block_name, + filename: select_md_file(files), + struct: true + ) + + fout "saved_filespec: #{@execute_script_filespec}" if @options[:output_saved_script_filename] + + output_execution_summary if @options[:output_execution_summary] + end + + # standard output; not for debug + # def fout(str) - puts str # to stdout + puts str end + def fout_list(str) + puts str + end + + def fout_section(name, data) + puts "# #{name}" + puts data.to_yaml + end + def get_block_by_name(table, name, default = {}) table.select { |block| block[:name] == name }.fetch(0, default) end def get_block_summary(opts, headings, block_title, current) return [current] unless opts[:struct] return [summarize_block(headings, block_title).merge({ body: current })] unless opts[:bash] - bm = block_title.match(/:(\S+)( |$)/) - reqs = block_title.scan(/\+\S+/).map { |s| s[1..] } + bm = block_title.match(Regexp.new(opts[:block_name_match])) + reqs = block_title.scan(Regexp.new(opts[:block_required_scan])).map { |s| s[1..] } if bm && bm[1] - [summarize_block(headings, bm[1]).merge({ body: current, reqs: reqs })] + [summarize_block(headings, bm[:title]).merge({ body: current, reqs: reqs })] else [summarize_block(headings, block_title).merge({ body: current, reqs: reqs })] end end @@ -86,140 +295,124 @@ unless opts[:filename]&.present? fout 'No blocks found.' exit 1 end + fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match] + fenced_start_ex = Regexp.new opts[:fenced_start_ex_match] + block_title = '' blocks = [] current = nil - in_block = false - block_title = '' - headings = [] + in_block = false File.readlines(opts[:filename]).each do |line| continue unless line if opts[:mdheadings] - if (lm = line.match(/^### *(.+?) *$/)) - headings = [headings[0], headings[1], lm[1]] - elsif (lm = line.match(/^## *([^#]*?) *$/)) - headings = [headings[0], lm[1]] - elsif (lm = line.match(/^# *([^#]*?) *$/)) - headings = [lm[1]] + 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]] end end - if line.match(/^`{3,}/) + if line.match(fenced_start_and_end_match) if in_block if current - block_title = current.join(' ').gsub(/ +/, ' ')[0..64] if block_title.nil? || block_title.empty? - blocks += get_block_summary opts, headings, block_title, current current = nil end in_block = false block_title = '' else - ## new block + # new block # - - lm = line.match(/^`{3,}([^`\s]*) *(.*)$/) + lm = line.match(fenced_start_ex) do1 = false if opts[:bash_only] - do1 = true if lm && (lm[1] == 'bash') + do1 = true if lm && (lm[:shell] == 'bash') else do1 = true - do1 = !(lm && (lm[1] == 'expect')) if opts[:exclude_expect_blocks] - - # if do1 && opts[:exclude_matching_block_names] - # puts " MW a4" - # puts " MW a4 #{(lm[2].match %r{^:\(.+\)$})}" - # do1 = !(lm && (lm[2].match %r{^:\(.+\)$})) - # end + do1 = !(lm && (lm[:shell] == 'expect')) if opts[:exclude_expect_blocks] end in_block = true - if do1 && (!opts[:title_match] || (lm && lm[2] && lm[2].match(opts[:title_match]))) + if do1 && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match]))) current = [] - block_title = (lm && lm[2]) + block_title = (lm && lm[:name]) end end elsif current current += [line.chomp] end end - blocks.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug } - # blocks.map do |block| - # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^\(.+\)$}) - # block - # end.compact.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug } + blocks.tap_inspect end def list_files_per_options(options) default_filename = 'README.md' default_folder = '.' if options[:filename]&.present? - list_files_specified(options[:filename], options[:folder], default_filename, default_folder) + list_files_specified(options[:filename], options[:path], default_filename, default_folder) else - list_files_specified(nil, options[:folder], default_filename, default_folder) - end + list_files_specified(nil, options[:path], default_filename, default_folder) + end.tap_inspect end def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil) - fn = if specified_filename&.present? - if specified_folder&.present? - "#{specified_folder}/#{specified_filename}" - else - "#{default_folder}/#{specified_filename}" - end - elsif specified_folder&.present? - if filetree - "#{specified_folder}/.+\\.md" - else - "#{specified_folder}/*.[Mm][Dd]" - end - else - "#{default_folder}/#{default_filename}" - end + fn = File.join(if specified_filename&.present? + if specified_folder&.present? + [specified_folder, specified_filename] + elsif specified_filename.start_with? '/' + [specified_filename] + else + [default_folder, specified_filename] + end + elsif specified_folder&.present? + if filetree + [specified_folder, @options[:md_filename_match]] + else + [specified_folder, @options[:md_filename_glob]] + end + else + [default_folder, default_filename] + end) if filetree filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) } else Dir.glob(fn) - end.tap { |ret| puts "list_files_specified() ret: #{ret.inspect}" if $pdebug } + end.tap_inspect end - def list_markdown_files_in_folder - Dir.glob(File.join(options[:folder], '*.md')) + def list_markdown_files_in_path + Dir.glob(File.join(@options[:path], @options[:md_filename_glob])).tap_inspect end def list_named_blocks_in_file(call_options = {}, &options_block) opts = optsmerge call_options, options_block + block_name_excluded_match = Regexp.new opts[:block_name_excluded_match] list_blocks_in_file(opts).map do |block| - next if opts[:exclude_matching_block_names] && block[:name].match(/^\(.+\)$/) + next if opts[:exclude_matching_block_names] && block[:name].match(block_name_excluded_match) block - end.compact.tap { |ret| puts "list_named_blocks_in_file() ret: #{ret.inspect}" if $pdebug } + end.compact.tap_inspect end - def code(table, block) - all = [block[:name]] + recursively_required(table, block[:reqs]) - all.reverse.map do |req| - get_block_by_name(table, req).fetch(:body, '') - end - .flatten(1) - .tap { |ret| puts "code() ret: #{ret.inspect}" if $pdebug } - end - def list_recursively_required_blocks(table, name) name_block = get_block_by_name(table, name) + raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty? + all = [name_block[:name]] + recursively_required(table, name_block[:reqs]) # in order of appearance in document table.select { |block| all.include? block[:name] } .map { |block| block.fetch(:body, '') } .flatten(1) - .tap { |ret| puts "list_recursively_required_blocks() ret: #{ret.inspect}" if $pdebug } + .tap_inspect end def make_block_label(block, call_options = {}) opts = options.merge(call_options) if opts[:mdheadings] @@ -234,38 +427,51 @@ opts = options.merge(call_options) list_blocks_in_file(opts).map do |block| # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^:\(.+\)$}) make_block_label block, opts - end.compact.tap { |ret| puts "make_block_labels() ret: #{ret.inspect}" if $pdebug } + end.compact.tap_inspect end def option_exclude_blocks(opts, blocks) + block_name_excluded_match = Regexp.new opts[:block_name_excluded_match] if opts[:exclude_matching_block_names] - blocks.reject { |block| block[:name].match(/^\(.+\)$/) } + blocks.reject { |block| block[:name].match(block_name_excluded_match) } else blocks end end def optsmerge(call_options = {}, options_block = nil) - class_call_options = options.merge(call_options || {}) + class_call_options = @options.merge(call_options || {}) if options_block options_block.call class_call_options else class_call_options - end.tap { |ret| puts "optsmerge() ret: #{ret.inspect}" if $pdebug } + end.tap_inspect end + def output_execution_summary + fout_section 'summary', { + execute_aborted_at: @execute_aborted_at, + execute_completed_at: @execute_completed_at, + execute_error: @execute_error, + execute_error_message: @execute_error_message, + execute_files: @execute_files, + execute_options: @execute_options, + execute_started_at: @execute_started_at, + execute_script_filespec: @execute_script_filespec + } + end + def read_configuration_file!(options, configuration_path) - if File.exist?(configuration_path) - # rubocop:disable Security/YAMLLoad - options.merge!((YAML.load(File.open(configuration_path)) || {}) - .transform_keys(&:to_sym)) - # rubocop:enable Security/YAMLLoad - end - options + return unless File.exist?(configuration_path) + + # rubocop:disable Security/YAMLLoad + options.merge!((YAML.load(File.open(configuration_path)) || {}) + .transform_keys(&:to_sym)) + # rubocop:enable Security/YAMLLoad end def recursively_required(table, reqs) all = [] rem = reqs @@ -276,233 +482,179 @@ all += [req] get_block_by_name(table, req).fetch(:reqs, []) end .compact .flatten(1) - .tap { |_ret| puts "recursively_required() rem: #{rem.inspect}" if $pdebug } + .tap_inspect(name: 'rem') end - all.tap { |ret| puts "recursively_required() ret: #{ret.inspect}" if $pdebug } + all.tap_inspect end def run ## default configuration # - options = { - mdheadings: true, - list_blocks: false, - list_docs: false - } + @options = base_options ## post-parse options configuration # options_finalize = ->(_options) {} + proc_self = ->(value) { value } + proc_to_i = ->(value) { value.to_i != 0 } + proc_true = ->(_) { true } + # read local configuration file # - read_configuration_file! options, ".#{MarkdownExec::APP_NAME.downcase}.yml" + read_configuration_file! @options, ".#{MarkdownExec::APP_NAME.downcase}.yml" option_parser = OptionParser.new do |opts| executable_name = File.basename($PROGRAM_NAME) opts.banner = [ - "#{MarkdownExec::APP_NAME} - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})", - "Usage: #{executable_name} [filename or path] [options]" + "#{MarkdownExec::APP_NAME}" \ + " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})", + "Usage: #{executable_name} [path] [filename] [options]" ].join("\n") - ## menu top: items appear in reverse order added - # - opts.on('--config PATH', 'Read configuration file') do |value| - read_configuration_file! options, value - end + summary_head = [ + ['config', nil, nil, 'PATH', 'Read configuration file', + nil, ->(value) { read_configuration_file! options, value }], + ['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output', + nil, ->(value) { $pdebug = value.to_i != 0 }] + ] - ## menu body: items appear in order added - # - opts.on('-f RELATIVE', '--filename', 'Name of document') do |value| - options[:filename] = value - end + summary_body = [ + ['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document', + :filename, proc_self], + ['list-blocks', nil, nil, nil, 'List blocks', + :list_blocks, proc_true], + ['list-docs', nil, nil, nil, 'List docs in current folder', + :list_docs, proc_true], + ['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution', + :output_execution_summary, proc_to_i], + ['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script', + :output_script, proc_to_i], + ['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution', + :output_stdout, proc_to_i], + ['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents', + :path, proc_self], + ['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script', + :save_executed_script, proc_to_i], + ['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder', + :saved_script_folder, proc_self], + ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause to approve execution', + :user_must_approve, proc_to_i] + ] - opts.on('-p PATH', '--path', 'Path to documents') do |value| - options[:folder] = value - end + # rubocop:disable Style/Semicolon + summary_tail = [ + [nil, '0', nil, nil, 'Show configuration', + nil, ->(_) { options_finalize.call options; fout options.to_yaml }], + ['help', 'h', nil, nil, 'App help', + nil, ->(_) { fout option_parser.help; exit }], + ['version', 'v', nil, nil, 'App version', + nil, ->(_) { fout MarkdownExec::VERSION; exit }], + ['exit', 'x', nil, nil, 'Exit app', + nil, ->(_) { exit }] + ] + # rubocop:enable Style/Semicolon - opts.on('--list-blocks', 'List blocks') do |_value| - options[:list_blocks] = true + (summary_head + summary_body + summary_tail) + .map do |long_name, short_name, env_var, arg_name, description, opt_name, proc1| # rubocop:disable Metrics/ParameterLists + opts.on(*[long_name.present? ? "--#{long_name}#{arg_name.present? ? (' ' + arg_name) : ''}" : nil, + short_name.present? ? "-#{short_name}" : nil, + [description, + env_var.present? ? "env: #{env_var}" : nil].compact.join(' - '), + lambda { |value| + ret = proc1.call(value) + options[opt_name] = ret if opt_name + ret + }].compact) end - - opts.on('--list-docs', 'List docs in current folder') do |_value| - options[:list_docs] = true - end - - ## menu bottom: items appear in order added - # - opts.on_tail('-h', '--help', 'App help') do |_value| - fout option_parser.help - exit - end - - opts.on_tail('-v', '--version', 'App version') do |_value| - fout MarkdownExec::VERSION - exit - end - - opts.on_tail('-x', '--exit', 'Exit app') do |_value| - exit - end - - opts.on_tail('-0', 'Show configuration') do |_v| - options_finalize.call options - fout options.to_yaml - 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. rest = option_parser.parse! # (into: options) + + ## finalize configuration + # options_finalize.call options - if rest.fetch(0, nil)&.present? - if Dir.exist?(rest[0]) - options[:folder] = rest[0] - elsif File.exist?(rest[0]) - options[:filename] = rest[0] + ## position 0: file or folder (optional) + # + if (pos = rest.fetch(0, nil))&.present? + if Dir.exist?(pos) + options[:path] = pos + elsif File.exist?(pos) + options[:filename] = pos + else + raise "Invalid parameter: #{pos}" end end - ## process + ## position 1: block name (optional) # - options.merge!( - { - approve: true, - bash: true, - display: true, - exclude_expect_blocks: true, - exclude_matching_block_names: true, - execute: true, - prompt: 'Execute', - struct: true - } - ) - mp = MarkParse.new options + block_name = rest.fetch(1, nil) - ## show - # - if options[:list_docs] - fout mp.list_files_per_options options - return - end - - if options[:list_blocks] - fout (mp.list_files_per_options(options).map do |file| - mp.make_block_labels(filename: file, struct: true) - end).flatten(1) - return - end - - mp.select_block( - bash: true, - filename: select_md_file(list_files_per_options(options)), - struct: true - ) + exec_block options, block_name end - def select_block(call_options = {}, &options_block) + def select_and_approve_block(call_options = {}, &options_block) opts = optsmerge call_options, options_block + blocks_in_file = list_blocks_in_file(opts.merge(struct: true)) - blocks = list_blocks_in_file(opts.merge(struct: true)) + unless opts[:block_name].present? + pt = (opts[:prompt_select_block]).to_s + blocks_in_file.each { |block| block.merge! label: make_block_label(block, opts) } + block_labels = option_exclude_blocks(opts, blocks_in_file).map { |block| block[:label] } - prompt = TTY::Prompt.new(interrupt: :exit) - pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:" + return nil if block_labels.count.zero? - # blocks.map do |block| - # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^\(.+\)$}) - # block - # end.compact.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug } - - blocks.each { |block| block.merge! label: make_block_label(block, opts) } - # block_labels = blocks.map { |block| block[:label] } - block_labels = option_exclude_blocks(opts, blocks).map { |block| block[:label] } - - if opts[:preview_options] - select_per_page = 3 - block_labels.each do |bn| - fout " - #{bn}" - end - else - select_per_page = SELECT_PAGE_HEIGHT + sel = @prompt.select(pt, block_labels, per_page: opts[:select_page_height]) + label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil) + opts[:block_name] = label_block[:name] end - return nil if block_labels.count.zero? - - sel = prompt.select(pt, block_labels, per_page: select_per_page) - - label_block = blocks.select { |block| block[:label] == sel }.fetch(0, nil) - sel = label_block[:name] - - cbs = list_recursively_required_blocks(blocks, sel) - - ## display code blocks for approval - # - cbs.each { |cb| fout cb } if opts[:display] || opts[:approve] - - allow = true - allow = prompt.yes? 'Process?' if opts[:approve] - - selected = get_block_by_name blocks, sel - if allow && opts[:execute] - - cmd2 = cbs.flatten.join("\n") - - Open3.popen3(cmd2) do |stdin, stdout, stderr| - stdin.close_write - begin - files = [stdout, stderr] - - until all_at_eof(files) - ready = IO.select(files) - - next unless ready - - readable = ready[0] - # writable = ready[1] - # exceptions = ready[2] - - readable.each do |f| - print f.read_nonblock(BLOCK_SIZE) - rescue EOFError #=> e - # do nothing at EOF - end - end - rescue IOError => e - fout "IOError: #{e}" - end - end - end - - selected[:name] + approve_block opts, blocks_in_file end def select_md_file(files_ = nil) opts = options - files = files_ || list_markdown_files_in_folder + files = files_ || list_markdown_files_in_path if files.count == 1 - sel = files[0] + files[0] elsif files.count >= 2 - - if opts[:preview_options] - select_per_page = 3 - files.each do |file| - fout " - #{file}" - end - else - select_per_page = SELECT_PAGE_HEIGHT - end - - prompt = TTY::Prompt.new - sel = prompt.select("#{opts.fetch(:prompt, 'Pick one')}:", files, per_page: select_per_page) + @prompt.select(opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height]) end - - sel end def summarize_block(headings, title) { headings: headings, name: title, title: title } + end + + def update_options(opts = {}, over: true) + if over + @options = @options.merge opts + else + @options.merge! opts + end + @options + end + + def write_command_file(opts, required_blocks) + return unless opts[:saved_script_filename].present? + + fne = File.basename(opts[:filename], '.*').gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join + bne = opts[:block_name].gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join + opts[:saved_script_filename] = "mde_#{Time.now.utc.strftime '%F-%H-%M-%S'}_#{fne}_#{bne}.sh" + + @options[:saved_filespec] = File.join opts[:saved_script_folder], opts[:saved_script_filename] + @execute_script_filespec = @options[:saved_filespec] + dirname = File.dirname(@options[:saved_filespec]) + Dir.mkdir dirname unless File.exist?(dirname) + File.write(@options[:saved_filespec], "#!/usr/bin/env bash\n" \ + "# file_name: #{opts[:filename]}\n" \ + "# block_name: #{opts[:block_name]}\n" \ + "# time: #{Time.now.utc}\n" \ + "#{required_blocks.flatten.join("\n")}\n") end end end