diff --git a/bin/example.rb b/bin/example.rb index ab13bff..c898258 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -1,265 +1,25 @@ #!/usr/bin/env ruby -require 'pathname' -require 'shellwords' -require 'stringio' -require 'ipaddr' -require_relative '../lib/dencli' +require 'dencli' -cli = DenCli.new :example, "This is an example for generate a DenCli-API" - -class Capture - def initialize cli, verbose: - @cli, @counter, @verbose = cli, 0, verbose - end - - def capture - @args = NilClass - @counter += 1 - stdout, stderr = $stdout, $stderr - $stdout = $stderr = StringIO.new - begin - yield stdout, stderr - ensure - $stderr, $stdout = stderr, stdout - end - end - - def args= args - @args = args - end - - def logstart command - STDERR.printf "[% 4d] \e[1;35m? \e[0m %s tests %s\r", @counter, $0.shellescape, command.shelljoin - end - - def logok info, command - STDERR.printf "[% 4d] \e[1;32mok\e[0m %s | %s tests %s\e[J\n", @counter, info, $0.shellescape, command.shelljoin - end - - def logfail command - STDERR.printf "[% 4d] \e[1;31mer\e[0m %s tests %s\e[J\n", @counter, $0.shellescape, command.shelljoin - end - - def logexception prefix, exception - loginfo "#{prefix} (#{exception.class.name}) #{exception}" - exception.backtrace[0...-Kernel.caller.length].each {|l| loginfo " #{l}" } - end - - def loginfo text - STDERR.printf " %s\n", text - end - - def should_ok expect, *command - logstart command - $capture.capture { @cli.call 'tests', *command } - if expect === @args - logok @args, command - else - logfail command - loginfo "expected args: #{expect.inspect}" - loginfo "given args: #{@args.inspect}" - STDERR.puts - end - rescue SystemExit - if 0 == $!.status - logok @args, command - else - logfail command - end - rescue Object - logfail command - logexception "unexpected raise:", $! - STDERR.puts - end - - def should_fail exception, message, *command - logstart command - $capture.capture { @cli.call 'tests', *command } - logfail command - rescue exception - if message === $!.message - logok exception, command - if @verbose - logexception "raised:", $! - STDERR.puts - end - else - logexception "unexpected message:", $! - STDERR.puts - end - rescue Object - logfail command - logexception "unexpected raised:", $! - STDERR.puts - end -end - -cli.cmd( :args, "Expects and prints given arguments", - &lambda {|a, b, c:, d:, e:, f:, g:| - p a: a, b: b, c: c, d: d, e: e - }). - opt( :c, '-c=ForC', "Option c"). - opt( :d, '-dForD', "Option d", default: "something"). - opt( :e, '-e', "Toggle e", default: false). - opt( :f, '--[no-]f', "Toggle f", default: false). - opt( :g, '--long-option=sth', "Long option, no short option", default: "nothing"). - opt( :h, '-hsth', "No long option, only short option", default: "nothing") - -cli.cmd( :example, "I have an example command") { $stderr.puts "This is an example" } -cli.cmd( :help, "An example for help", aliases: [nil, '-h', '--help'], &lambda {|*commands, full:| - if full - cli.help_full *commands, output: $stderr - else - cli.help *commands, output: $stderr - end -}). - opt( :full, '-f', '--[no-]full', "Print all commands and sub-commands.", default: false) +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.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( :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.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.sub( :deeper, "You want to have Sub-Sub-Commands?") do |sub2| - sub2.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*commands| sub2.help( *commands, output: $stderr) }) - sub2.cmd( :last, "The last example", &lambda { $stderr.puts "The last example" }) + sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', *args) } + sub2.cmd( :last, "The last example") { STDERR.puts "The last example" } - sub2.sub( :'sub-commands', "Endless Sub-Sub- ... with a special alias") do |sub3| - # h -> help - # he -> hehe - # hel -> help - # help -> help - # heh -> hehe - # hehe -> hehe - sub3.cmd( :help, "", min: 3, aliases: [nil, :h]) {|*args| $stderr.puts sub3.help( *args) } - sub3.cmd( :hehe, "The real last example", min: 2) { $stderr.puts "Trust me!" } + sub2.sub( :'sub-commands', "Endless Sub-Sub- ...") do |sub3| + sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', 'sub-commands', *args) } + sub3.cmd( :hehe, "The real last example", min: 2) { STDERR.puts "Trust me!" } end end end -cli.cmd( :cli, "Interactive shell", min: 3, &lambda {|| - cli.interactive( File.basename($0, '.rb')).run -}) - -cli.sub :tests, "Some tests", noshortaliases: true do |tcli| - tcli.cmd( :help, "", min: 4) {|*args| $stderr.puts tcli.help( *args) } - OptionParser.accept IPAddr do |arg| - begin - IPAddr.new arg - rescue IPAddr::InvalidAddressError - raise OptionParser::InvalidArgument, "#{$!.message}: #{arg}" - end - end - - tcli.cmd( :'-', "No arguments no options expected", &lambda {|| $capture.args = [] }) - tcli.cmd( :'arg', "", &lambda {|one| $capture.args = [one] }) - tcli.cmd( :'oar', "", &lambda {|one=nil| $capture.args = [one] }) - tcli.cmd( :'arg-arg', "", &lambda {|one, two| $capture.args = [one, two] }) - tcli.cmd( :'oar-oar', "", &lambda {|one=nil, two=nil| $capture.args = [one, two] }) - tcli.cmd( :'arg-oar', "expected", &lambda {|one, two=nil| $capture.args = [one, two] }) - tcli.cmd( :'oar-arg', "expected", &lambda {|one=nil, two| $capture.args = [one, two] }) - tcli.cmd( :'bool', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '') - tcli.cmd( :'optbool', '', &lambda {|a: nil| $capture.args = [a] }).opt(:a, '-a', '') - tcli.cmd( :'defbool', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '', default: 'default') - tcli.cmd( :'str', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a=STR', '') - tcli.cmd( :'lstr', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '--astr=STR', '') - tcli.cmd( :'bstr', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '--astr=STR', '') - tcli.cmd( :'ipaddr', '', &lambda {|a:| $capture&.args = [a] }).opt(:a, '-a', '--addr=ADDR', IPAddr, '') - - tcli.cmd( :run, "Run all tests") do |verbose:| - $capture = Capture.new cli, verbose: verbose - - $capture.should_fail DenCli::UnknownCommand, //, 'unknown-command' - - $capture.should_ok [], '-' - $capture.should_fail DenCli::UsageError, //, '-', 'unexpected' - - $capture.should_ok %w[first], 'arg', 'first' - $capture.should_fail DenCli::UsageError, //, 'arg' - $capture.should_fail DenCli::UsageError, //, 'arg', 'first', 'unexpected' - - $capture.should_ok %w[first], 'oar', 'first' - $capture.should_ok [nil], 'oar' - $capture.should_fail DenCli::UsageError, //, 'oar', 'first', 'unexpected' - - $capture.should_ok %w[first two], 'oar-oar', 'first', 'two' - $capture.should_ok ['first', nil], 'oar-oar', 'first' - $capture.should_ok [nil,nil], 'oar-oar' - $capture.should_fail DenCli::UsageError, //, 'oar-oar', 'first', 'two', 'unexpected' - - $capture.should_ok %w[first two], 'arg-oar', 'first', 'two' - $capture.should_ok ['first', nil], 'arg-oar', 'first' - $capture.should_fail DenCli::UsageError, //, 'arg-oar' - - $capture.should_ok [nil, 'first'], 'oar-arg', 'first' - $capture.should_ok ['first', 'second'], 'oar-arg', 'first', 'second' - $capture.should_fail DenCli::UsageError, //, 'oar-arg' - $capture.should_fail DenCli::UsageError, //, 'oar-arg', 'first', 'two', 'unexpected' - - $capture.should_ok [true], 'bool', '-a' - $capture.should_fail DenCli::UsageError, //, 'bool' - $capture.should_fail DenCli::UsageError, //, 'bool', '-a', 'unexpected' - $capture.should_fail OptionParser::InvalidOption, //, 'bool', '-b' - $capture.should_fail OptionParser::InvalidOption, //, 'bool', '--unexpected' - $capture.should_fail DenCli::UsageError, //, 'bool', 'unexpected' - - $capture.should_ok [true], 'optbool', '-a' - $capture.should_ok [nil], 'optbool' - $capture.should_fail DenCli::UsageError, //, 'optbool', '-a', 'unexpected' - $capture.should_fail OptionParser::InvalidOption, //, 'optbool', '-b' - $capture.should_fail OptionParser::InvalidOption, //, 'optbool', '--unexpected' - $capture.should_fail DenCli::UsageError, //, 'optbool', 'unexpected' - - $capture.should_ok [true], 'defbool', '-a' - $capture.should_ok ['default'], 'defbool' - $capture.should_fail DenCli::UsageError, //, 'defbool', '-a', 'unexpected' - $capture.should_fail OptionParser::InvalidOption, //, 'defbool', '-b' - $capture.should_fail OptionParser::InvalidOption, //, 'defbool', '--unexpected' - $capture.should_fail DenCli::UsageError, //, 'defbool', 'unexpected' - - $capture.should_ok %w[first], 'str', '-a', 'first' - $capture.should_ok %w[first], 'str', '-afirst' - $capture.should_fail OptionParser::MissingArgument, //, 'str', '-a' - $capture.should_fail DenCli::UsageError, //, 'str' - $capture.should_fail OptionParser::InvalidOption, //, 'str', '-b' - $capture.should_fail OptionParser::InvalidOption, //, 'str', '--unexpected' - $capture.should_fail DenCli::UsageError, //, 'str', 'unexpected' - - $capture.should_ok %w[first], 'lstr', '--astr', 'first' - $capture.should_ok %w[first], 'lstr', '--astr=first' - $capture.should_fail OptionParser::MissingArgument, //, 'lstr', '--astr' - $capture.should_fail DenCli::UsageError, //, 'lstr' - $capture.should_fail OptionParser::InvalidOption, //, 'lstr', '-b' - $capture.should_fail OptionParser::InvalidOption, //, 'lstr', '--unexpected' - $capture.should_fail DenCli::UsageError, //, 'lstr', 'unexpected' - - $capture.should_ok %w[first], 'bstr', '-a', 'first' - $capture.should_ok %w[first], 'bstr', '-afirst' - $capture.should_ok %w[first], 'bstr', '--astr', 'first' - $capture.should_ok %w[first], 'bstr', '--astr=first' - $capture.should_fail OptionParser::MissingArgument, //, 'bstr', '--astr' - $capture.should_fail OptionParser::MissingArgument, //, 'bstr', '-a' - $capture.should_fail DenCli::UsageError, //, 'bstr' - $capture.should_fail OptionParser::InvalidOption, //, 'bstr', '-b' - $capture.should_fail OptionParser::InvalidOption, //, 'bstr', '--unexpected' - $capture.should_fail DenCli::UsageError, //, 'bstr', 'unexpected' - - $capture.should_ok [IPAddr.new('1.2.3.4')], 'ipaddr', '-a', '1.2.3.4' - $capture.should_fail OptionParser::InvalidArgument, /invalid address/, 'ipaddr', '-a', '1.2.3.400' - end.opt( :verbose, '-v', 'Prints additional information per test', default: false) -end - -begin - cli.call *ARGV -rescue DenCli::UsageError - $stderr.puts $! - exit 1 -end +cli.call *ARGV diff --git a/lib/dencli.rb b/lib/dencli.rb index 26ee3ae..0c09257 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -1,13 +1,4 @@ -require 'optparse' - - class DenCli - def self.assert_type klass, method, argument_name, object, *types - unless types.any? {|type| object.is_a? type } - raise ArgumentError, "#{klass.name}.#{method} expects #{types.map( &:to_s).join '|' } for #{argument_name}, not: #{object.inspect}" - end - end - class UsageError < ::RuntimeError end class UnknownCommand < UsageError @@ -51,16 +42,11 @@ class DenCli # `g(:abc)` => `["a", "ab", "abc"]` # `g(:abcdef, 4)` => `["abcd", "abcde", "abcdef"]` def gen_aliases cmd, min = nil - case min - when false then min = cmd.length - when nil then min = 1 - end - r = ([min, 1].max - 1).upto cmd.length-2 + r = ((min||1)-1).upto cmd.length-1 if block_given? r.each {|i| yield cmd[0..i] } - yield cmd else - r.map {|i| cmd[0..i] } + [cmd] + r.map {|i| cmd[0..i] } end end alias g gen_aliases @@ -68,8 +54,8 @@ class DenCli attr_reader :subs - def initialize progname, description - @subs = Sub.new self, progname, description + def initialize progname, desc + @subs = Sub.new self, progname, desc end def full_cmd @@ -80,37 +66,24 @@ class DenCli post end - def []( k) @subs[k] end - def has?( k) @subs.has? k end - - def sub *a, **o, &exe - @subs.sub *a, **o, &exe + def sub *a, &exe + @subs.sub *a, &exe end - def cmd *a, **o, &exe - @subs.cmd *a, **o, &exe + def cmd *a, &exe + @subs.cmd *a, &exe end def call *a @subs.call *a end - def usage *args, **opts - @subs.usage *args, **opts + def help *args + @subs.help *args end - def help *args, **opts - @subs.help *args, **opts - end - - def help_full *args, output: nil - output ||= $stdout - x = @subs.goto *args - _help_full output, x - end - - def _help_full output, subs - Sub._help_commands output, subs.to_enum( :commands) + def [] k + @subs[k] end def interactive *args, **opts diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 3d65ce0..0633bfb 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -1,232 +1,30 @@ require_relative '../dencli' - class DenCli::CMD - attr_reader :parent, :name, :description, :exe, :completion, :options, :defined_in + attr_reader :parent, :name, :desc, :exe, :completion - def initialize parent, name, description, exe, defined_in + def initialize parent, name, desc, exe 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 = {} + @parent, @name, @desc, @exe = parent, name, desc, exe completion {|*a| [] } end def _full_cmd( post) parent._full_cmd [@name]+post end def full_cmd() _full_cmd [] end + def call( *a) @exe.call *a end + def help() "#{parent.full_cmd.join ' '} #{name}\n#{ desc}" 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 complete( *pre, str) @completion.call *pre, str 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=>" % [ + "#<%s:0x%x %s @name=%p @desc=%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, + @name, @desc, @parent.class.name, @parent.class.object_id, @parent.full_cmd, @exe.arity ] end diff --git a/lib/dencli/interactive.rb b/lib/dencli/interactive.rb index 8a75314..69eb07e 100644 --- a/lib/dencli/interactive.rb +++ b/lib/dencli/interactive.rb @@ -17,15 +17,12 @@ class DenCli::Interactive end read_history if @histfile - begin - Readline.vi_editing_mode - rescue NotImplementedError - end + Readline.vi_editing_mode rescue NotImplementedError Readline.completion_append_character = " " Readline.completion_proc = method :complete prepare_sub cl.subs - cl.cmd :exit, "exit", min: cl.has?(:ex) ? cl.has?(:exi) ? 4 : 3 : 2 do + cl.cmd :exit, "exit", min: 2 do exit 0 end cl.subs.aliases['?'] = cl.subs.subs['help'] @@ -93,16 +90,14 @@ class DenCli::Interactive c.subs.values.each do |n| case n when DenCli::Sub - n.cmd :exit, - "<- #{n.parent.full_cmd.join ' '} - #{n.parent.description[3..-1]}", - min: n.has?(:ex) ? n.has?( :exi) ? 4 : 3 : 2, - &lambda {|| @cur = n.parent } - n.aliases.delete nil - n.subs.delete nil + n.cmd :exit, "<- #{n.parent.full_cmd.join ' '} - #{n.parent.desc[3..-1]}", min: 2 do + @cur = n.parent + end n.cmd '', "", min: 2, aliases: [nil] do @cur = n end - n.aliases['?'] = n[:help] if n.has? :help and not n.has? '?' + n.subs.delete '' + n.aliases['?'] = n.subs['help'] prepare_sub n when DenCli::CMD else raise "Unsupported sub-type: #{x}" @@ -125,8 +120,8 @@ class DenCli::Interactive return nil if line.nil? begin cur.call *line - rescue ::DenCli::UsageError - $stderr.puts "! #$!" + rescue ::UsageError + STDERR.puts "! #$!" end true end diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index d8c34e6..e822e1d 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -1,191 +1,58 @@ require_relative '../dencli' class DenCli::Sub - attr_reader :parent, :name, :description, :subs, :aliases, :defined_in + attr_reader :parent, :name, :desc, :subs, :aliases - def initialize parent, name, description, noshortaliases: nil, defined_in: nil - #DenCli::assert_type self, __method__, :name, name, Symbol - #DenCli::assert_type self, __method__, :parent, parent, DenCli, DenCli::Sub - #DenCli::assert_type self, __method__, :description, description, String - @parent, @name, @description, @subs, @aliases = parent, name, "-> #{description}", {}, {} - @noshortaliases, @defined_in = ! ! noshortaliases, defined_in || Kernel.caller + def initialize parent, name, desc + @parent, @name, @desc, @subs, @aliases = parent, name, "-> #{desc}", {}, {} end def _full_cmd( post) parent._full_cmd [@name]+post end def full_cmd() _full_cmd [] end - def []( name) @aliases[name&.to_s] end - def has?( name) @aliases.has_key? name&.to_s end + def []( k) @aliases[k] end - - def usage output: nil - output ||= '' - _usage output - output - end - - def _usage output - output << full_cmd.join( ' ') - if @aliases.has_key? nil - output << " [ ...]" - else - output << " [...]" - end - end - - def help n = nil, *a, output: nil - output ||= '' - _help output, n, *a - output - end - - def _help output, n = nil, *a + def help n = nil, *a if n.nil? - output << "#{full_cmd.join ' '}: #{description}\n\n" - self.class._help_commands output, @subs - elsif has? n - self[n]._help output, *a - else - raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}" - end - end - - def commands &exe - yield name, self - @subs.each {|k, c| c.commands &exe } - end - - def help_commands output: nil - output ||= '' - self.class._help_commands output, subs.map {|_,c| c} - output - end - - class <).each do |w| - if prefix - output << prefix - prefix = nil - end - wl = w.length - if 75 < c+wl - output << "\n#{' ' * n}#{w}" - c = n+2+wl - else - output << " #{w}" - c += 1 + wl - end - end - prefix = "\n#{' ' * n}" - end - output << "\n" + r = "#{full_cmd.join ' '}: #{desc}\n\n" + m = @subs.map {|k,_| k.length }.max + @subs.each do |k, c| + r += " % -#{m}s %s\n" % [k, c.desc] unless k.nil? end - output - end - end - - def goto *a - return self if a.empty? - n, *a = *a - if has? n - self[n].goto *a + r + elsif @aliases.has_key? n + @aliases[n].help *a else - raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}" + raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}" end end def call *a n, *a = *a - if has? n - self[n].call *a + if @aliases.has_key? n + @aliases[n].call *a else - raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}" + raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}" end end def _add name, min, obj, aliases - #DenCli::assert_type self, __method__, :name, name, Symbol, String - #DenCli::assert_type self, __method__, :min, min, Integer, NilClass - #DenCli::assert_type self, __method__, :obj, obj, DenCli::Sub, DenCli::CMD - #DenCli::assert_type self, __method__, :aliases, aliases, Array, NilClass - name = name.to_s + name = name.to_s unless name.nil? @subs[name] = obj - if @noshortaliases - warn "Command/Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{name}. Used by #{@aliases[name].full_cmd} defined in #{@aliases[name].defined_in}" if @aliases.has_key? name - @aliases[name] = obj - else - DenCli.gen_aliases name, min do |a| - warn "Command/Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{a}. Used by #{@aliases[a].full_cmd} defined in #{@aliases[a].defined_in}" if @aliases.has_key? a - @aliases[a] ||= obj - end - end + DenCli.gen_aliases( name, min) {|a| @aliases[a] = obj } if aliases - [*aliases].each do |a| - a = a&.to_s - raise ArgumentError, "Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{a}. Used by #{@aliases[a].full_cmd} defined in #{@aliases[a].defined_in}" if @aliases.has_key? a - @aliases[a] = obj - end + [*aliases].each {|a| @aliases[a] = obj } end obj end private :_add - # Define a new sub-menu: - # - # DenCli.new {|c| - # c.sub( 'sub-command') {|s| - # s.cmd( :hello, 'Greetings', &lambda {|| puts 'hello world' }) - # } - # } - # - # # ./prog sub-command hello - # - # name should be a string/symbol. It will be converted to string. - # If provided, aliases must be a list of different aliases. It will be converted to string. - def sub name, description, min: nil, aliases: nil, noshortaliases: nil, defined_in: nil, &exe - r = _add name.to_s, min, DenCli::Sub.new( self, name, description, noshortaliases: noshortaliases, defined_in: defined_in || Kernel.caller.first), aliases + def sub name, desc, min: nil, aliases: nil, &exe + r = _add name, min, DenCli::Sub.new( self, name, desc), aliases block_given? ? yield( r) : r end - # Define a new command: - # - # DenCli.new {|c| - # c.cmd( :hello, 'Greetings', &lambda {|| puts 'hello world' }) - # } - # - # # ./prog hello - # hello world - # - # name should be a string/symbol. It will be converted to string. - # If provided, aliases must be a list of different aliases. Except of nil, any alias will be converted to string. - # nil is an alias for a default command for sub-commands, but interactive shells. - # - # DenCli.new {|c| - # c.sub( :greetings, 'Hello, Welcome, ...') do |s| - # s.cmd( :hello, 'A simple Hello', aliases: %w[hello-world hello_world], &lambda {|| puts 'Hello World' }) - # s.cmd( :welcome, 'More gracefull', aliases: [nil, 'welcome-world', :hello_world], &lambda {|| puts 'Welcome World' }) - # } - # } - # - # # ./prog greetings - # Welcome World - # # ./prog greetings welcome - # Welcome World - # # ./prog greetings hello - # Hello World - def cmd name, description, min: nil, aliases: nil, defined_in: nil, &exe - _add name, min, DenCli::CMD.new( self, name, description, exe, defined_in || Kernel.caller.first), aliases + def cmd name, desc, min: nil, aliases: nil, &exe + _add name, min, DenCli::CMD.new( self, name, desc, exe), aliases end def complete *pre, str @@ -194,14 +61,14 @@ class DenCli::Sub elsif sub = @subs[pre[0]] sub.complete *pre[1..-1], str else - $stdout.print "\a" + STDOUT.print "\a" end end def inspect - "#<%s:0x%x %s @name=%p @description=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [ + "#<%s:0x%x %s @name=%p @desc=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [ self.class.name, self.object_id, self.full_cmd, - @name, @description, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd + @name, @desc, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd ] end end diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index 835f9d2..a95042a 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.5.5' + VERSION = '0.2.0' end