diff --git a/bin/example.rb b/bin/example.rb index c898258..ab13bff 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -1,25 +1,265 @@ #!/usr/bin/env ruby -require 'dencli' +require 'pathname' +require 'shellwords' +require 'stringio' +require 'ipaddr' +require_relative '../lib/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 = 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.sub( :more, "Sub-Commands are also possible with a new cli") do |sub| - 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( :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.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 {|*commands| sub2.help( *commands, output: $stderr) }) + 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( :hehe, "The real last example", min: 2) { STDERR.puts "Trust me!" } + 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!" } end end end -cli.call *ARGV +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 diff --git a/lib/dencli.rb b/lib/dencli.rb index 0c09257..26ee3ae 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -1,4 +1,13 @@ +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 @@ -42,11 +51,16 @@ class DenCli # `g(:abc)` => `["a", "ab", "abc"]` # `g(:abcdef, 4)` => `["abcd", "abcde", "abcdef"]` def gen_aliases cmd, min = nil - r = ((min||1)-1).upto cmd.length-1 + case min + when false then min = cmd.length + when nil then min = 1 + end + r = ([min, 1].max - 1).upto cmd.length-2 if block_given? r.each {|i| yield cmd[0..i] } + yield cmd else - r.map {|i| cmd[0..i] } + r.map {|i| cmd[0..i] } + [cmd] end end alias g gen_aliases @@ -54,8 +68,8 @@ class DenCli attr_reader :subs - def initialize progname, desc - @subs = Sub.new self, progname, desc + def initialize progname, description + @subs = Sub.new self, progname, description end def full_cmd @@ -66,24 +80,37 @@ class DenCli post end - def sub *a, &exe - @subs.sub *a, &exe + def []( k) @subs[k] end + def has?( k) @subs.has? k end + + def sub *a, **o, &exe + @subs.sub *a, **o, &exe end - def cmd *a, &exe - @subs.cmd *a, &exe + def cmd *a, **o, &exe + @subs.cmd *a, **o, &exe end def call *a @subs.call *a end - def help *args - @subs.help *args + def usage *args, **opts + @subs.usage *args, **opts end - def [] k - @subs[k] + 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) end def interactive *args, **opts diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 0633bfb..3d65ce0 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -1,30 +1,232 @@ require_relative '../dencli' -class DenCli::CMD - attr_reader :parent, :name, :desc, :exe, :completion - def initialize parent, name, desc, exe +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, @desc, @exe = parent, name, desc, 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 - def call( *a) @exe.call *a end - def help() "#{parent.full_cmd.join ' '} #{name}\n#{ desc}" end - def complete( *pre, str) @completion.call *pre, str 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 @desc=%p @parent=<%s:0x%x %s> @exe=>" % [ + "#<%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, @desc, @parent.class.name, @parent.class.object_id, @parent.full_cmd, + @name, @description, @options.values, @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 69eb07e..8a75314 100644 --- a/lib/dencli/interactive.rb +++ b/lib/dencli/interactive.rb @@ -17,12 +17,15 @@ class DenCli::Interactive end read_history if @histfile - Readline.vi_editing_mode rescue NotImplementedError + begin + Readline.vi_editing_mode + rescue NotImplementedError + end Readline.completion_append_character = " " Readline.completion_proc = method :complete prepare_sub cl.subs - cl.cmd :exit, "exit", min: 2 do + cl.cmd :exit, "exit", min: cl.has?(:ex) ? cl.has?(:exi) ? 4 : 3 : 2 do exit 0 end cl.subs.aliases['?'] = cl.subs.subs['help'] @@ -90,14 +93,16 @@ class DenCli::Interactive c.subs.values.each do |n| case n when DenCli::Sub - n.cmd :exit, "<- #{n.parent.full_cmd.join ' '} - #{n.parent.desc[3..-1]}", min: 2 do - @cur = n.parent - end + 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 '', "", min: 2, aliases: [nil] do @cur = n end - n.subs.delete '' - n.aliases['?'] = n.subs['help'] + n.aliases['?'] = n[:help] if n.has? :help and not n.has? '?' prepare_sub n when DenCli::CMD else raise "Unsupported sub-type: #{x}" @@ -120,8 +125,8 @@ class DenCli::Interactive return nil if line.nil? begin cur.call *line - rescue ::UsageError - STDERR.puts "! #$!" + rescue ::DenCli::UsageError + $stderr.puts "! #$!" end true end diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index e822e1d..d8c34e6 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -1,58 +1,191 @@ require_relative '../dencli' class DenCli::Sub - attr_reader :parent, :name, :desc, :subs, :aliases + attr_reader :parent, :name, :description, :subs, :aliases, :defined_in - def initialize parent, name, desc - @parent, @name, @desc, @subs, @aliases = parent, name, "-> #{desc}", {}, {} + 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 end def _full_cmd( post) parent._full_cmd [@name]+post end def full_cmd() _full_cmd [] end - def []( k) @aliases[k] end + def []( name) @aliases[name&.to_s] end + def has?( name) @aliases.has_key? name&.to_s end - def help n = nil, *a - if n.nil? - 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 - r - elsif @aliases.has_key? n - @aliases[n].help *a + + def usage output: nil + output ||= '' + _usage output + output + end + + def _usage output + output << full_cmd.join( ' ') + if @aliases.has_key? nil + output << " [ ...]" else - raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}" + output << " [...]" + end + end + + def help n = nil, *a, output: nil + output ||= '' + _help output, n, *a + output + end + + def _help output, 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" + end + output + end + end + + def goto *a + return self if a.empty? + n, *a = *a + if has? n + self[n].goto *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 call *a n, *a = *a - if @aliases.has_key? n - @aliases[n].call *a + if has? n + self[n].call *a else - raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}" + raise DenCli::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 - name = name.to_s unless name.nil? + #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 @subs[name] = obj - DenCli.gen_aliases( name, min) {|a| @aliases[a] = 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 if aliases - [*aliases].each {|a| @aliases[a] = obj } + [*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 end obj end private :_add - def sub name, desc, min: nil, aliases: nil, &exe - r = _add name, min, DenCli::Sub.new( self, name, desc), aliases + # 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 block_given? ? yield( r) : r end - def cmd name, desc, min: nil, aliases: nil, &exe - _add name, min, DenCli::CMD.new( self, name, desc, exe), aliases + # 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 end def complete *pre, str @@ -61,14 +194,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 @desc=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [ + "#<%s:0x%x %s @name=%p @description=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [ self.class.name, self.object_id, self.full_cmd, - @name, @desc, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd + @name, @description, @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 a95042a..835f9d2 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.2.0' + VERSION = '0.5.5' end