lib/markdown_exec.rb in markdown_exec-1.3.0 vs lib/markdown_exec.rb in markdown_exec-1.3.1
- old
+ new
@@ -4,79 +4,117 @@
# encoding=utf-8
require 'English'
require 'clipboard'
require 'open3'
-require 'optparse'
+# require 'optparse'
require 'shellwords'
require 'tty-prompt'
require 'yaml'
+require_relative 'cli'
require_relative 'colorize'
require_relative 'env'
+require_relative 'environment_opt_parse'
+require_relative 'object_present'
require_relative 'shared'
require_relative 'tap'
require_relative 'markdown_exec/version'
+include CLI
include Tap
+
tap_config envvar: MarkdownExec::TAP_DEBUG
$stderr.sync = true
$stdout.sync = true
BLOCK_SIZE = 1024
+class FileMissingError < StandardError; end
+
# hash with keys sorted by name
#
class Hash
def sort_by_key
keys.sort.to_h { |key| [key, self[key]] }
end
end
-# is the value a non-empty string or a binary?
+# stdout manager
#
-# :reek:ManualDispatch ### temp
-class Object
- def present?
- case self.class.to_s
- when 'FalseClass', 'TrueClass'
- true
- else
- self && (!respond_to?(:blank?) || !blank?)
- end
+module FOUT
+ # standard output; not for debug
+ #
+ def fout(str)
+ puts str
end
-end
-# is value empty?
-#
-class String
- BLANK_RE = /\A[[:space:]]*\z/.freeze
- def blank?
- empty? || BLANK_RE.match?(self)
+ def fout_list(str)
+ puts str
end
+
+ def fout_section(name, data)
+ puts "# #{name}"
+ puts data.to_yaml
+ end
+
+ def approved_fout?(level)
+ level <= @options[:display_level]
+ 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
+ end
end
public
# execute markdown documents
#
module MarkdownExec
# :reek:IrresponsibleModule
class Error < StandardError; end
+ # cache lines in text file
+ #
+ class CFile
+ def initialize
+ @cache = {}
+ end
+
+ def readlines(filename)
+ if @cache[filename]
+ @cache[filename].each do |line|
+ yield line if block_given?
+ end
+ else
+ lines = []
+ File.readlines(filename).each do |line|
+ lines.push line
+ yield line if block_given?
+ end
+ @cache[filename] = lines
+ end
+ end
+ end # class CFile
+
## an imported markdown document
#
class MDoc
def initialize(table)
@table = table
end
def collect_recursively_required_code(name)
get_required_blocks(name)
.map do |block|
- block.tap_inspect name: :block, format: :yaml
+ block.tap_yaml name: :block
body = block[:body].join("\n")
if block[:cann]
xcall = block[:cann][1..-2].tap_inspect name: :xcall
mstdin = xcall.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/).tap_inspect name: :mstdin
@@ -104,16 +142,16 @@
end
else
block[:body]
end
end.flatten(1)
- .tap_inspect format: :yaml
+ .tap_yaml
end
def get_block_by_name(name, default = {})
name.tap_inspect name: :name
- @table.select { |block| block[:name] == name }.fetch(0, default).tap_inspect format: :yaml
+ @table.select { |block| block[:name] == name }.fetch(0, default).tap_yaml
end
def get_required_blocks(name)
name_block = get_block_by_name(name)
raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
@@ -123,23 +161,23 @@
# in order of appearance in document
sel = @table.select { |block| all.include? block[:name] }
# insert function blocks
sel.map do |block|
- block.tap_inspect name: :block, format: :yaml
+ block.tap_yaml name: :block
if (call = block[:call])
[get_block_by_name("[#{call.match(/^\((\S+) |\)/)[1]}]").merge({ cann: call })]
else
[]
end + [block]
- end.flatten(1) # .tap_inspect format: :yaml
+ end.flatten(1) # .tap_yaml
end
# :reek:UtilityFunction
def hide_menu_block_per_options(opts, block)
(opts[:hide_blocks_by_name] &&
- block[:name].match(Regexp.new(opts[:block_name_excluded_match]))).tap_inspect
+ block[:name].match(Regexp.new(opts[:block_name_hidden_match]))).tap_inspect
end
def blocks_for_menu(opts)
if opts[:hide_blocks_by_name]
@table.reject { |block| hide_menu_block_per_options opts, block }
@@ -159,11 +197,11 @@
get_block_by_name(req).fetch(:reqs, [])
end
.compact
.flatten(1)
end
- all.tap_inspect format: :yaml
+ all.tap_yaml
end
end # class MDoc
# format option defaults and values
#
@@ -295,79 +333,79 @@
# :reek:TooManyInstanceVariables ### temp
# :reek:TooManyMethods ### temp
class MarkParse
attr_reader :options
+ include FOUT
+
def initialize(options = {})
@options = options
- @prompt = TTY::Prompt.new(interrupt: :exit)
+ @prompt = TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
+ # @prompt = TTY::Prompt.new(interrupt: :exit, symbols: { cross: options[:menu_divider_symbol] })
@execute_aborted_at = nil
@execute_completed_at = nil
@execute_error = nil
@execute_error_message = nil
@execute_files = nil
@execute_options = nil
@execute_script_filespec = nil
@execute_started_at = nil
@option_parser = nil
+ @cfile = CFile.new
end
##
# options necessary to start, parse input, defaults for cli options
def base_options
menu_iter do |item|
- # noisy item.tap_inspect name: :item, format: :yaml
+ # noisy item.tap_yaml name: :item
next unless item[:opt_name].present?
item_default = item[:default]
# noisy item_default.tap_inspect name: :item_default
value = if item_default.nil?
item_default
else
env_str(item[:env_var], default: OptionValue.new(item_default).for_hash)
end
- [item[:opt_name], item[:proc1] ? item[:proc1].call(value) : value]
+ [item[:opt_name], item[:proccode] ? item[:proccode].call(value) : value]
end.compact.to_h.merge(
{
menu_exit_at_top: true,
menu_with_exit: true
}
- ).tap_inspect format: :yaml
+ ).tap_yaml
end
def default_options
{
bash: true, # bash block parsing in get_block_summary()
exclude_expect_blocks: true,
hide_blocks_by_name: true,
output_saved_script_filename: false,
- prompt_approve_block: 'Process?',
- prompt_select_block: 'Choose a block:',
- prompt_select_md: 'Choose a file:',
- prompt_select_output: 'Choose a file:',
saved_script_filename: nil, # calculated
struct: true # allow get_block_summary()
}
end
def approve_block(opts, mdoc)
required_blocks = mdoc.collect_recursively_required_code(opts[:block_name])
- display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
+ display_required_code(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
allow = true
if opts[:user_must_approve]
loop do
(sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
menu.default 1
# menu.enum '.'
# menu.filter true
- menu.choice 'Yes', 1
- menu.choice 'No', 2
- menu.choice 'Copy script to clipboard', 3
- menu.choice 'Save script', 4
+ 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).tap_inspect name: :sel
allow = (sel == 1)
if sel == 3
text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
Clipboard.copy(text)
@@ -395,80 +433,98 @@
end
selected[:name]
end
+ # def cc(str)
+ # puts " - - - #{Process.clock_gettime(Process::CLOCK_MONOTONIC)} - #{str}"
+ # end
+
# :reek:DuplicateMethodCall
# :reek:UncommunicativeVariableName { exclude: [ e ] }
# :reek:LongYieldList
def command_execute(opts, command)
+ # dd = lambda { |s| puts 'command_execute() ' + s }
+ #d 'execute command and yield outputs'
@execute_files = Hash.new([])
@execute_options = opts
@execute_started_at = Time.now.utc
Open3.popen3(@options[:shell], '-c', command) 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
- # thread killed, do nothing
+ #d 'stdout IOError, thread killed, do nothing'
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
- # thread killed, do nothing
+ #d 'stderr IOError, thread killed, do nothing'
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
+ #d 'stdin error, thread killed, do nothing'
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'
end
+ #d 'command completed'
@execute_completed_at = Time.now.utc
rescue Errno::ENOENT => e
- # error triggered by missing command in script
+ #d 'command error ENOENT triggered by missing command in script'
@execute_aborted_at = Time.now.utc
@execute_error_message = e.message
@execute_error = e
@execute_files[EF_STDERR] += [@execute_error_message]
fout "Error ENOENT: #{e.inspect}"
rescue SignalException => e
- # SIGTERM triggered by user or system
+ #d 'command SIGTERM triggered by user or system'
@execute_aborted_at = Time.now.utc
@execute_error_message = 'SIGTERM'
@execute_error = e
@execute_files[EF_STDERR] += [@execute_error_message]
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|
+ @cfile.readlines(@options[:filename]).each do |line|
cnt += 1 if line.match(fenced_start_and_end_match)
end
cnt / 2
end
# :reek:DuplicateMethodCall
- def display_command(_opts, required_blocks)
- frame = ' #=#=#'.yellow
+ def display_required_code(opts, required_blocks)
+ frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
fout frame
required_blocks.each { |cb| fout cb }
fout frame
end
@@ -521,25 +577,10 @@
struct: true
)
fout "saved_filespec: #{@execute_script_filespec}" if @options[:output_saved_script_filename]
end
- # standard output; not for debug
- #
- def fout(str)
- puts str
- end
-
- def fout_list(str)
- puts str
- end
-
- def fout_section(name, data)
- puts "# #{name}"
- puts data.to_yaml
- end
-
# :reek:LongParameterList
def get_block_summary(call_options = {}, headings:, block_title:, block_body:)
opts = optsmerge call_options
return [block_body] unless opts[:struct]
return [summarize_block(headings, block_title).merge({ body: block_body })] unless opts[:bash]
@@ -559,25 +600,13 @@
title = bm && bm[1] ? bm[:title] : titlexcall
[summarize_block(headings, title).merge({ body: block_body,
call: call,
reqs: reqs,
stdin: stdin,
- stdout: stdout })].tap_inspect format: :yaml
+ stdout: stdout })].tap_yaml
end
- def approved_fout?(level)
- level <= @options[:display_level]
- 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
- end
-
# :reek:DuplicateMethodCall
# :reek:LongYieldList
def iter_blocks_in_file(opts = {})
# opts = optsmerge call_options, options_block
@@ -598,11 +627,11 @@
headings = []
in_block = false
selected_messages = yield :filter
- File.readlines(opts[:filename]).each do |line|
+ @cfile.readlines(opts[:filename]).each do |line|
continue unless line
if opts[:menu_blocks_with_headings]
if (lm = line.match(Regexp.new(opts[:heading3_match])))
headings = [headings[0], headings[1], lm[:name]]
@@ -654,23 +683,36 @@
def list_blocks_in_file(call_options = {}, &options_block)
opts = optsmerge call_options, options_block
blocks = []
+ if opts[:menu_initial_divider].present?
+ blocks += [{
+ name: format(opts[:menu_divider_format],
+ opts[:menu_initial_divider]).send(opts[:menu_divider_color].to_sym), disabled: ''
+ }]
+ end
iter_blocks_in_file(opts) do |btype, headings, block_title, body|
case btype
when :filter
%i[blocks line]
when :line
if opts[:menu_divider_match] && (mbody = body.match opts[:menu_divider_match])
- blocks += [{ name: (opts[:menu_divider_format] % mbody[:name]), disabled: '' }]
+ blocks += [{ name: format(opts[:menu_divider_format], mbody[:name]).send(opts[:menu_divider_color].to_sym),
+ disabled: '' }]
end
when :blocks
blocks += get_block_summary opts, headings: headings, block_title: block_title, block_body: body
end
end
- blocks.tap_inspect format: :yaml
+ if opts[:menu_divider_format].present? && opts[:menu_final_divider].present?
+ blocks += [{
+ name: format(opts[:menu_divider_format],
+ opts[:menu_final_divider]).send(opts[:menu_divider_color].to_sym), disabled: ''
+ }]
+ end
+ blocks.tap_yaml
end
def list_default_env
menu_iter do |item|
next unless item[:env_var].present?
@@ -704,26 +746,34 @@
# :reek:LongParameterList
def list_files_specified(specified_filename: nil, specified_folder: nil,
default_filename: nil, default_folder: nil, filetree: nil)
fn = File.join(if specified_filename&.present?
- if specified_folder&.present?
- [specified_folder, specified_filename]
- elsif specified_filename.start_with? '/'
+ # puts ' LFS 01'
+ if specified_filename.start_with? '/'
+ # puts ' LFS 02'
[specified_filename]
+ elsif specified_folder&.present?
+ # puts ' LFS 03'
+ [specified_folder, specified_filename]
else
+ # puts ' LFS 04'
[default_folder, specified_filename]
end
elsif specified_folder&.present?
+ # puts ' LFS 05'
if filetree
+ # puts ' LFS 06'
[specified_folder, @options[:md_filename_match]]
else
+ # puts ' LFS 07'
[specified_folder, @options[:md_filename_glob]]
end
else
+ # puts ' LFS 08'
[default_folder, default_filename]
- end)
+ end).tap_inspect name: :fn
if filetree
filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) }
else
Dir.glob(fn)
end.tap_inspect
@@ -769,47 +819,47 @@
def menu_for_optparse
menu_from_yaml.map do |menu_item|
menu_item.merge(
{
opt_name: menu_item[:opt_name]&.to_sym,
- proc1: case menu_item[:proc1]
- when 'debug'
- lambda { |value|
- tap_config value: value
- }
- when 'exit'
- lambda { |_|
- exit
- }
- when 'help'
- lambda { |_|
- fout menu_help
- exit
- }
- when 'path'
- lambda { |value|
- read_configuration_file! options, value
- }
- when 'show_config'
- lambda { |_|
- options_finalize options
- fout options.sort_by_key.to_yaml
- }
- when 'val_as_bool'
- ->(value) { value.class.to_s == 'String' ? (value.chomp != '0') : value }
- when 'val_as_int'
- ->(value) { value.to_i }
- when 'val_as_str'
- ->(value) { value.to_s }
- when 'version'
- lambda { |_|
- fout MarkdownExec::VERSION
- exit
- }
- else
- menu_item[:proc1]
- end
+ proccode: case menu_item[:procname]
+ when 'debug'
+ lambda { |value|
+ tap_config value: value
+ }
+ when 'exit'
+ lambda { |_|
+ exit
+ }
+ when 'help'
+ lambda { |_|
+ fout menu_help
+ exit
+ }
+ when 'path'
+ lambda { |value|
+ read_configuration_file! options, value
+ }
+ when 'show_config'
+ lambda { |_|
+ options_finalize options
+ fout options.sort_by_key.to_yaml
+ }
+ when 'val_as_bool'
+ ->(value) { value.class.to_s == 'String' ? (value.chomp != '0') : value }
+ when 'val_as_int'
+ ->(value) { value.to_i }
+ when 'val_as_str'
+ ->(value) { value.to_s }
+ when 'version'
+ lambda { |_|
+ fout MarkdownExec::VERSION
+ exit
+ }
+ else
+ menu_item[:procname]
+ end
}
)
end
end
@@ -827,21 +877,48 @@
when :blocks
summ = get_block_summary options, headings: headings, block_title: block_title, block_body: body
menu += [summ[0][:name]]
end
end
- menu.tap_inspect format: :yaml
+ menu.tap_yaml
end
def menu_iter(data = menu_for_optparse, &block)
data.map(&block)
end
def menu_help
@option_parser.help
end
+ def menu_option_append(opts, options, item)
+ return unless item[:long_name].present? || item[:short_name].present?
+
+ opts.on(*[
+ # long name
+ if item[:long_name].present?
+ "--#{item[:long_name]}#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
+ end,
+
+ # short name
+ item[:short_name].present? ? "-#{item[:short_name]}" : nil,
+
+ # description and default
+ [item[:description],
+ item[:default].present? ? "[#{value_for_cli item[:default]}]" : nil].compact.join(' '),
+
+ # apply proccode, if present, to value
+ # save value to options hash if option is named
+ #
+ lambda { |value|
+ (item[:proccode] ? item[:proccode].call(value) : value).tap do |converted|
+ options[item[:opt_name]] = converted if item[:opt_name]
+ end
+ }
+ ].compact)
+ end
+
## post-parse options configuration
#
def options_finalize(rest)
## position 0: file or folder (optional)
#
@@ -849,11 +926,11 @@
if Dir.exist?(pos)
@options[:path] = pos
elsif File.exist?(pos)
@options[:filename] = pos
else
- raise "Invalid parameter: #{pos}"
+ raise FileMissingError, pos, caller
end
end
## position 1: block name (optional)
#
@@ -923,10 +1000,25 @@
.transform_keys(&:to_sym))
end
# :reek:NestedIterators
def run
+ # eop = EnvironmentOptParse.new(
+ # menu: File.join(File.expand_path(__dir__), 'menu.yml'),
+ # options: {
+ # menu_exit_at_top: true,
+ # menu_with_exit: true
+ # }
+ # ).tap_yaml '** eop'
+ # # eop = EnvironmentOptParse.new(menu: 'lib/menu.yml', options: ".#{MarkdownExec::APP_NAME.downcase}.yml", version: MarkdownExec::VERSION).tap_yaml '** eop'
+ # eop.options.tap_inspect 'eop.options'
+ # eop.remainder.tap_inspect 'eop.remainder'
+
+ # exec_block eop.options, eop.options[:block_name]
+
+ # return
+
## default configuration
#
@options = base_options
## read local configuration file
@@ -940,33 +1032,24 @@
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
"Usage: #{executable_name} [(path | filename [block_name])] [options]"
].join("\n")
menu_iter do |item|
- next unless item[:long_name].present? || item[:short_name].present?
-
- opts.on(*[if item[:long_name].present?
- "--#{item[:long_name]}#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
- end,
- item[:short_name].present? ? "-#{item[:short_name]}" : nil,
- [item[:description],
- item[:default].present? ? "[#{value_for_cli item[:default]}]" : nil].compact.join(' '),
- lambda { |value|
- # ret = item[:proc1].call(value)
- ret = item[:proc1] ? item[:proc1].call(value) : value
- options[item[:opt_name]] = ret if item[:opt_name]
- ret
- }].compact)
+ item.tap_yaml '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.
rest = option_parser.parse! # (into: options)
- options_finalize rest
-
- exec_block options, options[:block_name]
+ begin
+ options_finalize rest
+ exec_block options, options[:block_name]
+ rescue FileMissingError => e
+ puts "File missing: #{e}"
+ end
end
def saved_name_split(name)
mf = name.match(/#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/)
return unless mf
@@ -1011,11 +1094,11 @@
end
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)).tap_inspect name: :blocks_in_file
- mdoc = MDoc.new(blocks_in_file) { |nopts| opts.merge!(nopts).tap_inspect name: :infiled_opts, format: :yaml }
+ mdoc = MDoc.new(blocks_in_file) { |nopts| opts.merge!(nopts).tap_yaml name: :infiled_opts }
blocks_menu = mdoc.blocks_for_menu(opts.merge(struct: true)).tap_inspect name: :blocks_menu
repeat_menu = true && !opts[:block_name].present?
loop do
unless opts[:block_name].present?
@@ -1095,11 +1178,11 @@
{ headings: headings, name: title, title: title }
end
def menu_export(data = menu_for_optparse)
data.map do |item|
- item.delete(:proc1)
+ item.delete(:procname)
item
end.to_yaml
end
def tab_completions(data = menu_for_optparse)
@@ -1114,10 +1197,10 @@
if over
@options = @options.merge opts
else
@options.merge! opts
end
- @options.tap_inspect format: :yaml
+ @options.tap_yaml
end
def write_command_file(call_options, required_blocks)
return unless call_options[:save_executed_script]