From 09c4c46d2557aed87f309cdcc417aa454c069e5e Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Tue, 30 Nov 2021 23:09:06 +0100 Subject: [PATCH 01/22] help_full for printing recursive all available commands. support for --[no-]opt. --- bin/example.rb | 18 +++++-- lib/dencli.rb | 18 ++++++- lib/dencli/cmd.rb | 116 +++++++++++++++++++++++++++++----------------- lib/dencli/sub.rb | 72 ++++++++++++++++++++++++---- 4 files changed, 166 insertions(+), 58 deletions(-) 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 From eb61dcbc389bff287f5a543ea8c282bb9fc5e9f8 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Tue, 30 Nov 2021 23:09:50 +0100 Subject: [PATCH 02/22] bump 0.5.0 --- lib/dencli/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index 1d939af..c45b719 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.4.0' + VERSION = '0.5.0' end From df15912e7a3a1fbc7b0678cb38d657ccf3f287f1 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Tue, 7 Dec 2021 23:48:28 +0100 Subject: [PATCH 03/22] Sub#commands|CMD#commands should yield(name,self). assert_type for debugging. Commented out. --- bin/example.rb | 5 +++-- lib/dencli.rb | 7 +++++++ lib/dencli/cmd.rb | 4 ++-- lib/dencli/sub.rb | 13 ++++++++++--- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index 03787b4..4291c54 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -4,8 +4,9 @@ require 'pathname' $:.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( :args, "Expects and prints given arguments", &lambda {|a, b, c:, d:, e:| +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:| p a: a, b: b, c: c, d: d, e: e }). opt( :c, '-c=ForC', "Option c"). diff --git a/lib/dencli.rb b/lib/dencli.rb index 7681856..c9c8264 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 diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 12cad0a..1e4bfe1 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -80,8 +80,8 @@ class DenCli::CMD end end - def commands *args, &exe - yield self + def commands *args + yield name, self end def goto *args diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index c52ddc7..15207d5 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -4,6 +4,9 @@ class DenCli::Sub attr_reader :parent, :name, :description, :subs, :aliases def initialize parent, name, description + #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}", {}, {} end @@ -33,7 +36,7 @@ class DenCli::Sub end def commands &exe - yield self + yield name, self @subs.each {|k, c| c.commands &exe } end @@ -44,8 +47,8 @@ class DenCli::Sub end def self._help_commands output, subs - m = subs.map {|c| x = c.usage.length; 25 < x ? 0 : x }.max - subs.each do |c| + m = subs.map {|_name,c| x = c.usage.length; 25 < x ? 0 : x }.max + subs.each do |_name, c| if 25 < c.usage.length output << "% -#{m}s\n#{' ' * m} " % [c.usage] else @@ -96,6 +99,10 @@ class DenCli::Sub end def _add name, min, obj, aliases + #DenCli::assert_type self, __method__, :name, name, Symbol, NilClass + #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 unless name.nil? @subs[name] = obj DenCli.gen_aliases( name, min) {|a| @aliases[a] = obj } From 41940d3f10182635ef6880e46c50c68a5208e7f1 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Tue, 7 Dec 2021 23:51:19 +0100 Subject: [PATCH 04/22] 0.5.1 --- lib/dencli/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index c45b719..8a883ee 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.5.0' + VERSION = '0.5.1' end From 10061b19189e33fd5a5b3fcb1bdc7a0cf8633e2a Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Wed, 8 Dec 2021 00:08:47 +0100 Subject: [PATCH 05/22] Missing argument(s) errors fixed. CMD: Methods -> Variables. --- lib/dencli/cmd.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 1e4bfe1..48aff2c 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -7,6 +7,12 @@ class DenCli::CMD def initialize parent, name, description, exe raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe @parent, @name, @description, @exe = parent, name, description, lambda( &exe) + @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,14 +20,11 @@ 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 complete *pre, str @completion.call *pre, str @@ -54,7 +57,7 @@ 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.long || o.short }.join ', '}" end end @exe.call *as, **os From ffd24c02f8280ce9f5077f807bf1e80c177451ae Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Wed, 8 Dec 2021 00:10:18 +0100 Subject: [PATCH 06/22] 0.5.2 --- lib/dencli/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index 8a883ee..f5797a8 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.5.1' + VERSION = '0.5.2' end From d2158e08ea1a92c4d9bfbdb34a493c6230351405 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Wed, 8 Dec 2021 00:17:37 +0100 Subject: [PATCH 07/22] Usage of short options without `=` --- bin/example.rb | 8 +++++--- lib/dencli/cmd.rb | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index 4291c54..4651540 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -6,13 +6,15 @@ require 'dencli' 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:| + &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( :d, '-dForD', "Option d", default: "something"). opt( :e, '-e', "Toggle e", default: false). - opt( :f, '--[no-]f', "Toggle f", 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, "", aliases: [nil, '-h', '--help'], &lambda {|*args, full:| diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 48aff2c..e5d80ff 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -72,7 +72,7 @@ class DenCli::CMD def _usage output output << full_cmd.join( ' ') @options.each do |_, o| - s = "#{o.short||o.long}#{o.val ? ?= : ''}#{o.val}" + s = "#{o.short||o.long}#{(!o.short && o.val) ? ?= : ''}#{o.val}" output << (o.required? ? " #{s}" : " [#{s}]") end if @exe.lambda? @@ -161,9 +161,9 @@ class DenCli::CMD @long, @val = $1, $2 || @val when /\A(--[^=]+)\z/ @long, @val = $1, nil - when /\A(-[^=-]+)=(.+)\z/ + when /\A(-[^=-])=?(.+)\z/ @short, @val = $1, $2 || @val - when /\A(-[^=-]+)\z/ + when /\A(-[^=-])\z/ @short, @val = $1, nil else raise ArgumentError, "Unexpected format for option: #{opt.inspect}" From 6e253b8b114600df17b304f994eea72ae575818c Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Wed, 8 Dec 2021 00:17:57 +0100 Subject: [PATCH 08/22] 0.5.3 --- lib/dencli/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index f5797a8..90d2ced 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.5.2' + VERSION = '0.5.3' end From 3cc205cb02ba2c51832f4ffdef848320f5f41c5f Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Wed, 8 Dec 2021 23:52:05 +0100 Subject: [PATCH 09/22] ruby-2.5-support fix. usage with rest-arguments. ruby-2.5 does not support `String#split() {|t|}`. splits are replaced by `String#split().each {|t|}`. Usage supports printing rest-arguments. It uses the name with three dots: command [ ...] Sub-commands has a generic ` ...` or `[ ...]`. The last will be used if a nil-aliases exists. --- bin/example.rb | 8 +++--- lib/dencli/cmd.rb | 14 +++++++-- lib/dencli/sub.rb | 73 ++++++++++++++++++++++++++++------------------- 3 files changed, 58 insertions(+), 37 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index 4651540..557cbff 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -17,11 +17,11 @@ cli.cmd( :args, "Expects and prints given arguments", 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, "", aliases: [nil, '-h', '--help'], &lambda {|*args, full:| +cli.cmd( :help, "An example for help", aliases: [nil, '-h', '--help'], &lambda {|*commands, full:| if full - cli.help_full *args, output: STDERR + cli.help_full *commands, output: STDERR else - cli.help *args, output: STDERR + cli.help *commands, output: STDERR end }). opt( :full, '-f', '--[no-]full', "Print all commands and sub-commands.", default: false) @@ -40,7 +40,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| sub2.help( *args, output: STDERR) }) + 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| diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index e5d80ff..b60c895 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -76,8 +76,16 @@ class DenCli::CMD output << (o.required? ? " #{s}" : " [#{s}]") end if @exe.lambda? - required.each {|s| output << " <#{s}>" } - output << " [#{additional.map{|s|"<#{s}>"}.join " "}]" unless additional.empty? + parameters.each do |(type, name)| + case type + when :req + output << " <#{name}>" + when :opt + output << " [<#{name}>]" + when :rest + output << " [<#{name}> ...]" + end + end else output << ' ...' end @@ -130,7 +138,7 @@ class DenCli::CMD @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 diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 15207d5..16683d5 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -14,8 +14,19 @@ class DenCli::Sub def full_cmd() _full_cmd [] end def []( k) @aliases[k] end - def usage - "#{full_cmd.join ' '} ..." + def usage output: nil + output ||= '' + _usage output + output + end + + def _usage output + output << full_cmd.join( ' ') + if @aliases.has_key? nil + output << " [ ...]" + else + output << " [...]" + end end def help n = nil, *a, output: nil @@ -46,37 +57,39 @@ class DenCli::Sub output end - def self._help_commands output, subs - m = subs.map {|_name,c| x = c.usage.length; 25 < x ? 0 : x }.max - subs.each do |_name, 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 + 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 << "\n" + output end - output end def goto *a From e83a7b43610163e909d78bfb197a153282040966 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Wed, 8 Dec 2021 23:57:23 +0100 Subject: [PATCH 10/22] v0.5.4 --- lib/dencli/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index 90d2ced..95cec86 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.5.3' + VERSION = '0.5.4' end From 6254349aa9114b2deb1d269b543ee48ae8296fc5 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Thu, 9 Dec 2021 13:02:12 +0100 Subject: [PATCH 11/22] tests implemented. do not overwrite cmds by aliases. `call *a, **o` behaviour differs 2.5 <=> 2.7 --- bin/example.rb | 204 +++++++++++++++++++++++++++++++++++--- lib/dencli.rb | 2 +- lib/dencli/cmd.rb | 8 +- lib/dencli/interactive.rb | 2 +- lib/dencli/sub.rb | 4 +- 5 files changed, 202 insertions(+), 18 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index 557cbff..e98ea76 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -3,8 +3,188 @@ require 'pathname' $:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s require 'dencli' +require 'shellwords' +require 'stringio' 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 command + STDERR.printf "[% 4d] \e[1;32mok\e[0m %s tests %s\e[J\n", @counter, $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 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 command + else + logfail command + loginfo "expected args: #{expect.inspect}" + loginfo "given args: #{@args.inspect}" + STDERR.puts + end + rescue SystemExit + if 0 == $!.status + logok command + else + logfail command + end + rescue Object + logfail command + loginfo "unexpected raise: (#{$!.class.name}) #$!" + 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 command + if @verbose + loginfo "raised: (#{$!.class.name}) #$!" + STDERR.puts + end + else + loginfo "unexpected message: (#{$!.class.name}) #$!" + STDERR.puts + end + rescue Object + logfail command + loginfo "unexpected raised: (#{$!.class.name}) #$!" + STDERR.puts + end +end + +cli.sub :tests, "Some tests" do |tcli| + 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( :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' + end.opt( :verbose, '-v', 'Prints additional information per test', default: false) +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 @@ -16,21 +196,21 @@ cli.cmd( :args, "Expects and prints given arguments", 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( :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 + cli.help_full *commands, output: $stderr else - cli.help *commands, output: STDERR + 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( :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 @@ -40,12 +220,12 @@ 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 {|*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!" } + 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 @@ -53,6 +233,6 @@ 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 c9c8264..a4970d6 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -96,7 +96,7 @@ class DenCli end def help_full *args, output: nil - output ||= STDOUT + output ||= $stdout x = @subs.goto *args _help_full output, x end diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index b60c895..f057b2d 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -60,7 +60,11 @@ class DenCli::CMD 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 output: nil @@ -87,7 +91,7 @@ class DenCli::CMD end end else - output << ' ...' + output << ' [...]' end end diff --git a/lib/dencli/interactive.rb b/lib/dencli/interactive.rb index d03b482..be778c2 100644 --- a/lib/dencli/interactive.rb +++ b/lib/dencli/interactive.rb @@ -121,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 16683d5..3277e50 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -118,7 +118,7 @@ class DenCli::Sub #DenCli::assert_type self, __method__, :aliases, aliases, Array, NilClass name = name.to_s unless name.nil? @subs[name] = obj - DenCli.gen_aliases( name, min) {|a| @aliases[a] = obj } + DenCli.gen_aliases( name, min) {|a| @aliases[a] ||= obj } if aliases [*aliases].each {|a| @aliases[a] = obj } end @@ -141,7 +141,7 @@ class DenCli::Sub elsif sub = @subs[pre[0]] sub.complete *pre[1..-1], str else - STDOUT.print "\a" + $stdout.print "\a" end end From 2c654f2941291fc6b697b54caa8cc6c54341bf28 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Thu, 9 Dec 2021 13:17:13 +0100 Subject: [PATCH 12/22] noshortaliases. warn or raise if alias already exists --- bin/example.rb | 3 +-- lib/dencli/cmd.rb | 4 ++-- lib/dencli/sub.rb | 23 ++++++++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index e98ea76..3b9c839 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -90,7 +90,7 @@ class Capture end end -cli.sub :tests, "Some tests" do |tcli| +cli.sub :tests, "Some tests", noshortaliases: true do |tcli| 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] }) @@ -208,7 +208,6 @@ cli.cmd( :help, "An example for help", aliases: [nil, '-h', '--help'], &lambda { 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"} diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index f057b2d..cb24e9a 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -4,9 +4,9 @@ require_relative '../dencli' class DenCli::CMD attr_reader :parent, :name, :description, :exe, :completion, :options - def initialize parent, name, description, exe + def initialize parent, name, description, exe, noshortaliases: nil raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe - @parent, @name, @description, @exe = parent, name, description, lambda( &exe) + @parent, @name, @description, @exe, @noshortaliases = parent, name, description, lambda( &exe), noshortaliases @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] } diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 3277e50..03dc0de 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -3,11 +3,12 @@ require_relative '../dencli' class DenCli::Sub attr_reader :parent, :name, :description, :subs, :aliases - def initialize parent, name, description + def initialize parent, name, description, noshortaliases: 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 = ! ! noshortaliases end def _full_cmd( post) parent._full_cmd [@name]+post end @@ -118,21 +119,29 @@ class DenCli::Sub #DenCli::assert_type self, __method__, :aliases, aliases, Array, NilClass name = name.to_s unless name.nil? @subs[name] = obj - DenCli.gen_aliases( name, min) {|a| @aliases[a] ||= obj } + if @noshortaliases + DenCli.gen_aliases( name, min) {|a| @aliases[a] ||= obj } + else + warn "Alias already exists: #{full_cmd.join ' '} #{name}" if @aliases.has_key? name + @aliases[name] = obj + end if aliases - [*aliases].each {|a| @aliases[a] = obj } + [*aliases].each do |a| + raise ArgumentError, "Alias already exists: #{full_cmd.join ' '} #{a}" 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 + def sub name, description, min: nil, aliases: nil, noshortaliases: nil, &exe + r = _add name, min, DenCli::Sub.new( self, name, description, noshortaliases: noshortaliases), 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 + def cmd name, description, min: nil, aliases: nil, noshortaliases: nil, &exe + _add name, min, DenCli::CMD.new( self, name, description, exe, noshortaliases: noshortaliases), aliases end def complete *pre, str From 7be062c1dbf37b024721560a7eb3cd675618f43c Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Thu, 9 Dec 2021 13:28:46 +0100 Subject: [PATCH 13/22] min: false for no shorts. noshortaliases only for Sub-commands --- lib/dencli.rb | 9 +++++++-- lib/dencli/cmd.rb | 4 ++-- lib/dencli/sub.rb | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/dencli.rb b/lib/dencli.rb index a4970d6..45a233e 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -51,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 diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index cb24e9a..f057b2d 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -4,9 +4,9 @@ require_relative '../dencli' class DenCli::CMD attr_reader :parent, :name, :description, :exe, :completion, :options - def initialize parent, name, description, exe, noshortaliases: nil + def initialize parent, name, description, exe raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe - @parent, @name, @description, @exe, @noshortaliases = parent, name, description, lambda( &exe), noshortaliases + @parent, @name, @description, @exe = parent, name, description, lambda( &exe) @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] } diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 03dc0de..7b71093 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -140,8 +140,8 @@ class DenCli::Sub block_given? ? yield( r) : r end - def cmd name, description, min: nil, aliases: nil, noshortaliases: nil, &exe - _add name, min, DenCli::CMD.new( self, name, description, exe, noshortaliases: noshortaliases), 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 From ec025652c9e8362b8dc63f84501a2ec4d9f14cc2 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Thu, 9 Dec 2021 14:04:27 +0100 Subject: [PATCH 14/22] noshortaliases fixed (meaning swapped) --- lib/dencli/sub.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 7b71093..31a2acd 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -120,10 +120,13 @@ class DenCli::Sub name = name.to_s unless name.nil? @subs[name] = obj if @noshortaliases - DenCli.gen_aliases( name, min) {|a| @aliases[a] ||= obj } - else - warn "Alias already exists: #{full_cmd.join ' '} #{name}" if @aliases.has_key? name + warn "Command/Alias already exists: #{full_cmd.join ' '} #{name}" if @aliases.has_key? name @aliases[name] = obj + else + DenCli.gen_aliases name, min do |a| + warn "Command/Alias already exists: #{full_cmd.join ' '} #{a}" if @aliases.has_key? a + @aliases[a] ||= obj + end end if aliases [*aliases].each do |a| From 70783052560c344529bc736c95a6362af89a17bf Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Thu, 9 Dec 2021 14:11:45 +0100 Subject: [PATCH 15/22] print exception for tests (if occur or -v) --- bin/example.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index 3b9c839..006c188 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -41,6 +41,11 @@ class Capture 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 @@ -64,7 +69,7 @@ class Capture end rescue Object logfail command - loginfo "unexpected raise: (#{$!.class.name}) #$!" + logexception "unexpected raise:", $! STDERR.puts end @@ -76,16 +81,16 @@ class Capture if message === $!.message logok command if @verbose - loginfo "raised: (#{$!.class.name}) #$!" + logexception "raised:", $! STDERR.puts end else - loginfo "unexpected message: (#{$!.class.name}) #$!" + logexception "unexpected message:", $! STDERR.puts end rescue Object logfail command - loginfo "unexpected raised: (#{$!.class.name}) #$!" + logexception "unexpected raised:", $! STDERR.puts end end From a00149ce6a997ad0b9471444f5adc276704166e3 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Fri, 31 Dec 2021 13:47:54 +0100 Subject: [PATCH 16/22] ruby2.5: @os removed - OptionParser does not expect any **options, so we need not it. --- bin/example.rb | 25 ++++++++++++++++++------- lib/dencli/cmd.rb | 27 +++++++++++++++------------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index 006c188..2575dc7 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -1,10 +1,9 @@ #!/usr/bin/env ruby require 'pathname' -$:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s -require 'dencli' require 'shellwords' require 'stringio' +require_relative '../lib/dencli' cli = DenCli.new :example, "This is an example for generate a DenCli-API" @@ -33,8 +32,8 @@ class Capture STDERR.printf "[% 4d] \e[1;35m? \e[0m %s tests %s\r", @counter, $0.shellescape, command.shelljoin end - def logok command - STDERR.printf "[% 4d] \e[1;32mok\e[0m %s tests %s\e[J\n", @counter, $0.shellescape, command.shelljoin + 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 @@ -54,7 +53,7 @@ class Capture logstart command $capture.capture { @cli.call 'tests', *command } if expect === @args - logok command + logok @args, command else logfail command loginfo "expected args: #{expect.inspect}" @@ -63,7 +62,7 @@ class Capture end rescue SystemExit if 0 == $!.status - logok command + logok @args, command else logfail command end @@ -79,7 +78,7 @@ class Capture logfail command rescue exception if message === $!.message - logok command + logok exception, command if @verbose logexception "raised:", $! STDERR.puts @@ -96,6 +95,14 @@ class Capture end cli.sub :tests, "Some tests", noshortaliases: true do |tcli| + 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] }) @@ -109,6 +116,7 @@ cli.sub :tests, "Some tests", noshortaliases: true do |tcli| 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 @@ -187,6 +195,9 @@ cli.sub :tests, "Some tests", noshortaliases: true do |tcli| $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 diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index f057b2d..e16385d 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -160,7 +160,7 @@ 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 @@ -183,11 +183,12 @@ class DenCli::CMD 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 + 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 - alts.each &method( :parse_opt_string) + @type = args.pop if OptionParser.top.atype.has_key? args.last + args.each &method( :parse_opt_string) @req = if NilClass != default false @@ -200,30 +201,32 @@ class DenCli::CMD def on parser, store store[@name] = @default if default? - parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val| + 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, &conv + r = Opt.new( self, name, opt, *args, desc, &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 From 616989e96aa6cde3ff1fd46d760240559f058ea6 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Fri, 31 Dec 2021 13:55:27 +0100 Subject: [PATCH 17/22] ruby3: Opt.new: optional default - CMD.opt needs support. --- bin/example.rb | 1 + lib/dencli/cmd.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index 2575dc7..d1b8fbc 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -3,6 +3,7 @@ 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" diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index e16385d..0d214f5 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -217,8 +217,8 @@ class DenCli::CMD end end - def opt name, opt, *args, desc, &conv - r = Opt.new( self, name, opt, *args, desc, &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 From a630da465a30b242492f1ee6b92535e6dca16f52 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Fri, 31 Dec 2021 13:56:59 +0100 Subject: [PATCH 18/22] bump 0.5.5 --- lib/dencli/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index 95cec86..835f9d2 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.5.4' + VERSION = '0.5.5' end From ba922fe52ca0cdc23e909e8c222a768f38a077ef Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Fri, 31 Dec 2021 15:13:40 +0100 Subject: [PATCH 19/22] warnings/exception because of already used aliases more verbose. --- lib/dencli/sub.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 31a2acd..0fc6841 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -120,17 +120,17 @@ class DenCli::Sub name = name.to_s unless name.nil? @subs[name] = obj if @noshortaliases - warn "Command/Alias already exists: #{full_cmd.join ' '} #{name}" if @aliases.has_key? name + warn "Command/Alias for [#{obj.full_cmd}] already exists: #{full_cmd.join ' '} #{name}. Used by: #{@aliases[name].full_cmd}" if @aliases.has_key? name @aliases[name] = obj else DenCli.gen_aliases name, min do |a| - warn "Command/Alias already exists: #{full_cmd.join ' '} #{a}" if @aliases.has_key? a + warn "Command/Alias for [#{obj.full_cmd}] already exists: #{full_cmd.join ' '} #{a}. Used by: #{@aliases[a].full_cmd}" if @aliases.has_key? a @aliases[a] ||= obj end end if aliases [*aliases].each do |a| - raise ArgumentError, "Alias already exists: #{full_cmd.join ' '} #{a}" if @aliases.has_key? a + raise ArgumentError, "Alias for [#{obj.full_cmd}] already exists: #{full_cmd.join ' '} #{a}. Used by: #{@aliases[a].full_cmd}" if @aliases.has_key? a @aliases[a] = obj end end From 8fe78bb406a41b214b4622919181fd3b11401458 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Fri, 31 Dec 2021 15:17:46 +0100 Subject: [PATCH 20/22] CMD-options: OptionParser#on support "--abc STR". implemented for CMD#opt, too. --- lib/dencli/cmd.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 0d214f5..9445514 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -167,15 +167,15 @@ class DenCli::CMD def parse_opt_string opt case opt - when /\A(--\[no-\][^=]+)\z/ + when /\A(--\[no-\][^= ]+)\z/ @long, @val = $1, nil - when /\A(--[^=]+)=(.+)\z/ + when /\A(--[^= ]+)[= ](.+)\z/ @long, @val = $1, $2 || @val - when /\A(--[^=]+)\z/ + when /\A(--[^= ]+)\z/ @long, @val = $1, nil - when /\A(-[^=-])=?(.+)\z/ + when /\A(-[^= -])[= ]?(.+)\z/ @short, @val = $1, $2 || @val - when /\A(-[^=-])\z/ + when /\A(-[^= -])\z/ @short, @val = $1, nil else raise ArgumentError, "Unexpected format for option: #{opt.inspect}" From 09b467f7bd5819b0b90b45cb072694a54c8103ae Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Sun, 30 Jan 2022 01:41:54 +0100 Subject: [PATCH 21/22] defined_in for determine already defined commands/aliases. --- lib/dencli/cmd.rb | 6 +++--- lib/dencli/sub.rb | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb index 9445514..3d65ce0 100644 --- a/lib/dencli/cmd.rb +++ b/lib/dencli/cmd.rb @@ -2,11 +2,11 @@ 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] } diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 0fc6841..19f9357 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -1,14 +1,14 @@ 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, noshortaliases: nil + 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 = ! ! noshortaliases + @noshortaliases, @defined_in = ! ! noshortaliases, defined_in || Kernel.caller end def _full_cmd( post) parent._full_cmd [@name]+post end @@ -120,17 +120,17 @@ class DenCli::Sub name = name.to_s unless name.nil? @subs[name] = obj if @noshortaliases - warn "Command/Alias for [#{obj.full_cmd}] already exists: #{full_cmd.join ' '} #{name}. Used by: #{@aliases[name].full_cmd}" if @aliases.has_key? name + 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}] already exists: #{full_cmd.join ' '} #{a}. Used by: #{@aliases[a].full_cmd}" if @aliases.has_key? 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 do |a| - raise ArgumentError, "Alias for [#{obj.full_cmd}] already exists: #{full_cmd.join ' '} #{a}. Used by: #{@aliases[a].full_cmd}" if @aliases.has_key? a + 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 @@ -138,13 +138,13 @@ class DenCli::Sub end private :_add - def sub name, description, min: nil, aliases: nil, noshortaliases: nil, &exe - r = _add name, min, DenCli::Sub.new( self, name, description, noshortaliases: noshortaliases), aliases + def sub name, description, min: nil, aliases: nil, noshortaliases: nil, defined_in: nil, &exe + r = _add name, 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 + 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 From c946a093f79f7538773e0551359f4322ff2b1d51 Mon Sep 17 00:00:00 2001 From: Denis Knauf Date: Sun, 30 Jan 2022 14:18:20 +0100 Subject: [PATCH 22/22] aliases are strings only or nil. more examples. --- bin/example.rb | 99 ++++++++++++++++++++++----------------- lib/dencli.rb | 7 ++- lib/dencli/interactive.rb | 19 +++++--- lib/dencli/sub.rb | 61 ++++++++++++++++++++---- 4 files changed, 121 insertions(+), 65 deletions(-) diff --git a/bin/example.rb b/bin/example.rb index d1b8fbc..ab13bff 100755 --- a/bin/example.rb +++ b/bin/example.rb @@ -95,7 +95,62 @@ class Capture 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, "", 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, "", 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- ... 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 @@ -202,50 +257,6 @@ cli.sub :tests, "Some tests", noshortaliases: true do |tcli| end.opt( :verbose, '-v', 'Prints additional information per test', default: false) 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, "", 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, "", 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!" } - end - end -end - begin cli.call *ARGV rescue DenCli::UsageError diff --git a/lib/dencli.rb b/lib/dencli.rb index 45a233e..26ee3ae 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -80,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 @@ -110,10 +113,6 @@ class DenCli Sub._help_commands output, subs.to_enum( :commands) end - def [] k - @subs[k] - end - def interactive *args, **opts Interactive.new self, *args, **opts end diff --git a/lib/dencli/interactive.rb b/lib/dencli/interactive.rb index be778c2..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}" diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb index 19f9357..d8c34e6 100644 --- a/lib/dencli/sub.rb +++ b/lib/dencli/sub.rb @@ -13,7 +13,9 @@ class DenCli::Sub 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 output: nil output ||= '' @@ -40,8 +42,8 @@ class DenCli::Sub if n.nil? output << "#{full_cmd.join ' '}: #{description}\n\n" self.class._help_commands output, @subs - elsif @aliases.has_key? n - @aliases[n]._help output, *a + 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 @@ -96,8 +98,8 @@ class DenCli::Sub def goto *a return self if a.empty? n, *a = *a - if @aliases.has_key? n - @aliases[n].goto *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 @@ -105,19 +107,19 @@ 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 - #DenCli::assert_type self, __method__, :name, name, Symbol, NilClass + #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 unless name.nil? + name = name.to_s @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 @@ -130,6 +132,7 @@ class DenCli::Sub end 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 @@ -138,11 +141,49 @@ class DenCli::Sub 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, min, DenCli::Sub.new( self, name, description, noshortaliases: noshortaliases, defined_in: defined_in || Kernel.caller.first), aliases + 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 + # 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