require_relative 'safe/maputil' require 'optparse' require 'yaml' module EverydayCliUtils class Option class << self def add_option(options, opts, names, opt_name, settings = {}) opts.on(*names) { options[opt_name] = !settings[:toggle] || !options[opt_name] yield if block_given? } end def add_option_with_param(options, opts, names, opt_name, settings = {}) opts.on(*names, settings[:type] || String) { |param| if settings[:append] options[opt_name] << param else options[opt_name] = param end yield if block_given? } end end end class OptionType def initialize(default_value_block, value_determine_block, name_mod_block = nil, value_transform_block = nil) @default_value_block = default_value_block @value_determine_block = value_determine_block @name_mod_block = name_mod_block @value_transform_block = value_transform_block end def default_value(settings = {}) @default_value_block.call(settings) end def updated_value(current_value, new_value, settings = {}) new_value = @value_transform_block.call(new_value, settings) unless @value_transform_block.nil? @value_determine_block.call(current_value, new_value, settings) end def mod_names(names, settings = {}) @name_mod_block.call(names, settings) end end class OptionTypes class << self def def_type(type, default_value_block, value_determine_block, name_mod_block = nil, value_transform_block = nil) @types ||= {} @types[type] = OptionType.new(default_value_block, value_determine_block, name_mod_block, value_transform_block) end def default_value(type, settings = {}) @types ||= {} @types.has_key?(type) ? @types[type].default_value(settings) : nil end def updated_value(type, current_value, new_value, settings = {}) @types ||= {} @types.has_key?(type) ? @types[type].updated_value(current_value, new_value, settings) : current_value end def mod_names(type, names, settings = {}) @types ||= {} @types.has_key?(type) ? @types[type].mod_names(names, settings) : names end #region option procs def option_default(_) false end def option_value_determine(current_value, new_value, settings) new_value ? (!settings[:toggle] || !current_value) : current_value end def option_name_mod(names, settings) settings.has_key?(:desc) ? (names + [settings[:desc]]) : names end def option_value_transform(new_value, _) !(!new_value) end #endregion def def_option_type def_type(:option, method(:option_default), method(:option_value_determine), method(:option_name_mod), method(:option_value_transform)) end #region option_with_param procs def param_option_default(settings) settings[:append] ? [] : nil end def param_option_value_determine(current_value, new_value, settings) settings[:append] ? (current_value + new_value) : ((new_value.nil? || new_value == '') ? current_value : new_value) end def param_option_name_mod(names, settings) names[0] << ' PARAM' unless names.any? { |v| v.include?(' ') } names = settings.has_key?(:desc) ? (names + [settings[:desc]]) : names settings.has_key?(:type) ? (names + [settings[:type]]) : names end def param_option_value_transform(new_value, settings) new_value.is_a?(Array) ? (settings[:append] ? new_value : new_value[0]) : (settings[:append] ? [new_value] : new_value) end #endregion def def_option_with_param_type def_type(:option_with_param, method(:param_option_default), method(:param_option_value_determine), method(:param_option_name_mod), method(:param_option_value_transform)) end end def_option_type def_option_with_param_type end class OptionDef attr_reader :value, :names def initialize(type, names, settings = {}, &block) @type = type @names = names @settings = settings @block = block @value = OptionTypes.default_value(type, settings) @values = {} end def set(value) @value = value @values = {} end def update(value, layer) @values[layer] = OptionTypes.default_value(@type, @settings) unless @values.has_key?(layer) @values[layer] = OptionTypes.updated_value(@type, @values[layer], value, @settings) end def run @block.call unless @block.nil? || !@block end def composite(*layers) value = @value layers.each { |layer| value = OptionTypes.updated_value(@type, value, @values[layer], @settings) if @values.has_key?(layer) } value end class << self def register(opts, options, type, opt_name, names, settings = {}, default_settings = {}, &block) settings = EverydayCliUtils::MapUtil.extend_hash(default_settings, settings) opt = OptionDef.new(type, names.clone, settings, &block) options[opt_name] = opt names = OptionTypes.mod_names(type, names, settings) opts.on(*names) { |*args| opt.update(args, :arg) opt.run } end end end class SpecialOptionDef attr_reader :order, :settings, :names, :order attr_accessor :state def initialize(order, exit_on_action, names, print_on_exit_str, settings, action_block, pre_parse_block = nil) @order = order @exit_on_action = exit_on_action @names = names @print_on_exit_str = print_on_exit_str @settings = settings @action_block = action_block @pre_parse_block = pre_parse_block @state = false end def run(options_list) if @state @action_block.call(self, options_list) if @exit_on_action puts @print_on_exit_str unless @print_on_exit_str.nil? exit 0 end end end def run_pre_parse(options_list) @pre_parse_block.call(self, options_list) unless @pre_parse_block.nil? end class << self def register(order, opts, options, opt_name, names, exit_on_action, print_on_exit_str, settings, default_settings, action_block, pre_parse_block = nil) settings = EverydayCliUtils::MapUtil.extend_hash(default_settings, settings) opt = SpecialOptionDef.new(order, exit_on_action, names, print_on_exit_str, settings, action_block, pre_parse_block) options.special_options[opt_name] = opt names << settings[:desc] if settings.has_key?(:desc) opts.on(*names) { opt.state = true } end end end class OptionList attr_reader :opts, :special_options attr_accessor :default_settings, :help_str def initialize @options = {} @special_options = {} @default_settings = {} @opts = OptionParser.new @help_str = nil end def []=(opt_name, opt) @options[opt_name] = opt end def set(opt_name, value) @options[opt_name].set(value) if @options.has_key?(opt_name) end def set_all(opts) opts.each { |opt| set(opt[0], opt[1]) } end def update(opt_name, value, layer) @options[opt_name].update(value, layer) if @options.has_key?(opt_name) end def update_all(layer, opts) opts.each { |opt| update(opt[0], opt[1], layer) } end def register(type, opt_name, names, settings = {}, &block) OptionDef.register(@opts, self, type, opt_name, names, settings, @default_settings, &block) end def register_special(order, opt_name, names, exit_on_action, print_on_exit_str, settings, action_block, pre_parse_block = nil) SpecialOptionDef.register(order, @opts, self, opt_name, names, exit_on_action, print_on_exit_str, settings, @default_settings, action_block, pre_parse_block) end def run_special run_special_helper { |v| v[1].run(self) } end def run_special_pre_parse run_special_helper { |v| v[1].run_pre_parse(self) } end def run_special_helper(&block) @special_options.to_a.sort_by { |v| v[1].order }.each(&block) end def composite(*layers) hash = {} @options.each { |v| hash[v[0]] = v[1].composite(*layers) } hash end def help @help_str.nil? ? @opts.help : @help_str end def to_s @help_str.nil? ? @opts.to_s : @help_str end def banner=(banner) @opts.banner = banner end def parse!(argv = ARGV) @opts.parse!(argv) end def show_defaults script_defaults = composite global_defaults = composite(:global) local_defaults = composite(:global, :local) global_diff = EverydayCliUtils::MapUtil.hash_diff(global_defaults, script_defaults) local_diff = EverydayCliUtils::MapUtil.hash_diff(local_defaults, global_defaults) str = "Script Defaults:\n#{options_to_str(script_defaults)}\n" str << "Script + Global Defaults:\n#{options_to_str(global_diff)}\n" unless global_diff.empty? str << "Script + Global + Local Defaults:\n#{options_to_str(local_diff)}\n" unless local_diff.empty? str end def options_to_str(options, indent = 4) str = '' max_name_len = @options.values.map { |v| v.names.join(', ').length }.max options.each { |v| str << build_option_str(v, indent, max_name_len) } str end def build_option_str(v, indent, max_name_len) opt = @options[v[0]] val = v[1] names_str = opt.names.join(', ') "#{' ' * indent}#{names_str}#{' ' * ((max_name_len + 4) - names_str.length)}#{val_to_str(val)}\n" end def val_to_str(val) if val.nil? 'nil' elsif val.is_a?(TrueClass) 'true' elsif val.is_a?(FalseClass) 'false' elsif val.is_a?(Enumerable) "[#{val.map { |v| val_to_str(v) }.join(', ')}]" elsif val.is_a?(Numeric) val.to_s else "'#{val.to_s}'" end end end module OptionUtil def option(opt_name, names, settings = {}, &block) @options ||= OptionList.new @options.register(:option, opt_name, names, settings, &block) end def option_with_param(opt_name, names, settings = {}, &block) @options ||= OptionList.new @options.register(:option_with_param, opt_name, names, settings, &block) end def defaults_option(file_path, names, settings = {}) defaults_options_helper(file_path, names, settings, 4, :defaults, 'Defaults set', :local) end def global_defaults_option(file_path, names, settings = {}) defaults_options_helper(file_path, names, settings, 3, :global_defaults, 'Global defaults set', :global) end def defaults_options_helper(file_path, names, settings, order, opt_name, print_on_exit_string, composite_name) @options ||= OptionList.new settings[:file_path] = File.expand_path(file_path) @options.register_special(order, opt_name, names, key_absent_or_true(settings, :exit_on_save), print_on_exit_string, settings, write_defaults_proc(composite_name), read_defaults_proc(composite_name)) end def write_defaults_proc(composite_name) ->(opt, options) { IO.write(opt.settings[:file_path], options.composite(composite_name, :arg).to_yaml) } end def read_defaults_proc(composite_name) ->(opt, options) { file_path = opt.settings[:file_path] options.update_all composite_name, YAML::load_file(file_path) unless file_path_nil_or_exists?(file_path) } end def file_path_nil_or_exists?(file_path) file_path.nil? || !File.exist?(file_path) end def key_absent_or_true(settings, key) !settings.has_key?(key) || settings[key] end def show_defaults_option(names, settings = {}) show_info_helper(names, settings, 2, :show_defaults, :exit_on_show) { |_, options| puts options.show_defaults } end def help_option(names, settings = {}) show_info_helper(names, settings, 1, :help, :exit_on_print) { |_, options| puts options.help } end def show_info_helper(names, settings, order, opt_name, exit_on_sym, &block) @options ||= OptionList.new @options.register_special(order, opt_name, names, key_absent_or_true(settings, exit_on_sym), nil, settings, block) end def default_settings(settings = {}) @options ||= OptionList.new @options.default_settings = settings end def default_options(opts = {}) @options ||= OptionList.new @options.set_all(opts) end def apply_options(layer, opts = {}) @options ||= OptionList.new @options.update_all(layer, opts) end def banner(banner) @options ||= OptionList.new @options.banner = banner end def opts @options ||= OptionList.new @options.opts end def options @options ||= OptionList.new @options.composite(:global, :local, :arg) end def option_list @options ||= OptionList.new @options end def help @options ||= OptionList.new @options.help end def to_s @options ||= OptionList.new @options.to_s end def help_str=(str) @options ||= OptionList.new @options.help_str = str end def parse!(argv = ARGV) @options ||= OptionList.new @options.run_special_pre_parse @options.parse!(argv) @options.run_special end end end