lib/markdown_exec.rb in markdown_exec-0.1.3 vs lib/markdown_exec.rb in markdown_exec-0.2.0

- old
+ new

@@ -1,23 +1,38 @@ #!/usr/bin/env ruby # frozen_string_literal: true # encoding=utf-8 -# rubocop:disable Style/GlobalVars $pdebug = !(ENV['MARKDOWN_EXEC_DEBUG'] || '').empty? require 'open3' require 'optparse' -# require 'pathname' require 'tty-prompt' require 'yaml' + 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 +end + +class String # rubocop:disable Style/Documentation + BLANK_RE = /\A[[:space:]]*\z/.freeze + def blank? + empty? || BLANK_RE.match?(self) + end +end + module MarkdownExec class Error < StandardError; end ## # @@ -26,125 +41,99 @@ def initialize(options = {}) @options = options end - def count_blocks + # Returns true if all files are EOF + # + def all_at_eof(files) + files.find { |f| !f.eof }.nil? + end + + def count_blocks_in_filename cnt = 0 - File.readlines(options[:mdfilename]).each do |line| + File.readlines(options[:filename]).each do |line| cnt += 1 if line.match(/^```/) end cnt / 2 end - def find_files - puts "pwd: #{`pwd`}" if $pdebug - # `ls -1 *.md`.split("\n").tap { |ret| puts "find_files() ret: #{ret.inspect}" if $pdebug } - `ls -1 #{File.join options[:mdfolder], '*.md'}`.split("\n").tap do |ret| - puts "find_files() ret: #{ret.inspect}" if $pdebug - end - end - def fout(str) puts str # to stdout end - def copts(call_options = {}, options_block = nil) - class_call_options = options.merge(call_options || {}) - if options_block - options_block.call class_call_options - else - class_call_options - end.tap { |ret| puts "copts() ret: #{ret.inspect}" if $pdebug } + def get_block_by_name(table, name, default = {}) + table.select { |block| block[:name] == name }.fetch(0, default) end - def bsr(headings, title) - # puts "bsr() headings: #{headings.inspect}" - { headings: headings, name: title, title: title } - end - - def block_summary(opts, headings, block_title, current) - puts "block_summary() block_title: #{block_title.inspect}" if $pdebug + def get_block_summary(opts, headings, block_title, current) return [current] unless opts[:struct] - # return [{ body: current, name: block_title, title: block_title }] unless opts[:bash] - return [bsr(headings, block_title).merge({ body: current })] unless opts[:bash] + 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..] } - if $pdebug - puts ["block_summary() bm: #{bm.inspect}", - "block_summary() reqs: #{reqs.inspect}"] - end - if bm && bm[1] - # [{ body: current, name: bm[1], reqs: reqs, title: bm[1] }] - [bsr(headings, bm[1]).merge({ body: current, reqs: reqs })] + [summarize_block(headings, bm[1]).merge({ body: current, reqs: reqs })] else - # [{ body: current, name: block_title, reqs: reqs, title: block_title }] - [bsr(headings, block_title).merge({ body: current, reqs: reqs })] + [summarize_block(headings, block_title).merge({ body: current, reqs: reqs })] end end - def get_blocks(call_options = {}, &options_block) - opts = copts call_options, options_block + def list_blocks_in_file(call_options = {}, &options_block) + opts = optsmerge call_options, options_block + unless opts[:filename]&.present? + fout 'No blocks found.' + exit 1 + end + blocks = [] current = nil in_block = false block_title = '' headings = [] - File.readlines(opts[:mdfilename]).each do |line| - puts "get_blocks() line: #{line.inspect}" if $pdebug + 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]] end - puts "get_blocks() headings: #{headings.inspect}" if $pdebug end if line.match(/^`{3,}/) if in_block - puts 'get_blocks() in_block' if $pdebug if current - # block_title ||= current.join(' ').gsub(/ +/, ' ')[0..64] block_title = current.join(' ').gsub(/ +/, ' ')[0..64] if block_title.nil? || block_title.empty? - blocks += block_summary opts, headings, block_title, current + blocks += get_block_summary opts, headings, block_title, current current = nil end in_block = false block_title = '' else ## new block # - # lm = line.match(/^`{3,}([^`\s]+)( .+)?$/) lm = line.match(/^`{3,}([^`\s]*) *(.*)$/) do1 = false if opts[:bash_only] do1 = true if lm && (lm[1] == 'bash') elsif opts[:exclude_expect_blocks] do1 = true unless lm && (lm[1] == 'expect') else do1 = true end - if $pdebug - puts ["get_blocks() lm: #{lm.inspect}", - "get_blocks() opts: #{opts.inspect}", - "get_blocks() do1: #{do1}"] - end if do1 && (!opts[:title_match] || (lm && lm[2] && lm[2].match(opts[:title_match]))) current = [] in_block = true block_title = (lm && lm[2]) @@ -153,277 +142,161 @@ end elsif current current += [line.chomp] end end - blocks.tap { |ret| puts "get_blocks() ret: #{ret.inspect}" if $pdebug } - end # get_blocks + blocks.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug } + end - def make_block_label(block, call_options = {}) - opts = options.merge(call_options) - puts "make_block_label() opts: #{opts.inspect}" if $pdebug - puts "make_block_label() block: #{block.inspect}" if $pdebug - if opts[:mdheadings] - heads = block.fetch(:headings, []).compact.join(' # ') - "#{block[:title]} [#{heads}] (#{opts[:mdfilename]})" + 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) else - "#{block[:title]} (#{opts[:mdfilename]})" + list_files_specified(nil, options[:folder], default_filename, default_folder) end end - def make_block_labels(call_options = {}) - opts = options.merge(call_options) - get_blocks(opts).map do |block| - make_block_label block, opts - end - end - - def select_block(call_options = {}, &options_block) - opts = copts call_options, options_block - - blocks = get_blocks(opts.merge(struct: true)) - puts "select_block() blocks: #{blocks.to_yaml}" if $pdebug - - prompt = TTY::Prompt.new(interrupt: :exit) - pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:" - puts "select_block() pt: #{pt.inspect}" if $pdebug - - blocks.each { |block| block.merge! label: make_block_label(block, opts) } - block_labels = blocks.map { |block| block[:label] } - puts "select_block() block_labels: #{block_labels.inspect}" if $pdebug - - if opts[:preview_options] - select_per_page = 3 - block_labels.each do |bn| - fout " - #{bn}" - 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 + if filetree + filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) } else - select_per_page = SELECT_PAGE_HEIGHT - end - - return nil if block_labels.count.zero? - - sel = prompt.select(pt, block_labels, per_page: select_per_page) - puts "select_block() sel: #{sel.inspect}" if $pdebug - # catch - # # catch TTY::Reader::InputInterrupt - # puts "InputInterrupt" - # end - - label_block = blocks.select { |block| block[:label] == sel }.fetch(0, nil) - puts "select_block() label_block: #{label_block.inspect}" if $pdebug - sel = label_block[:name] - puts "select_block() sel: #{sel.inspect}" if $pdebug - - cbs = code_blocks(blocks, sel) - puts "select_block() cbs: #{cbs.inspect}" if $pdebug - - ## 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 = block_by_name blocks, sel - puts "select_block() selected: #{selected.inspect}" if $pdebug - if allow && opts[:execute] - - ## process in script, to handle line continuations - # - cmd2 = cbs.flatten.join("\n") - fout "$ #{cmd2.to_yaml}" - - # Open3.popen3(cmd2) do |stdin, stdout, stderr, wait_thr| - # cnt += 1 - # # stdin.puts "This is sent to the command" - # # stdin.close # we're done - # stdout_str = stdout.read # read stdout to string. note that this will block until the command is done! - # stderr_str = stderr.read # read stderr to string - # status = wait_thr.value # will block until the command finishes; returns status that responds to .success? - # fout "#{stdout_str}" - # fout "#{cnt}: err: #{stderr_str}" if stderr_str != '' - # # fout "#{cnt}: stt: #{status}" - # end - - Open3.popen3(cmd2) do |stdin, stdout, stderr| - stdin.close_write - begin - files = [stdout, stderr] - - until all_eof(files) - ready = IO.select(files) - - next unless ready - - readable = ready[0] - # writable = ready[1] - # exceptions = ready[2] - - readable.each do |f| - # fileno = f.fileno - - data = f.read_nonblock(BLOCK_SIZE) - # fout "- fileno: #{fileno}\n#{data}" - fout data - rescue EOFError #=> e - # fout "fileno: #{fileno} EOF" - end - end - rescue IOError => e - fout "IOError: #{e}" - end - end - end - - selected[:name] - end # select_block - - def select_md_file - opts = options - files = find_files - if files.count == 1 - sel = 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) - end - - sel + Dir.glob(fn) + end.tap { |ret| puts "list_files_specified() ret: #{ret.inspect}" if $pdebug } end - # Returns true if all files are EOF - # - def all_eof(files) - files.find { |f| !f.eof }.nil? + def list_markdown_files_in_folder + Dir.glob(File.join(options[:folder], '*.md')) end def code(table, block) - puts "code() table: #{table.inspect}" if $pdebug - puts "code() block: #{block.inspect}" if $pdebug - all = [block[:name]] + unroll(table, block[:reqs]) - puts "code() all: #{all.inspect}" if $pdebug + all = [block[:name]] + recursively_required(table, block[:reqs]) all.reverse.map do |req| - puts "code() req: #{req.inspect}" if $pdebug - block_by_name(table, req).fetch(:body, '') + get_block_by_name(table, req).fetch(:body, '') end .flatten(1) .tap { |ret| puts "code() ret: #{ret.inspect}" if $pdebug } end - def block_by_name(table, name, default = {}) - table.select { |block| block[:name] == name }.fetch(0, default) - end + def list_recursively_required_blocks(table, name) + name_block = get_block_by_name(table, name) + all = [name_block[:name]] + recursively_required(table, name_block[:reqs]) - def code_blocks(table, name) - puts "code_blocks() table: #{table.inspect}" if $pdebug - puts "code_blocks() name: #{name.inspect}" if $pdebug - name_block = block_by_name(table, name) - puts "code_blocks() name_block: #{name_block.inspect}" if $pdebug - all = [name_block[:name]] + unroll(table, name_block[:reqs]) - puts "code_blocks() all: #{all.inspect}" if $pdebug - # in order of appearance in document table.select { |block| all.include? block[:name] } .map { |block| block.fetch(:body, '') } .flatten(1) - .tap { |ret| puts "code_blocks() ret: #{ret.inspect}" if $pdebug } + .tap { |ret| puts "list_recursively_required_blocks() ret: #{ret.inspect}" if $pdebug } end - def unroll(table, reqs) - puts "unroll() table: #{table.inspect}" if $pdebug - puts "unroll() reqs: #{reqs.inspect}" if $pdebug - all = [] - rem = reqs - while rem.count.positive? - puts "unrol() rem: #{rem.inspect}" if $pdebug - rem = rem.map do |req| - puts "unrol() req: #{req.inspect}" if $pdebug - next if all.include? req + def make_block_label(block, call_options = {}) + opts = options.merge(call_options) + if opts[:mdheadings] + heads = block.fetch(:headings, []).compact.join(' # ') + "#{block[:title]} [#{heads}] (#{opts[:filename]})" + else + "#{block[:title]} (#{opts[:filename]})" + end + end - all += [req] - puts "unrol() all: #{all.inspect}" if $pdebug - block_by_name(table, req).fetch(:reqs, []) - end - .compact - .flatten(1) - .tap { |_ret| puts "unroll() rem: #{rem.inspect}" if $pdebug } + def make_block_labels(call_options = {}) + opts = options.merge(call_options) + list_blocks_in_file(opts).map do |block| + make_block_label block, opts end - all.tap { |ret| puts "unroll() ret: #{ret.inspect}" if $pdebug } end - # $stderr.sync = true - # $stdout.sync = true + def optsmerge(call_options = {}, options_block = nil) + 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 - ## configuration file - # - def read_configuration!(options, configuration_path) + 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 end + def recursively_required(table, reqs) + all = [] + rem = reqs + while rem.count.positive? + rem = rem.map do |req| + next if all.include? req + + all += [req] + get_block_by_name(table, req).fetch(:reqs, []) + end + .compact + .flatten(1) + .tap { |_ret| puts "recursively_required() rem: #{rem.inspect}" if $pdebug } + end + all.tap { |ret| puts "recursively_required() ret: #{ret.inspect}" if $pdebug } + end + def run ## default configuration # options = { mdheadings: true, list_blocks: false, - list_docs: false, - mdfilename: 'README.md', - mdfolder: '.' + list_docs: false } - def options_finalize!(options); end + ## post-parse options configuration + # + options_finalize = ->(_options) {} - # puts "MDE run() ARGV: #{ARGV.inspect}" - # read local configuration file # - read_configuration! options, ".#{MarkdownExec::APP_NAME.downcase}.yml" + read_configuration_file! options, ".#{MarkdownExec::APP_NAME.downcase}.yml" - ## read current details for aws resources from app_data_file - # - # load_resources! options - # puts "q31 options: #{options.to_yaml}" if $pdebug - - # rubocop:disable Metrics/BlockLength 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} [options]" ].join("\n") - ## menu top: on_head appear in reverse order added + ## menu top: items appear in reverse order added # opts.on('--config PATH', 'Read configuration file') do |value| - read_configuration! options, value + read_configuration_file! options, value end ## menu body: items appear in order added # - opts.on('-f RELATIVE', '--mdfilename', 'Name of document') do |value| - options[:mdfilename] = value + opts.on('-f RELATIVE', '--filename', 'Name of document') do |value| + options[:filename] = value end - opts.on('-p PATH', '--mdfolder', 'Path to documents') do |value| - options[:mdfolder] = value + opts.on('-p PATH', '--folder', 'Path to documents') do |value| + options[:folder] = value end opts.on('--list-blocks', 'List blocks') do |_value| options[:list_blocks] = true end @@ -433,87 +306,164 @@ end ## menu bottom: items appear in order added # opts.on_tail('-h', '--help', 'App help') do |_value| - puts option_parser.help + fout option_parser.help exit end opts.on_tail('-v', '--version', 'App version') do |_value| - puts MarkdownExec::VERSION + 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! options - puts options.to_yaml + options_finalize.call options + fout options.to_yaml end - end # OptionParser - # rubocop:enable Metrics/BlockLength - + 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. option_parser.parse! # (into: options) - options_finalize! options + options_finalize.call options ## process # - # rubocop:disable Metrics/BlockLength - loop do # once - mp = MarkParse.new options - options.merge!( - { - approve: true, - bash: true, - display: true, - exclude_expect_blocks: true, - execute: true, - prompt: 'Execute', - struct: true - } - ) + options.merge!( + { + approve: true, + bash: true, + display: true, + exclude_expect_blocks: true, + execute: true, + prompt: 'Execute', + struct: true + } + ) + mp = MarkParse.new options - ## show - # - if options[:list_docs] - fout mp.find_files - break - end + ## show + # + if options[:list_docs] + fout mp.list_files_per_options options + return + end - if options[:list_blocks] - fout (mp.find_files.map do |file| - mp.make_block_labels(mdfilename: file, struct: true) - end).flatten(1).to_yaml - break + 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 + ) + end + + def select_block(call_options = {}, &options_block) + opts = optsmerge call_options, options_block + + blocks = list_blocks_in_file(opts.merge(struct: true)) + + prompt = TTY::Prompt.new(interrupt: :exit) + pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:" + + blocks.each { |block| block.merge! label: make_block_label(block, opts) } + block_labels = 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 + end - ## process + 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] + + ## process in script, to handle line continuations # - mp.select_block(bash: true, struct: true) if options[:mdfilename] + cmd2 = cbs.flatten.join("\n") -# rubocop:disable Style/BlockComments -=begin - # rescue ArgumentError => e - # puts "User abort: #{e}" + Open3.popen3(cmd2) do |stdin, stdout, stderr| + stdin.close_write + begin + files = [stdout, stderr] - # rescue StandardError => e - # puts "ERROR: #{e}" - # raise StandardError, e + until all_at_eof(files) + ready = IO.select(files) - # ensure - # exit -=end - # rubocop:enable Style/BlockComments + next unless ready - break unless false # rubocop:disable Lint/LiteralAsCondition - end # loop - end # run - end # class MarkParse + readable = ready[0] + # writable = ready[1] + # exceptions = ready[2] - # rubocop:enable Metrics/BlockLength - # rubocop:enable Style/GlobalVars -end # module MarkdownExec + 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] + end + + def select_md_file(files_ = nil) + opts = options + files = files_ || list_markdown_files_in_folder + if files.count == 1 + sel = 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) + end + + sel + end + + def summarize_block(headings, title) + { headings: headings, name: title, title: title } + end + end +end