diff --git a/bin/example.rb b/bin/example.rb index c898258..963ad32 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -1,25 +1,47 @@ #!/usr/bin/env ruby +require 'pathname' +$:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s require 'dencli' cli = DenCli.new 'example', "This is an example for generate a DenCli-API" cli.cmd( :example, "I have an example command") { STDERR.puts "This is an example" } cli.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts cli.help(*args) } +cli.cmd( :args, "Expects and prints given arguments", &lambda {|a, b, c:, d:, e:| + p a: a, b: b, c: c, d: d, e: e + }). + opt( :c, '-c=ForC', "Option c"). + opt( :d, '-d=ForD', "Option d", default: "something"). + opt( :e, '-e', "Toggle e", default: false) + cli.sub( :more, "Sub-Commands are also possible with a new cli") do |sub| + sub.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts sub.help(*args) } sub.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', *args) } sub.cmd( :example, "Here is an example, too") { STDERR.puts "This is an other example" } sub.cmd( :foo, "BAR") { STDERR.puts "FOO bar"} + sub.cmd( :args, "Expects and prints given arguments", &lambda {|a, b=1, c:, d: 5, e:| + p a: a, b: b, c: c, d: d, e: e + }). + opt( :c, '-c=ForC', "Option c"). + opt( :d, '-d=ForD', "Option d (implicit default)"). + opt( :e, '-e', "Toggle e") + sub.sub( :deeper, "You want to have Sub-Sub-Commands?") do |sub2| - sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', *args) } - sub2.cmd( :last, "The last example") { STDERR.puts "The last example" } + sub2.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*args| STDERR.puts sub2.help(*args) }) + sub2.cmd( :last, "The last example", &lambda { STDERR.puts "The last example" }) sub2.sub( :'sub-commands', "Endless Sub-Sub- ...") do |sub3| - sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', 'sub-commands', *args) } + sub3.cmd( :help, "") {|*args| STDERR.puts sub3.help( sub3, *args) } sub3.cmd( :hehe, "The real last example", min: 2) { STDERR.puts "Trust me!" } end end end -cli.call *ARGV +begin + cli.call *ARGV +rescue DenCli::UsageError + STDERR.puts $! + exit 1 +end diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 8d4dfd6..f7cd0f8 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -1,12 +1,13 @@ require_relative '../dencli' + class DenCli::CMD attr_reader :parent, :name, :description, :exe, :completion, :options def initialize parent, name, description, exe raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe @parent, @name, @description, @exe = parent, name, description, lambda( &exe) - @options = [] + @options = {} completion {|*a| [] } end @@ -14,59 +15,163 @@ class DenCli::CMD def full_cmd() _full_cmd [] end def parameters() @exe.parameters end - def required() @exe.parameters.select{|e|:req == e[0]}.map{|e|e[1]} end - def additional() @exe.parameters.select{|e|:opt == e[0]}.map{|e|e[1]} end + def arguments_required() @exe.parameters.select {|e| :req == e[0] }.map {|e| e[1] } end + alias required arguments_required + def arguments_additional() @exe.parameters.select {|e| :opt == e[0] }.map {|e| e[1] } end + alias additional arguments_additional + def arguments() @exe.parameters.select {|e| :req == e[0] or :opt == e[0] }.map {|e| e[1] } end + def options_required() @exe.parameters.select {|e| :keyreq == e[0] }.map {|e| e[1] } end + def options_additional() @exe.parameters.select {|e| :key == e[0] }.map {|e| e[1] } end + + def help() "Usage: #{usage}\n#{description}\n#{options_help}" end + def complete( *pre, str) @completion.call *pre, str end def call( *as) - if @options.empty? - @exe.call *as - else - os = {} + os = {} + unless @options.empty? + # options like --abc | -x will be provided in os options = OptionParser.new options.banner = "#{full_cmd.join ' '}" - @options.each do |(aname, aas, aos, aexe)| - os[aname] = aos[aname] if aos.has_key? :default - options.on( *aas) {|val| os[aname] = aexe[val] } - end + # see also @options-array + @options.each {|_, opt| opt.on options, os } as = options.parse! as - if @exe.lambda? - 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 + 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 - @exe.call *as, **os + 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.as.first }.join ', '}" + end end + @exe.call *as, **os end def usage - "#{parent.full_cmd.join ' '} #{name} "+ - @options.map{|(_,(o,_,_,_),_)|"[#{o}] "}.join( '')+ + args = + @options.map do |_, o| + s = "#{o.short}#{o.val ? ?= : ''}#{o.val}"; o.required? ? "#{s} " : "[#{s}] " + end + "#{full_cmd.join ' '} #{args.join ''}"+ (@exe.lambda? ? ( - required.join( " ")+ - (additional.empty? ? "" : " [#{additional.join " "}]") + required.map{|s|"<#{s}>"}.join( " ")+ + (additional.empty? ? "" : " [#{additional.map{|s|"<#{s}>"}.join " "}]") ) : '...') end - def help - "#{usage}\n#{description}" + def options_help + 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" + @options.map {|_, 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? + format % [ s, y, l, d ] + }.join "\n" end - def complete( *pre, str) @completion.call *pre, str end - def completion &exe @completion = exe self end - def opt name, *as, **os, &exe - @options.push [name.to_s.to_sym, as, os, exe || lambda{|val|val} ] + class Opt + attr_reader :name, :long, :short, :val, :desc, :os, :conv, :req + def required?() @req end + def default?() NilClass != @default end + def default() NilClass == @default ? nil : @default end + + def initialize cmd, name, opt, *alts, desc, default: NilClass, **os, &conv + long = short = val = nil + case opt + when /\A(--[^=-]+)=(.+)\z/ + long, val = $1, $2 + when /\A(--[^=-]+)\z/ + long, val = $1, nil + when /\A(-[^=-]+)=(.+)\z/ + short, val = $1, $2 + when /\A(-[^=-]+)\z/ + short, val = $1, nil + else raise ArgumentError, "Unexpected format for option: #{opt.inspect}" + end + alts.each do |alt| + case alt + when /\A(--[^=-]+)=(.+)\z/ + long, val = $1, val || $2 + when /\A(--[^=-]+)\z/ + long, val = $1, nil + when /\A(-[^=-]+)=(.+)\z/ + short, val = $1, val || $2 + when /\A(-[^=-]+)\z/ + short, val = $1, nil + else raise ArgumentError, "Unexpected format for option: #{alt.inspect}" + end + end + @name, @short, @long, @val, @desc, @default, @os, @conv = + name.to_s.to_sym, short, long, val, desc, default, os, conv || lambda{|v|v} + @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 + parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val| + store[@name] = @conv[val] + end + end + + def inspect + "#<%s:0x%016x %s %s %s %s (%p) %p os=%p conv=%s>" % [ + self.class.name, object_id, @req ? "<#{@name}>" : "[#{@name}]", + @short, @long, @val, @default, @desc, @os, + @exe ? "<#{@exe.lambda? ? :lambda: :proc} ##{@exe.arity}>" : "nil" + ] + end + end + + def opt name, opt, *alts, desc, **os, &conv + r = Opt.new( self, name, opt, *alts, desc, **os, &conv) + @options[r.name] = r self end diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index ad7110b..1d939af 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.3.2' + VERSION = '0.4.0' end