diff --git a/bin/example.rb b/bin/example.rb index 963ad32..03787b4 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -5,15 +5,23 @@ $:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s require 'dencli' cli = DenCli.new 'example', "This is an example for generate a DenCli-API" -cli.cmd( :example, "I have an example command") { STDERR.puts "This is an example" } -cli.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts cli.help(*args) } - cli.cmd( :args, "Expects and prints given arguments", &lambda {|a, b, c:, d:, e:| p a: a, b: b, c: c, d: d, e: e }). opt( :c, '-c=ForC', "Option c"). opt( :d, '-d=ForD', "Option d", default: "something"). - opt( :e, '-e', "Toggle e", default: false) + opt( :e, '-e', "Toggle e", default: false). + opt( :f, '--[no-]f', "Toggle f", default: false) + +cli.cmd( :example, "I have an example command") { STDERR.puts "This is an example" } +cli.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*args, full:| + if full + cli.help_full *args, output: STDERR + else + cli.help *args, 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) } @@ -29,7 +37,7 @@ 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( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*args| sub2.help( *args, output: STDERR) }) sub2.cmd( :last, "The last example", &lambda { STDERR.puts "The last example" }) sub2.sub( :'sub-commands', "Endless Sub-Sub- ...") do |sub3| diff --git a/lib/dencli.rb b/lib/dencli.rb index 4f4781c..7681856 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -80,8 +80,22 @@ class DenCli @subs.call *a end - def help *args - @subs.help *args + def usage *args, **opts + @subs.usage *args, **opts + 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) end def [] k diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index f7cd0f8..12cad0a 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -23,10 +23,11 @@ class DenCli::CMD 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 +54,58 @@ 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 end - def usage - args = - @options.map do |_, o| - s = "#{o.short}#{o.val ? ?= : ''}#{o.val}"; o.required? ? "#{s} " : "[#{s}] " - end - "#{full_cmd.join ' '} #{args.join ''}"+ - (@exe.lambda? ? ( - required.map{|s|"<#{s}>"}.join( " ")+ - (additional.empty? ? "" : " [#{additional.map{|s|"<#{s}>"}.join " "}]") - ) : '...') + def usage output: nil + output ||= '' + _usage output + output end - def options_help + def _usage output + output << full_cmd.join( ' ') + @options.each do |_, o| + s = "#{o.short||o.long}#{o.val ? ?= : ''}#{o.val}" + output << (o.required? ? " #{s}" : " [#{s}]") + end + if @exe.lambda? + required.each {|s| output << " <#{s}>" } + output << " [#{additional.map{|s|"<#{s}>"}.join " "}]" unless additional.empty? + else + output << ' ...' + end + end + + def commands *args, &exe + yield 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,8 +123,8 @@ 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 @@ -101,8 +135,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 @@ -116,34 +150,29 @@ class DenCli::CMD 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(--\[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 + @short, @val = $1, $2 || @val when /\A(-[^=-]+)\z/ - short, val = $1, nil - else raise ArgumentError, "Unexpected format for option: #{opt.inspect}" + @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, *alts, desc, default: NilClass, **os, &conv + @name, @desc, @default, @os, @conv, @val = + name.to_s.to_sym, desc, default, os, conv || lambda{|v|v}, nil + parse_opt_string opt + alts.each &method( :parse_opt_string) @req = if NilClass != default false @@ -155,6 +184,7 @@ class DenCli::CMD end def on parser, store + store[@name] = @default if default? parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val| store[@name] = @conv[val] end diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 5622d69..c52ddc7 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -15,16 +15,72 @@ class DenCli::Sub "#{full_cmd.join ' '} ..." end - def help n = nil, *a + 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? - end - r + output << "#{full_cmd.join ' '}: #{description}\n\n" + self.class._help_commands output, @subs elsif @aliases.has_key? n - @aliases[n].help *a + @aliases[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 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 + + def self._help_commands output, subs + m = subs.map {|c| x = c.usage.length; 25 < x ? 0 : x }.max + subs.each do |c| + if 25 < c.usage.length + output << "% -#{m}s\n#{' ' * m} " % [c.usage] + else + output << "% -#{m}s " % [c.usage] + end + n = m+2 + prefix = nil + c.description.split /\n/ do |l| + c = 0 + l.split %r< > 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 + + def goto *a + return self if a.empty? + n, *a = *a + if @aliases.has_key? n + @aliases[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