require_relative '../dencli' class DenCli::CMD attr_reader :parent, :name, :description, :exe, :completion, :options, :defined_in def initialize parent, name, description, exe, defined_in raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe @parent, @name, @description, @exe, @defined_in = parent, name, description, lambda( &exe), defined_in @parameters = @exe.parameters @arguments_required = @exe.parameters.select {|e| :req == e[0] }.map {|e| e[1] } @arguments_additional = @exe.parameters.select {|e| :opt == e[0] }.map {|e| e[1] } @arguments = @exe.parameters.select {|e| :req == e[0] or :opt == e[0] }.map {|e| e[1] } @options_required = @exe.parameters.select {|e| :keyreq == e[0] }.map {|e| e[1] } @options_additional = @exe.parameters.select {|e| :key == e[0] }.map {|e| e[1] } @options = {} completion {|*a| [] } end def _full_cmd( post) parent._full_cmd [@name]+post end def full_cmd() _full_cmd [] end attr_reader :parameters, :arguments_required, :arguments_additional, :arguments, :options_required, :options_additional alias required arguments_required alias additional arguments_additional def complete *pre, str @completion.call *pre, str end def call *as os = {} unless @options.empty? # options like --abc | -x will be provided in os options = OptionParser.new options.banner = "#{full_cmd.join ' '}" # see also @options-array @options.each {|_, opt| opt.on options, os } as = options.parse! as end if @exe.lambda? # The difference between a lambda and a Proc is, that Proc has anytime arity=-1. # There will be no check if all arguments are given or some were missing or more than expected. # lambda checks these arguments and has a arity. # We will check it to provide useful errors. pars = required if as.length < pars.length raise DenCli::UsageError, "Missing parameter(s): #{pars[as.length..-1].join " "}" end if parameters.select {|e| :rest == e[0] }.empty? pars = pars + additional if as.length > pars.length raise DenCli::UsageError, "Unused parameter(s): #{as[-pars.length..-1].shelljoin}" end end kr = @options.select {|_, o| o.required? and not os.has_key? o.name } unless kr.empty? raise DenCli::UsageError, "Missing argument(s): #{kr.map {|_, o| o.long || o.short }.join ', '}" end end if os.empty? @exe.call *as else @exe.call *as, **os end end def usage output: nil output ||= '' _usage output output end def _usage output output << full_cmd.join( ' ') @options.each do |_, o| s = "#{o.short||o.long}#{(!o.short && o.val) ? ?= : ''}#{o.val}" output << (o.required? ? " #{s}" : " [#{s}]") end if @exe.lambda? parameters.each do |(type, name)| case type when :req output << " <#{name}>" when :opt output << " [<#{name}>]" when :rest output << " [<#{name}> ...]" end end else output << ' [...]' end end def commands *args yield name, self end def goto *args self end def help output: nil output ||= '' _help output output end def _help output output << "Usage: #{usage}\n#{description}\n" _help_options output end def help_options output: nil output ||= '' _help_options output output end def _help_options output sc, lc, dc = 0, 0, 0 @options.each do |_, o| s = o.short&.length || 0 l = o.long&.length || 0 v = o.val&.length || 0 d = o.desc&.to_s&.length || 0 d += 3 + o.default.to_s.length if o.default? if 0 == l x = s + (0==v ? 0 : 1+v) sc = x if sc < x else sc = s if sc < s x = l + (0==v ? 0 : 1+v) lc = x if lc < x end dc = d if dc < d end format = " %-#{sc}s%s %-#{lc}s %s\n" @options.map do |_, o| s, l, v, y = o.short, o.long, o.val, ',' if l.nil? s += "#{v}" if v y = ' ' elsif s.nil? l += "=#{v}" if v y = ' ' end d = o.desc || '' d += " (#{o.default})" if o.default? output << format % [ s, y, l, d ] end end def completion &exe @completion = exe self end class Opt attr_reader :name, :long, :short, :type, :val, :desc, :conv, :req def required?() @req end def default?() NilClass != @default end def default() NilClass == @default ? nil : @default end def parse_opt_string opt case opt when /\A(--\[no-\][^= ]+)\z/ @long, @val = $1, nil when /\A(--[^= ]+)[= ](.+)\z/ @long, @val = $1, $2 || @val when /\A(--[^= ]+)\z/ @long, @val = $1, nil when /\A(-[^= -])[= ]?(.+)\z/ @short, @val = $1, $2 || @val when /\A(-[^= -])\z/ @short, @val = $1, nil else raise ArgumentError, "Unexpected format for option: #{opt.inspect}" end end private :parse_opt_string def initialize cmd, name, opt, *args, desc, default: NilClass, &conv @name, @desc, @default, @conv, @val, @type = name.to_s.to_sym, desc, default, conv || lambda{|v|v}, nil, nil parse_opt_string opt @type = args.pop if OptionParser.top.atype.has_key? args.last args.each &method( :parse_opt_string) @req = if NilClass != default false elsif cmd.exe.lambda? ! cmd.exe.parameters.select {|e| [:keyreq, @name] == e[0..1] }.empty? else nil end end def on parser, store store[@name] = @default if default? short = "#{@short}#{@val ? ?= : ''}#{@val}" long = "#{@long}#{@val ? ?= : ''}#{@val}" parser.on short, long, *[@type].compact do |val| store[@name] = @conv[val] end end def inspect "#<%s:0x%x %s %s %s %s (%p) %p conv=%s>" % [ self.class.name, object_id, @req ? "<#{@name}>" : "[#{@name}]", @short, @long, @val, @default, @desc, @type ? " type=#{type}" : '', @exe ? "<#{@exe.lambda? ? :lambda: :proc} ##{@exe.arity}>" : "nil" ] end end def opt name, opt, *args, desc, default: NilClass, &conv r = Opt.new( self, name, opt, *args, desc, default: default, &conv) @options[r.name] = r self end def inspect "#<%s:0x%x %s @name=%p @description=%p @options=%p @parent=<%s:0x%x %s> @exe=>" % [ self.class.name, self.object_id, self.full_cmd, @name, @description, @options.values, @parent.class.name, @parent.class.object_id, @parent.full_cmd, @exe.arity ] end end