Compare commits

...

29 Commits

Author SHA1 Message Date
Denis Knauf c946a093f7 aliases are strings only or nil. more examples. 2022-01-30 14:18:20 +01:00
Denis Knauf 09b467f7bd defined_in for determine already defined commands/aliases. 2022-01-30 01:41:54 +01:00
Denis Knauf 8fe78bb406 CMD-options: OptionParser#on support "--abc STR". implemented for CMD#opt, too. 2021-12-31 15:17:46 +01:00
Denis Knauf ba922fe52c warnings/exception because of already used aliases more verbose. 2021-12-31 15:13:40 +01:00
Denis Knauf a630da465a bump 0.5.5 2021-12-31 13:56:59 +01:00
Denis Knauf 616989e96a ruby3: Opt.new: optional default - CMD.opt needs support. 2021-12-31 13:55:27 +01:00
Denis Knauf a00149ce6a ruby2.5: @os removed - OptionParser does not expect any **options, so we need not it. 2021-12-31 13:47:54 +01:00
Denis Knauf 7078305256 print exception for tests (if occur or -v) 2021-12-09 14:16:08 +01:00
Denis Knauf ec025652c9 noshortaliases fixed (meaning swapped) 2021-12-09 14:04:27 +01:00
Denis Knauf 7be062c1db min: false for no shorts. noshortaliases only for Sub-commands 2021-12-09 13:28:46 +01:00
Denis Knauf 2c654f2941 noshortaliases. warn or raise if alias already exists 2021-12-09 13:17:13 +01:00
Denis Knauf 6254349aa9 tests implemented. do not overwrite cmds by aliases. `call *a, **o` behaviour differs 2.5 <=> 2.7 2021-12-09 13:02:12 +01:00
Denis Knauf e83a7b4361 v0.5.4 2021-12-08 23:57:23 +01:00
Denis Knauf 3cc205cb02 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 [<argname> ...]

Sub-commands has a generic `<command> ...` or `[<command> ...]`.
The last will be used if a nil-aliases exists.
2021-12-08 23:52:05 +01:00
Denis Knauf 6e253b8b11 0.5.3 2021-12-08 00:17:57 +01:00
Denis Knauf d2158e08ea Usage of short options without `=` 2021-12-08 00:17:37 +01:00
Denis Knauf ffd24c02f8 0.5.2 2021-12-08 00:10:18 +01:00
Denis Knauf 10061b1918 Missing argument(s) errors fixed. CMD: Methods -> Variables. 2021-12-08 00:08:47 +01:00
Denis Knauf 41940d3f10 0.5.1 2021-12-07 23:51:19 +01:00
Denis Knauf d8ecfaeb86 Merge branch 'master' of git.denkn.at:deac/dencli 2021-12-07 23:50:48 +01:00
Denis Knauf df15912e7a Sub#commands|CMD#commands should yield(name,self).
assert_type for debugging.  Commented out.
2021-12-07 23:48:28 +01:00
Denis Knauf eb61dcbc38 bump 0.5.0 2021-11-30 23:09:50 +01:00
Denis Knauf 09c4c46d25 help_full for printing recursive all available commands. support for --[no-]opt. 2021-11-30 23:09:06 +01:00
Denis Knauf d3930c00bc opts-mething redesign. completion improved. example: args-command for demonstrate arguments and options. 2021-11-30 15:23:05 +01:00
Denis Knauf 5b5b620601 version 0.3.2. calling uses kvar-splats. 2021-09-06 18:34:31 +02:00
Denis Knauf ac5e852692 bump 0.3.1 2021-01-03 16:29:05 +01:00
Denis Knauf fa5fbf8f6f options instead of arguments. arguments checking against arity if lambda. help improved. 2021-01-03 16:28:12 +01:00
Denis Knauf 27ca6faf31 desc renamed to description. arguments implemented (first dirty try) 2021-01-03 13:42:00 +01:00
Denis Knauf cae3692008 Fixing Exceptions and Exception-Handling 2020-12-28 00:29:54 +01:00
6 changed files with 679 additions and 72 deletions

View File

@ -1,25 +1,265 @@
#!/usr/bin/env ruby
require 'dencli'
require 'pathname'
require 'shellwords'
require 'stringio'
require 'ipaddr'
require_relative '../lib/dencli'
cli = DenCli.new 'example', "This is an example for generate a DenCli-API"
cli.cmd( :example, "I have an example command") { STDERR.puts "This is an example" }
cli.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts cli.help(*args) }
cli = DenCli.new :example, "This is an example for generate a DenCli-API"
class Capture
def initialize cli, verbose:
@cli, @counter, @verbose = cli, 0, verbose
end
def capture
@args = NilClass
@counter += 1
stdout, stderr = $stdout, $stderr
$stdout = $stderr = StringIO.new
begin
yield stdout, stderr
ensure
$stderr, $stdout = stderr, stdout
end
end
def args= args
@args = args
end
def logstart command
STDERR.printf "[% 4d] \e[1;35m? \e[0m %s tests %s\r", @counter, $0.shellescape, command.shelljoin
end
def logok info, command
STDERR.printf "[% 4d] \e[1;32mok\e[0m %s | %s tests %s\e[J\n", @counter, info, $0.shellescape, command.shelljoin
end
def logfail command
STDERR.printf "[% 4d] \e[1;31mer\e[0m %s tests %s\e[J\n", @counter, $0.shellescape, command.shelljoin
end
def logexception prefix, exception
loginfo "#{prefix} (#{exception.class.name}) #{exception}"
exception.backtrace[0...-Kernel.caller.length].each {|l| loginfo " #{l}" }
end
def loginfo text
STDERR.printf " %s\n", text
end
def should_ok expect, *command
logstart command
$capture.capture { @cli.call 'tests', *command }
if expect === @args
logok @args, command
else
logfail command
loginfo "expected args: #{expect.inspect}"
loginfo "given args: #{@args.inspect}"
STDERR.puts
end
rescue SystemExit
if 0 == $!.status
logok @args, command
else
logfail command
end
rescue Object
logfail command
logexception "unexpected raise:", $!
STDERR.puts
end
def should_fail exception, message, *command
logstart command
$capture.capture { @cli.call 'tests', *command }
logfail command
rescue exception
if message === $!.message
logok exception, command
if @verbose
logexception "raised:", $!
STDERR.puts
end
else
logexception "unexpected message:", $!
STDERR.puts
end
rescue Object
logfail command
logexception "unexpected raised:", $!
STDERR.puts
end
end
cli.cmd( :args, "Expects and prints given arguments",
&lambda {|a, b, c:, d:, e:, f:, g:|
p a: a, b: b, c: c, d: d, e: e
}).
opt( :c, '-c=ForC', "Option c").
opt( :d, '-dForD', "Option d", default: "something").
opt( :e, '-e', "Toggle e", default: false).
opt( :f, '--[no-]f', "Toggle f", default: false).
opt( :g, '--long-option=sth', "Long option, no short option", default: "nothing").
opt( :h, '-hsth', "No long option, only short option", default: "nothing")
cli.cmd( :example, "I have an example command") { $stderr.puts "This is an example" }
cli.cmd( :help, "An example for help", aliases: [nil, '-h', '--help'], &lambda {|*commands, full:|
if full
cli.help_full *commands, output: $stderr
else
cli.help *commands, output: $stderr
end
}).
opt( :full, '-f', '--[no-]full', "Print all commands and sub-commands.", default: false)
cli.sub( :more, "Sub-Commands are also possible with a new cli") do |sub|
sub.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', *args) }
sub.cmd( :example, "Here is an example, too") { STDERR.puts "This is an other example" }
sub.cmd( :foo, "BAR") { STDERR.puts "FOO bar"}
sub.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| $stderr.puts sub.help(*args) }
sub.cmd( :example, "Here is an example, too") { $stderr.puts "This is an other example" }
sub.cmd( :foo, "BAR") { $stderr.puts "FOO bar"}
sub.cmd( :args, "Expects and prints given arguments", &lambda {|a, b=1, c:, d: 5, e:|
p a: a, b: b, c: c, d: d, e: e
}).
opt( :c, '-c=ForC', "Option c").
opt( :d, '-d=ForD', "Option d (implicit default)").
opt( :e, '-e', "Toggle e")
sub.sub( :deeper, "You want to have Sub-Sub-Commands?") do |sub2|
sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', *args) }
sub2.cmd( :last, "The last example") { STDERR.puts "The last example" }
sub2.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*commands| sub2.help( *commands, output: $stderr) })
sub2.cmd( :last, "The last example", &lambda { $stderr.puts "The last example" })
sub2.sub( :'sub-commands', "Endless Sub-Sub- ...") do |sub3|
sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', 'sub-commands', *args) }
sub3.cmd( :hehe, "The real last example", min: 2) { STDERR.puts "Trust me!" }
sub2.sub( :'sub-commands', "Endless Sub-Sub- ... with a special alias") do |sub3|
# h -> help
# he -> hehe
# hel -> help
# help -> help
# heh -> hehe
# hehe -> hehe
sub3.cmd( :help, "", min: 3, aliases: [nil, :h]) {|*args| $stderr.puts sub3.help( *args) }
sub3.cmd( :hehe, "The real last example", min: 2) { $stderr.puts "Trust me!" }
end
end
end
cli.call *ARGV
cli.cmd( :cli, "Interactive shell", min: 3, &lambda {||
cli.interactive( File.basename($0, '.rb')).run
})
cli.sub :tests, "Some tests", noshortaliases: true do |tcli|
tcli.cmd( :help, "", min: 4) {|*args| $stderr.puts tcli.help( *args) }
OptionParser.accept IPAddr do |arg|
begin
IPAddr.new arg
rescue IPAddr::InvalidAddressError
raise OptionParser::InvalidArgument, "#{$!.message}: #{arg}"
end
end
tcli.cmd( :'-', "No arguments no options expected", &lambda {|| $capture.args = [] })
tcli.cmd( :'arg', "", &lambda {|one| $capture.args = [one] })
tcli.cmd( :'oar', "", &lambda {|one=nil| $capture.args = [one] })
tcli.cmd( :'arg-arg', "", &lambda {|one, two| $capture.args = [one, two] })
tcli.cmd( :'oar-oar', "", &lambda {|one=nil, two=nil| $capture.args = [one, two] })
tcli.cmd( :'arg-oar', "expected", &lambda {|one, two=nil| $capture.args = [one, two] })
tcli.cmd( :'oar-arg', "expected", &lambda {|one=nil, two| $capture.args = [one, two] })
tcli.cmd( :'bool', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '')
tcli.cmd( :'optbool', '', &lambda {|a: nil| $capture.args = [a] }).opt(:a, '-a', '')
tcli.cmd( :'defbool', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '', default: 'default')
tcli.cmd( :'str', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a=STR', '')
tcli.cmd( :'lstr', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '--astr=STR', '')
tcli.cmd( :'bstr', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '--astr=STR', '')
tcli.cmd( :'ipaddr', '', &lambda {|a:| $capture&.args = [a] }).opt(:a, '-a', '--addr=ADDR', IPAddr, '')
tcli.cmd( :run, "Run all tests") do |verbose:|
$capture = Capture.new cli, verbose: verbose
$capture.should_fail DenCli::UnknownCommand, //, 'unknown-command'
$capture.should_ok [], '-'
$capture.should_fail DenCli::UsageError, //, '-', 'unexpected'
$capture.should_ok %w[first], 'arg', 'first'
$capture.should_fail DenCli::UsageError, //, 'arg'
$capture.should_fail DenCli::UsageError, //, 'arg', 'first', 'unexpected'
$capture.should_ok %w[first], 'oar', 'first'
$capture.should_ok [nil], 'oar'
$capture.should_fail DenCli::UsageError, //, 'oar', 'first', 'unexpected'
$capture.should_ok %w[first two], 'oar-oar', 'first', 'two'
$capture.should_ok ['first', nil], 'oar-oar', 'first'
$capture.should_ok [nil,nil], 'oar-oar'
$capture.should_fail DenCli::UsageError, //, 'oar-oar', 'first', 'two', 'unexpected'
$capture.should_ok %w[first two], 'arg-oar', 'first', 'two'
$capture.should_ok ['first', nil], 'arg-oar', 'first'
$capture.should_fail DenCli::UsageError, //, 'arg-oar'
$capture.should_ok [nil, 'first'], 'oar-arg', 'first'
$capture.should_ok ['first', 'second'], 'oar-arg', 'first', 'second'
$capture.should_fail DenCli::UsageError, //, 'oar-arg'
$capture.should_fail DenCli::UsageError, //, 'oar-arg', 'first', 'two', 'unexpected'
$capture.should_ok [true], 'bool', '-a'
$capture.should_fail DenCli::UsageError, //, 'bool'
$capture.should_fail DenCli::UsageError, //, 'bool', '-a', 'unexpected'
$capture.should_fail OptionParser::InvalidOption, //, 'bool', '-b'
$capture.should_fail OptionParser::InvalidOption, //, 'bool', '--unexpected'
$capture.should_fail DenCli::UsageError, //, 'bool', 'unexpected'
$capture.should_ok [true], 'optbool', '-a'
$capture.should_ok [nil], 'optbool'
$capture.should_fail DenCli::UsageError, //, 'optbool', '-a', 'unexpected'
$capture.should_fail OptionParser::InvalidOption, //, 'optbool', '-b'
$capture.should_fail OptionParser::InvalidOption, //, 'optbool', '--unexpected'
$capture.should_fail DenCli::UsageError, //, 'optbool', 'unexpected'
$capture.should_ok [true], 'defbool', '-a'
$capture.should_ok ['default'], 'defbool'
$capture.should_fail DenCli::UsageError, //, 'defbool', '-a', 'unexpected'
$capture.should_fail OptionParser::InvalidOption, //, 'defbool', '-b'
$capture.should_fail OptionParser::InvalidOption, //, 'defbool', '--unexpected'
$capture.should_fail DenCli::UsageError, //, 'defbool', 'unexpected'
$capture.should_ok %w[first], 'str', '-a', 'first'
$capture.should_ok %w[first], 'str', '-afirst'
$capture.should_fail OptionParser::MissingArgument, //, 'str', '-a'
$capture.should_fail DenCli::UsageError, //, 'str'
$capture.should_fail OptionParser::InvalidOption, //, 'str', '-b'
$capture.should_fail OptionParser::InvalidOption, //, 'str', '--unexpected'
$capture.should_fail DenCli::UsageError, //, 'str', 'unexpected'
$capture.should_ok %w[first], 'lstr', '--astr', 'first'
$capture.should_ok %w[first], 'lstr', '--astr=first'
$capture.should_fail OptionParser::MissingArgument, //, 'lstr', '--astr'
$capture.should_fail DenCli::UsageError, //, 'lstr'
$capture.should_fail OptionParser::InvalidOption, //, 'lstr', '-b'
$capture.should_fail OptionParser::InvalidOption, //, 'lstr', '--unexpected'
$capture.should_fail DenCli::UsageError, //, 'lstr', 'unexpected'
$capture.should_ok %w[first], 'bstr', '-a', 'first'
$capture.should_ok %w[first], 'bstr', '-afirst'
$capture.should_ok %w[first], 'bstr', '--astr', 'first'
$capture.should_ok %w[first], 'bstr', '--astr=first'
$capture.should_fail OptionParser::MissingArgument, //, 'bstr', '--astr'
$capture.should_fail OptionParser::MissingArgument, //, 'bstr', '-a'
$capture.should_fail DenCli::UsageError, //, 'bstr'
$capture.should_fail OptionParser::InvalidOption, //, 'bstr', '-b'
$capture.should_fail OptionParser::InvalidOption, //, 'bstr', '--unexpected'
$capture.should_fail DenCli::UsageError, //, 'bstr', 'unexpected'
$capture.should_ok [IPAddr.new('1.2.3.4')], 'ipaddr', '-a', '1.2.3.4'
$capture.should_fail OptionParser::InvalidArgument, /invalid address/, 'ipaddr', '-a', '1.2.3.400'
end.opt( :verbose, '-v', 'Prints additional information per test', default: false)
end
begin
cli.call *ARGV
rescue DenCli::UsageError
$stderr.puts $!
exit 1
end

View File

@ -1,4 +1,13 @@
require 'optparse'
class DenCli
def self.assert_type klass, method, argument_name, object, *types
unless types.any? {|type| object.is_a? type }
raise ArgumentError, "#{klass.name}.#{method} expects #{types.map( &:to_s).join '|' } for #{argument_name}, not: #{object.inspect}"
end
end
class UsageError < ::RuntimeError
end
class UnknownCommand < UsageError
@ -42,11 +51,16 @@ class DenCli
# `g(:abc)` => `["a", "ab", "abc"]`
# `g(:abcdef, 4)` => `["abcd", "abcde", "abcdef"]`
def gen_aliases cmd, min = nil
r = ((min||1)-1).upto cmd.length-1
case min
when false then min = cmd.length
when nil then min = 1
end
r = ([min, 1].max - 1).upto cmd.length-2
if block_given?
r.each {|i| yield cmd[0..i] }
yield cmd
else
r.map {|i| cmd[0..i] }
r.map {|i| cmd[0..i] } + [cmd]
end
end
alias g gen_aliases
@ -54,8 +68,8 @@ class DenCli
attr_reader :subs
def initialize progname, desc
@subs = Sub.new self, progname, desc
def initialize progname, description
@subs = Sub.new self, progname, description
end
def full_cmd
@ -66,24 +80,37 @@ class DenCli
post
end
def sub *a, &exe
@subs.sub *a, &exe
def []( k) @subs[k] end
def has?( k) @subs.has? k end
def sub *a, **o, &exe
@subs.sub *a, **o, &exe
end
def cmd *a, &exe
@subs.cmd *a, &exe
def cmd *a, **o, &exe
@subs.cmd *a, **o, &exe
end
def call *a
@subs.call *a
end
def help *args
@subs.help *args
def usage *args, **opts
@subs.usage *args, **opts
end
def [] k
@subs[k]
def help *args, **opts
@subs.help *args, **opts
end
def help_full *args, output: nil
output ||= $stdout
x = @subs.goto *args
_help_full output, x
end
def _help_full output, subs
Sub._help_commands output, subs.to_enum( :commands)
end
def interactive *args, **opts

View File

@ -1,30 +1,232 @@
require_relative '../dencli'
class DenCli::CMD
attr_reader :parent, :name, :desc, :exe, :completion
def initialize parent, name, desc, exe
class DenCli::CMD
attr_reader :parent, :name, :description, :exe, :completion, :options, :defined_in
def initialize parent, name, description, exe, defined_in
raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe
@parent, @name, @desc, @exe = parent, name, desc, exe
@parent, @name, @description, @exe, @defined_in = parent, name, description, lambda( &exe), defined_in
@parameters = @exe.parameters
@arguments_required = @exe.parameters.select {|e| :req == e[0] }.map {|e| e[1] }
@arguments_additional = @exe.parameters.select {|e| :opt == e[0] }.map {|e| e[1] }
@arguments = @exe.parameters.select {|e| :req == e[0] or :opt == e[0] }.map {|e| e[1] }
@options_required = @exe.parameters.select {|e| :keyreq == e[0] }.map {|e| e[1] }
@options_additional = @exe.parameters.select {|e| :key == e[0] }.map {|e| e[1] }
@options = {}
completion {|*a| [] }
end
def _full_cmd( post) parent._full_cmd [@name]+post end
def full_cmd() _full_cmd [] end
def call( *a) @exe.call *a end
def help() "#{parent.full_cmd.join ' '} #{name}\n#{ desc}" end
def complete( *pre, str) @completion.call *pre, str end
attr_reader :parameters,
:arguments_required, :arguments_additional, :arguments,
:options_required, :options_additional
alias required arguments_required
alias additional arguments_additional
def complete *pre, str
@completion.call *pre, str
end
def call *as
os = {}
unless @options.empty?
# options like --abc | -x will be provided in os
options = OptionParser.new
options.banner = "#{full_cmd.join ' '}"
# see also @options-array
@options.each {|_, opt| opt.on options, os }
as = options.parse! as
end
if @exe.lambda?
# The difference between a lambda and a Proc is, that Proc has anytime arity=-1.
# There will be no check if all arguments are given or some were missing or more than expected.
# lambda checks these arguments and has a arity.
# We will check it to provide useful errors.
pars = required
if as.length < pars.length
raise DenCli::UsageError, "Missing parameter(s): #{pars[as.length..-1].join " "}"
end
if parameters.select {|e| :rest == e[0] }.empty?
pars = pars + additional
if as.length > pars.length
raise DenCli::UsageError, "Unused parameter(s): #{as[-pars.length..-1].shelljoin}"
end
end
kr = @options.select {|_, o| o.required? and not os.has_key? o.name }
unless kr.empty?
raise DenCli::UsageError, "Missing argument(s): #{kr.map {|_, o| o.long || o.short }.join ', '}"
end
end
if os.empty?
@exe.call *as
else
@exe.call *as, **os
end
end
def usage output: nil
output ||= ''
_usage output
output
end
def _usage output
output << full_cmd.join( ' ')
@options.each do |_, o|
s = "#{o.short||o.long}#{(!o.short && o.val) ? ?= : ''}#{o.val}"
output << (o.required? ? " #{s}" : " [#{s}]")
end
if @exe.lambda?
parameters.each do |(type, name)|
case type
when :req
output << " <#{name}>"
when :opt
output << " [<#{name}>]"
when :rest
output << " [<#{name}> ...]"
end
end
else
output << ' [...]'
end
end
def commands *args
yield name, self
end
def goto *args
self
end
def help output: nil
output ||= ''
_help output
output
end
def _help output
output << "Usage: #{usage}\n#{description}\n"
_help_options output
end
def help_options output: nil
output ||= ''
_help_options output
output
end
def _help_options output
sc, lc, dc = 0, 0, 0
@options.each do |_, o|
s = o.short&.length || 0
l = o.long&.length || 0
v = o.val&.length || 0
d = o.desc&.to_s&.length || 0
d += 3 + o.default.to_s.length if o.default?
if 0 == l
x = s + (0==v ? 0 : 1+v)
sc = x if sc < x
else
sc = s if sc < s
x = l + (0==v ? 0 : 1+v)
lc = x if lc < x
end
dc = d if dc < d
end
format = " %-#{sc}s%s %-#{lc}s %s\n"
@options.map do |_, o|
s, l, v, y = o.short, o.long, o.val, ','
if l.nil?
s += "#{v}" if v
y = ' '
elsif s.nil?
l += "=#{v}" if v
y = ' '
end
d = o.desc || ''
d += " (#{o.default})" if o.default?
output << format % [ s, y, l, d ]
end
end
def completion &exe
@completion = exe
self
end
class Opt
attr_reader :name, :long, :short, :type, :val, :desc, :conv, :req
def required?() @req end
def default?() NilClass != @default end
def default() NilClass == @default ? nil : @default end
def parse_opt_string opt
case opt
when /\A(--\[no-\][^= ]+)\z/
@long, @val = $1, nil
when /\A(--[^= ]+)[= ](.+)\z/
@long, @val = $1, $2 || @val
when /\A(--[^= ]+)\z/
@long, @val = $1, nil
when /\A(-[^= -])[= ]?(.+)\z/
@short, @val = $1, $2 || @val
when /\A(-[^= -])\z/
@short, @val = $1, nil
else
raise ArgumentError, "Unexpected format for option: #{opt.inspect}"
end
end
private :parse_opt_string
def initialize cmd, name, opt, *args, desc, default: NilClass, &conv
@name, @desc, @default, @conv, @val, @type =
name.to_s.to_sym, desc, default, conv || lambda{|v|v}, nil, nil
parse_opt_string opt
@type = args.pop if OptionParser.top.atype.has_key? args.last
args.each &method( :parse_opt_string)
@req =
if NilClass != default
false
elsif cmd.exe.lambda?
! cmd.exe.parameters.select {|e| [:keyreq, @name] == e[0..1] }.empty?
else
nil
end
end
def on parser, store
store[@name] = @default if default?
short = "#{@short}#{@val ? ?= : ''}#{@val}"
long = "#{@long}#{@val ? ?= : ''}#{@val}"
parser.on short, long, *[@type].compact do |val|
store[@name] = @conv[val]
end
end
def inspect
"#<%s:0x%x %s %s %s %s (%p) %p conv=%s>" % [
self.class.name, object_id, @req ? "<#{@name}>" : "[#{@name}]",
@short, @long, @val, @default, @desc, @type ? " type=#{type}" : '',
@exe ? "<#{@exe.lambda? ? :lambda: :proc} ##{@exe.arity}>" : "nil"
]
end
end
def opt name, opt, *args, desc, default: NilClass, &conv
r = Opt.new( self, name, opt, *args, desc, default: default, &conv)
@options[r.name] = r
self
end
def inspect
"#<%s:0x%x %s @name=%p @desc=%p @parent=<%s:0x%x %s> @exe=<arity=%d>>" % [
"#<%s:0x%x %s @name=%p @description=%p @options=%p @parent=<%s:0x%x %s> @exe=<arity=%d>>" % [
self.class.name, self.object_id, self.full_cmd,
@name, @desc, @parent.class.name, @parent.class.object_id, @parent.full_cmd,
@name, @description, @options.values, @parent.class.name, @parent.class.object_id, @parent.full_cmd,
@exe.arity
]
end

View File

@ -17,12 +17,15 @@ class DenCli::Interactive
end
read_history if @histfile
Readline.vi_editing_mode rescue NotImplementedError
begin
Readline.vi_editing_mode
rescue NotImplementedError
end
Readline.completion_append_character = " "
Readline.completion_proc = method :complete
prepare_sub cl.subs
cl.cmd :exit, "exit", min: 2 do
cl.cmd :exit, "exit", min: cl.has?(:ex) ? cl.has?(:exi) ? 4 : 3 : 2 do
exit 0
end
cl.subs.aliases['?'] = cl.subs.subs['help']
@ -90,14 +93,16 @@ class DenCli::Interactive
c.subs.values.each do |n|
case n
when DenCli::Sub
n.cmd :exit, "<- #{n.parent.full_cmd.join ' '} - #{n.parent.desc[3..-1]}", min: 2 do
@cur = n.parent
end
n.cmd :exit,
"<- #{n.parent.full_cmd.join ' '} - #{n.parent.description[3..-1]}",
min: n.has?(:ex) ? n.has?( :exi) ? 4 : 3 : 2,
&lambda {|| @cur = n.parent }
n.aliases.delete nil
n.subs.delete nil
n.cmd '', "", min: 2, aliases: [nil] do
@cur = n
end
n.subs.delete ''
n.aliases['?'] = n.subs['help']
n.aliases['?'] = n[:help] if n.has? :help and not n.has? '?'
prepare_sub n
when DenCli::CMD
else raise "Unsupported sub-type: #{x}"
@ -120,8 +125,8 @@ class DenCli::Interactive
return nil if line.nil?
begin
cur.call *line
rescue ::UsageError
STDERR.puts "! #$!"
rescue ::DenCli::UsageError
$stderr.puts "! #$!"
end
true
end

View File

@ -1,58 +1,191 @@
require_relative '../dencli'
class DenCli::Sub
attr_reader :parent, :name, :desc, :subs, :aliases
attr_reader :parent, :name, :description, :subs, :aliases, :defined_in
def initialize parent, name, desc
@parent, @name, @desc, @subs, @aliases = parent, name, "-> #{desc}", {}, {}
def initialize parent, name, description, noshortaliases: nil, defined_in: nil
#DenCli::assert_type self, __method__, :name, name, Symbol
#DenCli::assert_type self, __method__, :parent, parent, DenCli, DenCli::Sub
#DenCli::assert_type self, __method__, :description, description, String
@parent, @name, @description, @subs, @aliases = parent, name, "-> #{description}", {}, {}
@noshortaliases, @defined_in = ! ! noshortaliases, defined_in || Kernel.caller
end
def _full_cmd( post) parent._full_cmd [@name]+post end
def full_cmd() _full_cmd [] end
def []( k) @aliases[k] end
def []( name) @aliases[name&.to_s] end
def has?( name) @aliases.has_key? name&.to_s end
def help n = nil, *a
if n.nil?
r = "#{full_cmd.join ' '}: #{desc}\n\n"
m = @subs.map {|k,_| k.length }.max
@subs.each do |k, c|
r += " % -#{m}s %s\n" % [k, c.desc] unless k.nil?
end
r
elsif @aliases.has_key? n
@aliases[n].help *a
def usage output: nil
output ||= ''
_usage output
output
end
def _usage output
output << full_cmd.join( ' ')
if @aliases.has_key? nil
output << " [<command> ...]"
else
raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
output << " <command> [...]"
end
end
def help n = nil, *a, output: nil
output ||= ''
_help output, n, *a
output
end
def _help output, n = nil, *a
if n.nil?
output << "#{full_cmd.join ' '}: #{description}\n\n"
self.class._help_commands output, @subs
elsif has? n
self[n]._help output, *a
else
raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
end
end
def commands &exe
yield name, self
@subs.each {|k, c| c.commands &exe }
end
def help_commands output: nil
output ||= ''
self.class._help_commands output, subs.map {|_,c| c}
output
end
class <<self
def _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/).each do |l|
c = 0
l.split( %r< >).each do |w|
if prefix
output << prefix
prefix = nil
end
wl = w.length
if 75 < c+wl
output << "\n#{' ' * n}#{w}"
c = n+2+wl
else
output << " #{w}"
c += 1 + wl
end
end
prefix = "\n#{' ' * n}"
end
output << "\n"
end
output
end
end
def goto *a
return self if a.empty?
n, *a = *a
if has? n
self[n].goto *a
else
raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
end
end
def call *a
n, *a = *a
if @aliases.has_key? n
@aliases[n].call *a
if has? n
self[n].call *a
else
raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
end
end
def _add name, min, obj, aliases
name = name.to_s unless name.nil?
#DenCli::assert_type self, __method__, :name, name, Symbol, String
#DenCli::assert_type self, __method__, :min, min, Integer, NilClass
#DenCli::assert_type self, __method__, :obj, obj, DenCli::Sub, DenCli::CMD
#DenCli::assert_type self, __method__, :aliases, aliases, Array, NilClass
name = name.to_s
@subs[name] = obj
DenCli.gen_aliases( name, min) {|a| @aliases[a] = obj }
if @noshortaliases
warn "Command/Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{name}. Used by #{@aliases[name].full_cmd} defined in #{@aliases[name].defined_in}" if @aliases.has_key? name
@aliases[name] = obj
else
DenCli.gen_aliases name, min do |a|
warn "Command/Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{a}. Used by #{@aliases[a].full_cmd} defined in #{@aliases[a].defined_in}" if @aliases.has_key? a
@aliases[a] ||= obj
end
end
if aliases
[*aliases].each {|a| @aliases[a] = obj }
[*aliases].each do |a|
a = a&.to_s
raise ArgumentError, "Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{a}. Used by #{@aliases[a].full_cmd} defined in #{@aliases[a].defined_in}" if @aliases.has_key? a
@aliases[a] = obj
end
end
obj
end
private :_add
def sub name, desc, min: nil, aliases: nil, &exe
r = _add name, min, DenCli::Sub.new( self, name, desc), aliases
# Define a new sub-menu:
#
# DenCli.new {|c|
# c.sub( 'sub-command') {|s|
# s.cmd( :hello, 'Greetings', &lambda {|| puts 'hello world' })
# }
# }
#
# # ./prog sub-command hello
#
# name should be a string/symbol. It will be converted to string.
# If provided, aliases must be a list of different aliases. It will be converted to string.
def sub name, description, min: nil, aliases: nil, noshortaliases: nil, defined_in: nil, &exe
r = _add name.to_s, min, DenCli::Sub.new( self, name, description, noshortaliases: noshortaliases, defined_in: defined_in || Kernel.caller.first), aliases
block_given? ? yield( r) : r
end
def cmd name, desc, min: nil, aliases: nil, &exe
_add name, min, DenCli::CMD.new( self, name, desc, exe), aliases
# Define a new command:
#
# DenCli.new {|c|
# c.cmd( :hello, 'Greetings', &lambda {|| puts 'hello world' })
# }
#
# # ./prog hello
# hello world
#
# name should be a string/symbol. It will be converted to string.
# If provided, aliases must be a list of different aliases. Except of nil, any alias will be converted to string.
# nil is an alias for a default command for sub-commands, but interactive shells.
#
# DenCli.new {|c|
# c.sub( :greetings, 'Hello, Welcome, ...') do |s|
# s.cmd( :hello, 'A simple Hello', aliases: %w[hello-world hello_world], &lambda {|| puts 'Hello World' })
# s.cmd( :welcome, 'More gracefull', aliases: [nil, 'welcome-world', :hello_world], &lambda {|| puts 'Welcome World' })
# }
# }
#
# # ./prog greetings
# Welcome World
# # ./prog greetings welcome
# Welcome World
# # ./prog greetings hello
# Hello World
def cmd name, description, min: nil, aliases: nil, defined_in: nil, &exe
_add name, min, DenCli::CMD.new( self, name, description, exe, defined_in || Kernel.caller.first), aliases
end
def complete *pre, str
@ -61,14 +194,14 @@ class DenCli::Sub
elsif sub = @subs[pre[0]]
sub.complete *pre[1..-1], str
else
STDOUT.print "\a"
$stdout.print "\a"
end
end
def inspect
"#<%s:0x%x %s @name=%p @desc=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [
"#<%s:0x%x %s @name=%p @description=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [
self.class.name, self.object_id, self.full_cmd,
@name, @desc, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd
@name, @description, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd
]
end
end

View File

@ -1,3 +1,3 @@
class DenCli
VERSION = '0.2.0'
VERSION = '0.5.5'
end