require 'cl/cast' require 'cl/errors' class Cl class Opt < Struct.new(:strs, :opts, :block) include Cast, Regex OPTS = %i( alias default deprecated description downcase eg enum example format internal max min negate note required requires secret see sep type upcase ) OPT = /^--(?:\[.*\])?(.*)$/ TYPES = { int: :integer, str: :string, bool: :flag, boolean: :flag } attr_reader :short, :long def initialize(strs, *) super @short, @long = Validator.new(strs, opts).apply end def define(const) return unless __key__ = name const.send :include, Module.new { define_method (__key__) { opts[__key__] } define_method (:"#{__key__}?") { !!opts[__key__] } } end def name return @name if instance_variable_defined?(:@name) name = long.split(' ').first.match(OPT)[1] if long @name = name.sub('-', '_').to_sym if name end def type @type ||= TYPES[opts[:type]] || opts[:type] || infer_type end def infer_type strs.any? { |str| str.split(' ').size > 1 } ? :string : :flag end def help? name == :help end def flag? type == :flag end def int? type == :int || type == :integer end def array? type == :array end def aliases? !!opts[:alias] end def aliases Array(opts[:alias]) end def description opts[:description] end def deprecated?(name = nil) name ? deprecated? && deprecated.first == name : !!opts[:deprecated] end def deprecated # If it's a string then it's a deprecation message and the option itself # is considered deprecated. If it's a symbol it refers to a deprecated # alias, and the option's name is the deprecation message. return [name, opts[:deprecated]] unless opts[:deprecated].is_a?(Symbol) opts[:deprecated] ? [opts[:deprecated], name] : [] end def downcase? !!opts[:downcase] end def default? opts.key?(:default) end def default opts[:default] end def enum? !!opts[:enum] end def enum Array(opts[:enum]) end def known?(value) return value.all? { |value| known?(value) } if value.is_a?(Array) enum.any? { |obj| obj.is_a?(Regexp) ? obj =~ value.to_s : obj == value } end def unknown(value) return value.reject { |value| known?(value) } if value.is_a?(Array) known?(value) ? [] : Array(value) end def example? !!opts[:example] end def example opts[:example] end def format? !!opts[:format] end def format format_regex(opts[:format]) end def formatted?(value) return value.all? { |value| formatted?(value) } if value.is_a?(Array) opts[:format] =~ value end def internal? !!opts[:internal] end def min? int? && !!opts[:min] end def min opts[:min] end def max? int? && !!opts[:max] end def max opts[:max] end def negate? !!negate end def negate ['no'] + Array(opts[:negate]) if flag? end def note? !!opts[:note] end def note opts[:note] end def required? !!opts[:required] end def requires? !!opts[:requires] end def requires Array(opts[:requires]) end def secret? !!opts[:secret] end def see? !!opts[:see] end def see opts[:see] end def separator opts[:sep] end def upcase? !!opts[:upcase] end def block # raise if no block was given, and the option's name cannot be inferred super || method(:assign) end def assign(opts, type, _, value) [name, *aliases].each do |name| if array? opts[name] ||= [] opts[name] << value else opts[name] = value end end end def long?(str) str.start_with?('--') end class Validator < Struct.new(:strs, :opts) SHORT = /^-\w( \w+)?$/ LONG = /^--[\w\-\[\]]+( \w+)?$/ MSGS = { missing_strs: 'No option strings given. Pass one short -s and/or one --long option string.', wrong_strs: 'Wrong option strings given. Pass one short -s and/or one --long option string.', invalid_strs: 'Invalid option strings given: %p', unknown_opts: 'Unknown options: %s' } def apply error :missing_strs if strs.empty? error :wrong_strs if short.size > 1 || long.size > 1 error :invalid_strs, invalid unless invalid.empty? error :unknown_opts, unknown.map(&:inspect).join(', ') unless unknown.empty? [short.first, long.first] end def unknown @unknown ||= opts.keys - Opt::OPTS end def invalid @invalid ||= strs.-(valid).join(', ') end def valid strs.grep(Regexp.union(SHORT, LONG)) end def short strs.grep(SHORT) end def long strs.grep(LONG) end def error(key, *args) raise Cl::Error, MSGS[key] % args end end end end