lib/markdown_exec.rb in markdown_exec-0.2.4 vs lib/markdown_exec.rb in markdown_exec-0.2.5

- old
+ new

@@ -12,25 +12,27 @@ # default if nil # false if empty or '0' # else true def env_bool(name, default: false) - return default if (val = ENV[name]).nil? + return default if name.nil? || (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 name.nil? || (val = ENV[name]).nil? return default if val.empty? val.to_i end def env_str(name, default: '') - ENV[name] || default + return default if name.nil? || (val = ENV[name]).nil? + + val || default end $pdebug = env_bool 'MDE_DEBUG' require_relative 'markdown_exec/version' @@ -40,11 +42,16 @@ BLOCK_SIZE = 1024 class Object # rubocop:disable Style/Documentation def present? - self && !blank? + case self.class.to_s + when 'FalseClass', 'TrueClass' + true + else + self && (!respond_to?(:blank?) || !blank?) + end end end class String # rubocop:disable Style/Documentation BLANK_RE = /\A[[:space:]]*\z/.freeze @@ -92,48 +99,21 @@ ## # options necessary to start, parse input, defaults for cli options def base_options - { - # commands - list_blocks: false, # command - list_docs: false, # command - list_recent_scripts: false, # command - run_last_script: false, # command - select_recent_script: false, # command + menu_data + .map do |_long_name, _short_name, env_var, _arg_name, _description, opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists + next unless opt_name.present? - # command options - filename: env_str('MDE_FILENAME', default: nil), # option Filename to open - list_count: 16, - logged_stdout_filename_prefix: 'mde', - 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 - save_execution_output: env_bool('MDE_SAVE_EXECUTION_OUTPUT', default: false), # option - saved_script_filename_prefix: 'mde', - saved_script_folder: env_str('MDE_SAVED_SCRIPT_FOLDER', default: 'logs'), # option - saved_script_glob: 'mde_*.sh', - saved_stdout_folder: env_str('MDE_SAVED_STDOUT_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) - } + [opt_name, env_bool(env_var, default: value_for_hash(default))] + end.compact.to_h.merge( + { + mdheadings: true, # use headings (levels 1,2,3) in block lable + menu_with_exit: true + } + ).tap_inspect format: :yaml end def default_options { bash: true, # bash block parsing in get_block_summary() @@ -246,15 +226,25 @@ make_block_labels(filename: file, struct: true) end).flatten(1) return end + if @options[:list_default_yaml] + fout_list list_default_yaml + return + end + if @options[:list_docs] fout_list files return end + if @options[:list_default_env] + fout_list list_default_env + return + end + if @options[:list_recent_scripts] fout_list list_recent_scripts return end @@ -374,18 +364,41 @@ end end blocks.tap_inspect end + def list_default_env + menu_data + .map do |_long_name, _short_name, env_var, _arg_name, description, _opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists + next unless env_var.present? + + [ + "#{env_var}=#{value_for_cli default}", + description.present? ? description : nil + ].compact.join(' # ') + end.compact.sort + end + + def list_default_yaml + menu_data + .map do |_long_name, _short_name, _env_var, _arg_name, description, opt_name, default, _proc1| # rubocop:disable Metrics/ParameterLists + next unless opt_name.present? && default.present? + + [ + "#{opt_name}: #{value_for_yaml default}", + description.present? ? description : nil + ].compact.join(' # ') + end.compact.sort + end + def list_files_per_options(options) - default_filename = 'README.md' - default_folder = '.' - if options[:filename]&.present? - list_files_specified(options[:filename], options[:path], default_filename, default_folder) - else - list_files_specified(nil, options[:path], default_filename, default_folder) - end.tap_inspect + list_files_specified( + options[:filename]&.present? ? options[:filename] : nil, + options[:path], + 'README.md', + '.' + ).tap_inspect end def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil) fn = File.join(if specified_filename&.present? if specified_folder&.present? @@ -460,19 +473,125 @@ make_block_label block, opts end.compact.tap_inspect end + def menu_data + proc_self = ->(value) { value } + proc_to_i = ->(value) { value.to_i != 0 } + proc_true = ->(_) { true } + + summary_head = [ + ['config', nil, nil, 'PATH', 'Read configuration file', + nil, '.', ->(value) { read_configuration_file! options, value }], + ['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output', + nil, false, ->(value) { $pdebug = value.to_i != 0 }] + ] + + summary_body = [ + ['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document', + :filename, nil, proc_self], + ['list-blocks', nil, nil, nil, 'List blocks', + :list_blocks, nil, proc_true], + ['list-default-env', nil, nil, nil, 'List default configuration as environment variables', + :list_default_env, nil, proc_true], + ['list-default-yaml', nil, nil, nil, 'List default configuration as YAML', + :list_default_yaml, nil, proc_true], + ['list-docs', nil, nil, nil, 'List docs in current folder', + :list_docs, nil, proc_true], + ['list-recent-scripts', nil, nil, nil, 'List recent saved scripts', + :list_recent_scripts, nil, proc_true], + ['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution', + :output_execution_summary, false, proc_to_i], + ['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script prior to execution', + :output_script, false, proc_to_i], + ['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution', + :output_stdout, true, proc_to_i], + ['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents', + :path, nil, proc_self], + ['run-last-script', nil, nil, nil, 'Run most recently saved script', + :run_last_script, nil, proc_true], + ['select-recent-script', nil, nil, nil, 'Select and execute a recently saved script', + :select_recent_script, nil, proc_true], + ['save-execution-output', nil, 'MDE_SAVE_EXECUTION_OUTPUT', 'BOOL', 'Save execution output', + :save_execution_output, false, proc_to_i], + ['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script', + :save_executed_script, false, proc_to_i], + ['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder', + :saved_script_folder, 'logs', proc_self], + ['saved-stdout-folder', nil, 'MDE_SAVED_STDOUT_FOLDER', 'SPEC', 'Saved stdout folder', + :saved_stdout_folder, 'logs', proc_self], + ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause for user to approve script', + :user_must_approve, true, proc_to_i] + ] + + # rubocop:disable Style/Semicolon + summary_tail = [ + [nil, '0', nil, nil, 'Show current configuration values', + nil, nil, ->(_) { options_finalize options; fout sorted_keys(options).to_yaml }], + ['help', 'h', nil, nil, 'App help', + nil, nil, ->(_) { fout menu_help; exit }], + ['version', 'v', nil, nil, 'App version', + nil, nil, ->(_) { fout MarkdownExec::VERSION; exit }], + ['exit', 'x', nil, nil, 'Exit app', + nil, nil, ->(_) { exit }] + ] + # rubocop:enable Style/Semicolon + + env_vars = [ + [nil, nil, 'MDE_BLOCK_NAME_EXCLUDED_MATCH', nil, 'Pattern for blocks to hide from user-selection', + :block_name_excluded_match, '^\(.+\)$', nil], + [nil, nil, 'MDE_BLOCK_NAME_MATCH', nil, '', :block_name_match, ':(?<title>\S+)( |$)', nil], + [nil, nil, 'MDE_BLOCK_REQUIRED_SCAN', nil, '', :block_required_scan, '\+\S+', nil], + [nil, nil, 'MDE_FENCED_START_AND_END_MATCH', nil, '', :fenced_start_and_end_match, '^`{3,}', nil], + [nil, nil, 'MDE_FENCED_START_EX_MATCH', nil, '', :fenced_start_ex_match, + '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$', nil], + [nil, nil, 'MDE_HEADING1_MATCH', nil, '', :heading1_match, '^# *(?<name>[^#]*?) *$', nil], + [nil, nil, 'MDE_HEADING2_MATCH', nil, '', :heading2_match, '^## *(?<name>[^#]*?) *$', nil], + [nil, nil, 'MDE_HEADING3_MATCH', nil, '', :heading3_match, '^### *(?<name>.+?) *$', nil], + [nil, nil, 'MDE_MD_FILENAME_GLOB', nil, '', :md_filename_glob, '*.[Mm][Dd]', nil], + [nil, nil, 'MDE_MD_FILENAME_MATCH', nil, '', :md_filename_match, '.+\\.md', nil], + [nil, nil, 'MDE_SELECT_PAGE_HEIGHT', nil, '', :select_page_height, 12, nil] + # [nil, nil, 'MDE_', nil, '', nil, '', nil], + ] + + summary_head + summary_body + summary_tail + env_vars + end + + def menu_help + @option_parser.help + end + def option_exclude_blocks(opts, blocks) block_name_excluded_match = Regexp.new opts[:block_name_excluded_match] if opts[:hide_blocks_by_name] blocks.reject { |block| block[:name].match(block_name_excluded_match) } else blocks end end + ## post-parse options configuration + # + def options_finalize(rest) + ## 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 + + ## position 1: block name (optional) + # + @options[:block_name] = rest.fetch(1, nil) + end + def optsmerge(call_options = {}, options_block = nil) class_call_options = @options.merge(call_options || {}) if options_block options_block.call class_call_options else @@ -493,10 +612,18 @@ execute_started_at: @execute_started_at, execute_script_filespec: @execute_script_filespec } end + def prompt_with_quit(prompt_text, items, opts = {}) + exit_option = '* Exit' + sel = @prompt.select prompt_text, + items + (@options[:menu_with_exit] ? [exit_option] : []), + opts + sel == exit_option ? nil : sel + end + def read_configuration_file!(options, configuration_path) return unless File.exist?(configuration_path) # rubocop:disable Security/YAMLLoad options.merge!((YAML.load(File.open(configuration_path)) || {}) @@ -524,91 +651,32 @@ def run ## default configuration # @options = base_options - ## post-parse options configuration + ## read local configuration file # - 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" - option_parser = OptionParser.new do |opts| + @option_parser = 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} [path] [filename] [options]" ].join("\n") - 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_data + .map do |long_name, short_name, _env_var, arg_name, description, opt_name, default, proc1| # rubocop:disable Metrics/ParameterLists + next unless long_name.present? || short_name.present? - 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], - ['list-recent-scripts', nil, nil, nil, 'List recent saved scripts', - :list_recent_scripts, 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], - ['run-last-script', nil, nil, nil, 'Run most recently saved script', - :run_last_script, proc_true], - ['select-recent-script', nil, nil, nil, 'Select and execute a recently saved script', - :select_recent_script, proc_true], - ['save-execution-output', nil, 'MDE_SAVE_EXECUTION_OUTPUT', 'BOOL', 'Save execution output', - :save_execution_output, proc_to_i], - ['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], - ['saved-stdout-folder', nil, 'MDE_SAVED_STDOUT_FOLDER', 'SPEC', 'Saved stdout folder', - :saved_stdout_folder, proc_self], - ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause to approve execution', - :user_must_approve, proc_to_i] - ] - - # rubocop:disable Style/Semicolon - summary_tail = [ - [nil, '0', nil, nil, 'Show configuration', - nil, ->(_) { options_finalize.call options; fout sorted_keys(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 - - (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(*[if long_name.present? "--#{long_name}#{arg_name.present? ? " #{arg_name}" : ''}" end, short_name.present? ? "-#{short_name}" : nil, [description, - env_var.present? ? "env: #{env_var}" : nil].compact.join(' - '), + default.present? ? "[#{value_for_cli default}]" : nil].compact.join(' '), lambda { |value| ret = proc1.call(value) options[opt_name] = ret if opt_name ret }].compact) @@ -616,31 +684,13 @@ 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 + options_finalize rest - ## 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 - - ## position 1: block name (optional) - # - block_name = rest.fetch(1, nil) - - exec_block options, block_name + exec_block options, options[:block_name] end def run_last_script filename = Dir.glob(File.join(@options[:saved_script_folder], @options[:saved_script_glob])).sort[0..(options[:list_count] - 1)].last @@ -665,10 +715,16 @@ @options[:logged_stdout_filespec] = File.join @options[:saved_stdout_folder], @options[:logged_stdout_filename] @logged_stdout_filespec = @options[:logged_stdout_filespec] dirname = File.dirname(@options[:logged_stdout_filespec]) Dir.mkdir dirname unless File.exist?(dirname) File.write(@options[:logged_stdout_filespec], @execute_files&.fetch(0, '')) + # @options[:logged_stderr_filename] = + # "#{[@options[:logged_stdout_filename_prefix], Time.now.utc.strftime('%F-%H-%M-%S'), fne, + # @options[:block_name]].join('_')}.err.txt" + # @options[:logged_stderr_filespec] = File.join @options[:saved_stdout_folder], @options[:logged_stderr_filename] + # @logged_stderr_filespec = @options[:logged_stderr_filespec] + # File.write(@options[:logged_stderr_filespec], @execute_files&.fetch(1, '')) 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)) @@ -678,11 +734,13 @@ 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] } return nil if block_labels.count.zero? - sel = @prompt.select pt, block_labels, per_page: opts[:select_page_height] + sel = prompt_with_quit pt, block_labels, per_page: opts[:select_page_height] + return nil if sel.nil? + label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil) opts[:block_name] = @options[:block_name] = label_block[:name] end approve_block opts, blocks_in_file @@ -692,17 +750,19 @@ opts = options files = files_ || list_markdown_files_in_path if files.count == 1 files[0] elsif files.count >= 2 - @prompt.select opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height] + prompt_with_quit opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height] end end def select_recent_script - filename = @prompt.select @options[:prompt_select_md].to_s, list_recent_scripts, - per_page: @options[:select_page_height] + filename = prompt_with_quit @options[:prompt_select_md].to_s, list_recent_scripts, + per_page: @options[:select_page_height] + return if filename.nil? + mf = filename.match(/#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_(?<block>.+)\.sh/) @options[:block_name] = mf[:block] @options[:filename] = "#{mf[:file]}.md" ### other extensions select_and_approve_block( @@ -726,10 +786,53 @@ if over @options = @options.merge opts else @options.merge! opts end - @options + @options.tap_inspect format: :yaml + end + + def value_for_cli(value) + case value.class.to_s + when 'String' + "'#{value}'" + when 'FalseClass', 'TrueClass' + value ? '1' : '0' + when 'Integer' + value + else + value.to_s + end + end + + def value_for_hash(value, default = nil) + return default if value.nil? + + case value.class.to_s + when 'String', 'Integer', 'FalseClass', 'TrueClass' + value + when value.empty? + default + else + value.to_s + end + end + + def value_for_yaml(value) + return default if value.nil? + + case value.class.to_s + when 'String' + "'#{value}'" + when 'Integer' + value + when 'FalseClass', 'TrueClass' + value ? true : false + when value.empty? + default + else + value.to_s + end end def write_command_file(opts, required_blocks) fne = File.basename(opts[:filename], '.*') opts[:saved_script_filename] =