diff --git a/bin/example.rb b/bin/example.rb index 963ad32..ab13bff 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -1,25 +1,125 @@ #!/usr/bin/env ruby require 'pathname' -$:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s -require 'dencli' +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" -cli.cmd( :args, "Expects and prints given arguments", &lambda {|a, b, c:, d:, e:| +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, '-d=ForD', "Option d", default: "something"). - opt( :e, '-e', "Toggle e", default: false) + 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, "", 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( :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 @@ -29,19 +129,137 @@ cli.sub( :more, "Sub-Commands are also possible with a new cli") do |sub| opt( :e, '-e', "Toggle e") sub.sub( :deeper, "You want to have Sub-Sub-Commands?") do |sub2| - 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.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| - sub3.cmd( :help, "") {|*args| STDERR.puts sub3.help( sub3, *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.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 $! + $stderr.puts $! exit 1 end diff --git a/lib/dencli.rb b/lib/dencli.rb index 4f4781c..26ee3ae 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -1,6 +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 @@ -44,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 @@ -68,6 +80,9 @@ 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 end @@ -80,12 +95,22 @@ class DenCli @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 f7cd0f8..3d65ce0 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -2,11 +2,17 @@ require_relative '../dencli' class DenCli::CMD - attr_reader :parent, :name, :description, :exe, :completion, :options + attr_reader :parent, :name, :description, :exe, :completion, :options, :defined_in - def initialize parent, name, description, exe + def initialize parent, name, description, exe, defined_in raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe - @parent, @name, @description, @exe = parent, name, description, lambda( &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 @@ -14,19 +20,17 @@ class DenCli::CMD def _full_cmd( post) parent._full_cmd [@name]+post end def full_cmd() _full_cmd [] end - def parameters() @exe.parameters end - def arguments_required() @exe.parameters.select {|e| :req == e[0] }.map {|e| e[1] } end + attr_reader :parameters, + :arguments_required, :arguments_additional, :arguments, + :options_required, :options_additional 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 complete *pre, str + @completion.call *pre, str + end - def call( *as) + def call *as os = {} unless @options.empty? # options like --abc | -x will be provided in os @@ -53,25 +57,70 @@ class DenCli::CMD 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.as.first }.join ', '}" + raise DenCli::UsageError, "Missing argument(s): #{kr.map {|_, o| o.long || o.short }.join ', '}" end end - @exe.call *as, **os + if os.empty? + @exe.call *as + else + @exe.call *as, **os + end end - def usage - args = - @options.map do |_, o| - s = "#{o.short}#{o.val ? ?= : ''}#{o.val}"; o.required? ? "#{s} " : "[#{s}] " + 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 - "#{full_cmd.join ' '} #{args.join ''}"+ - (@exe.lambda? ? ( - required.map{|s|"<#{s}>"}.join( " ")+ - (additional.empty? ? "" : " [#{additional.map{|s|"<#{s}>"}.join " "}]") - ) : '...') + else + output << ' [...]' + end end - def options_help + 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 @@ -89,11 +138,11 @@ class DenCli::CMD end dc = d if dc < d end - format = " %-#{sc}s%s %-#{lc}s %s" - @options.map {|_, o| + 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 + s += "#{v}" if v y = ' ' elsif s.nil? l += "=#{v}" if v @@ -101,8 +150,8 @@ class DenCli::CMD end d = o.desc || '' d += " (#{o.default})" if o.default? - format % [ s, y, l, d ] - }.join "\n" + output << format % [ s, y, l, d ] + end end def completion &exe @@ -111,39 +160,35 @@ class DenCli::CMD end class Opt - attr_reader :name, :long, :short, :val, :desc, :os, :conv, :req + 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 initialize cmd, name, opt, *alts, desc, default: NilClass, **os, &conv - long = short = val = nil + def parse_opt_string opt 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}" + 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 - 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} + 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 @@ -155,30 +200,33 @@ class DenCli::CMD end def on parser, store - parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val| + 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%016x %s %s %s %s (%p) %p os=%p conv=%s>" % [ + "#<%s:0x%x %s %s %s %s (%p) %p conv=%s>" % [ self.class.name, object_id, @req ? "<#{@name}>" : "[#{@name}]", - @short, @long, @val, @default, @desc, @os, + @short, @long, @val, @default, @desc, @type ? " type=#{type}" : '', @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) + 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 @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, @description, @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 d03b482..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.description[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}" @@ -121,7 +126,7 @@ class DenCli::Interactive begin cur.call *line rescue ::DenCli::UsageError - STDERR.puts "! #$!" + $stderr.puts "! #$!" end true end diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 5622d69..d8c34e6 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -1,30 +1,105 @@ require_relative '../dencli' class DenCli::Sub - attr_reader :parent, :name, :description, :subs, :aliases + attr_reader :parent, :name, :description, :subs, :aliases, :defined_in - def initialize parent, name, description + 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 usage - "#{full_cmd.join ' '} ..." + + def usage output: nil + output ||= '' + _usage output + output end - def help n = nil, *a + 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 if n.nil? - r = "#{full_cmd.join ' '}: #{description}\n\n" - m = @subs.map {|k,c| k.nil? ? 0 : c.usage.length }.max - @subs.each do |k, c| - r += " % -#{m}s %s\n" % [c.usage, c.description] unless k.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 - r - elsif @aliases.has_key? n - @aliases[n].help *a + 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 @@ -32,31 +107,85 @@ class DenCli::Sub def call *a n, *a = *a - if @aliases.has_key? n - @aliases[n].call *a + if has? n + self[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 ' '}" 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, description, min: nil, aliases: nil, &exe - r = _add name, min, DenCli::Sub.new( self, name, description), 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, description, min: nil, aliases: nil, &exe - _add name, min, DenCli::CMD.new( self, name, description, 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 @@ -65,7 +194,7 @@ class DenCli::Sub elsif sub = @subs[pre[0]] sub.complete *pre[1..-1], str else - STDOUT.print "\a" + $stdout.print "\a" end end diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index 1d939af..835f9d2 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.4.0' + VERSION = '0.5.5' end