diff --git a/bin/example.rb b/bin/example.rb index ab13bff..963ad32 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -1,125 +1,25 @@ #!/usr/bin/env ruby require 'pathname' -require 'shellwords' -require 'stringio' -require 'ipaddr' -require_relative '../lib/dencli' +$:.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 = 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) } -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:| +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, '-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) + 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( :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( :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 @@ -129,137 +29,19 @@ 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 {|*commands| sub2.help( *commands, output: $stderr) }) - sub2.cmd( :last, "The last example", &lambda { $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- ... 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| + 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.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 26ee3ae..4f4781c 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -1,13 +1,6 @@ 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 +44,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 @@ -80,9 +68,6 @@ 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 @@ -95,22 +80,12 @@ class DenCli @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..f7cd0f8 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -2,17 +2,11 @@ require_relative '../dencli' class DenCli::CMD - attr_reader :parent, :name, :description, :exe, :completion, :options, :defined_in + attr_reader :parent, :name, :description, :exe, :completion, :options - def initialize parent, name, description, exe, defined_in + def initialize parent, name, description, 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] } + @parent, @name, @description, @exe = parent, name, description, lambda( &exe) @options = {} completion {|*a| [] } end @@ -20,17 +14,19 @@ class DenCli::CMD def _full_cmd( post) parent._full_cmd [@name]+post end def full_cmd() _full_cmd [] end - attr_reader :parameters, - :arguments_required, :arguments_additional, :arguments, - :options_required, :options_additional + def parameters() @exe.parameters 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 complete *pre, str - @completion.call *pre, str - end + def help() "Usage: #{usage}\n#{description}\n#{options_help}" 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 @@ -57,70 +53,25 @@ 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.long || o.short }.join ', '}" + raise DenCli::UsageError, "Missing argument(s): #{kr.map {|o| o.as.first }.join ', '}" end end - if os.empty? - @exe.call *as - else - @exe.call *as, **os - end + @exe.call *as, **os 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 + def usage + args = + @options.map do |_, o| + s = "#{o.short}#{o.val ? ?= : ''}#{o.val}"; o.required? ? "#{s} " : "[#{s}] " end - else - output << ' [...]' - end + "#{full_cmd.join ' '} #{args.join ''}"+ + (@exe.lambda? ? ( + required.map{|s|"<#{s}>"}.join( " ")+ + (additional.empty? ? "" : " [#{additional.map{|s|"<#{s}>"}.join " "}]") + ) : '...') 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 + def options_help sc, lc, dc = 0, 0, 0 @options.each do |_, o| s = o.short&.length || 0 @@ -138,11 +89,11 @@ class DenCli::CMD end dc = d if dc < d end - format = " %-#{sc}s%s %-#{lc}s %s\n" - @options.map do |_, o| + 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 + s += "=#{v}" if v y = ' ' elsif s.nil? l += "=#{v}" if v @@ -150,8 +101,8 @@ class DenCli::CMD end d = o.desc || '' d += " (#{o.default})" if o.default? - output << format % [ s, y, l, d ] - end + format % [ s, y, l, d ] + }.join "\n" end def completion &exe @@ -160,35 +111,39 @@ class DenCli::CMD end class Opt - attr_reader :name, :long, :short, :type, :val, :desc, :conv, :req + 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 parse_opt_string opt + def initialize cmd, name, opt, *alts, desc, default: NilClass, **os, &conv + long = short = val = nil 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}" + 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 - 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) + 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 @@ -200,33 +155,30 @@ class DenCli::CMD 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| + parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val| store[@name] = @conv[val] end end def inspect - "#<%s:0x%x %s %s %s %s (%p) %p conv=%s>" % [ + "#<%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, @type ? " type=#{type}" : '', + @short, @long, @val, @default, @desc, @os, @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) + def opt name, opt, *alts, desc, **os, &conv + r = Opt.new( self, name, opt, *alts, desc, **os, &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 @description=%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, @description, @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..d03b482 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.description[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}" @@ -126,7 +121,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 d8c34e6..5622d69 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -1,105 +1,30 @@ require_relative '../dencli' class DenCli::Sub - attr_reader :parent, :name, :description, :subs, :aliases, :defined_in + attr_reader :parent, :name, :description, :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 + def initialize parent, name, description @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 []( 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 + def usage + "#{full_cmd.join ' '} ..." 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 ' '}: #{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? 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 ' '}" end @@ -107,85 +32,31 @@ class DenCli::Sub 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 ' '}" 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, description, min: nil, aliases: nil, &exe + r = _add name, min, DenCli::Sub.new( self, name, description), 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, description, min: nil, aliases: nil, &exe + _add name, min, DenCli::CMD.new( self, name, description, exe), aliases end def complete *pre, str @@ -194,7 +65,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 835f9d2..1d939af 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.5.5' + VERSION = '0.4.0' end