Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
c946a093f7 | |||
09b467f7bd | |||
8fe78bb406 | |||
ba922fe52c | |||
a630da465a | |||
616989e96a | |||
a00149ce6a | |||
7078305256 | |||
ec025652c9 | |||
7be062c1db | |||
2c654f2941 | |||
6254349aa9 | |||
e83a7b4361 | |||
3cc205cb02 | |||
6e253b8b11 | |||
d2158e08ea | |||
ffd24c02f8 | |||
10061b1918 | |||
41940d3f10 | |||
d8ecfaeb86 | |||
df15912e7a | |||
eb61dcbc38 | |||
09c4c46d25 |
254
bin/example.rb
254
bin/example.rb
|
@ -1,25 +1,125 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require 'pathname'
|
||||
$:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s
|
||||
require 'dencli'
|
||||
require 'shellwords'
|
||||
require 'stringio'
|
||||
require 'ipaddr'
|
||||
require_relative '../lib/dencli'
|
||||
|
||||
cli = DenCli.new 'example', "This is an example for generate a DenCli-API"
|
||||
cli.cmd( :example, "I have an example command") { STDERR.puts "This is an example" }
|
||||
cli.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts cli.help(*args) }
|
||||
cli = DenCli.new :example, "This is an example for generate a DenCli-API"
|
||||
|
||||
cli.cmd( :args, "Expects and prints given arguments", &lambda {|a, b, c:, d:, e:|
|
||||
class Capture
|
||||
def initialize cli, verbose:
|
||||
@cli, @counter, @verbose = cli, 0, verbose
|
||||
end
|
||||
|
||||
def capture
|
||||
@args = NilClass
|
||||
@counter += 1
|
||||
stdout, stderr = $stdout, $stderr
|
||||
$stdout = $stderr = StringIO.new
|
||||
begin
|
||||
yield stdout, stderr
|
||||
ensure
|
||||
$stderr, $stdout = stderr, stdout
|
||||
end
|
||||
end
|
||||
|
||||
def args= args
|
||||
@args = args
|
||||
end
|
||||
|
||||
def logstart command
|
||||
STDERR.printf "[% 4d] \e[1;35m? \e[0m %s tests %s\r", @counter, $0.shellescape, command.shelljoin
|
||||
end
|
||||
|
||||
def logok info, command
|
||||
STDERR.printf "[% 4d] \e[1;32mok\e[0m %s | %s tests %s\e[J\n", @counter, info, $0.shellescape, command.shelljoin
|
||||
end
|
||||
|
||||
def logfail command
|
||||
STDERR.printf "[% 4d] \e[1;31mer\e[0m %s tests %s\e[J\n", @counter, $0.shellescape, command.shelljoin
|
||||
end
|
||||
|
||||
def logexception prefix, exception
|
||||
loginfo "#{prefix} (#{exception.class.name}) #{exception}"
|
||||
exception.backtrace[0...-Kernel.caller.length].each {|l| loginfo " #{l}" }
|
||||
end
|
||||
|
||||
def loginfo text
|
||||
STDERR.printf " %s\n", text
|
||||
end
|
||||
|
||||
def should_ok expect, *command
|
||||
logstart command
|
||||
$capture.capture { @cli.call 'tests', *command }
|
||||
if expect === @args
|
||||
logok @args, command
|
||||
else
|
||||
logfail command
|
||||
loginfo "expected args: #{expect.inspect}"
|
||||
loginfo "given args: #{@args.inspect}"
|
||||
STDERR.puts
|
||||
end
|
||||
rescue SystemExit
|
||||
if 0 == $!.status
|
||||
logok @args, command
|
||||
else
|
||||
logfail command
|
||||
end
|
||||
rescue Object
|
||||
logfail command
|
||||
logexception "unexpected raise:", $!
|
||||
STDERR.puts
|
||||
end
|
||||
|
||||
def should_fail exception, message, *command
|
||||
logstart command
|
||||
$capture.capture { @cli.call 'tests', *command }
|
||||
logfail command
|
||||
rescue exception
|
||||
if message === $!.message
|
||||
logok exception, command
|
||||
if @verbose
|
||||
logexception "raised:", $!
|
||||
STDERR.puts
|
||||
end
|
||||
else
|
||||
logexception "unexpected message:", $!
|
||||
STDERR.puts
|
||||
end
|
||||
rescue Object
|
||||
logfail command
|
||||
logexception "unexpected raised:", $!
|
||||
STDERR.puts
|
||||
end
|
||||
end
|
||||
|
||||
cli.cmd( :args, "Expects and prints given arguments",
|
||||
&lambda {|a, b, c:, d:, e:, f:, g:|
|
||||
p a: a, b: b, c: c, d: d, e: e
|
||||
}).
|
||||
opt( :c, '-c=ForC', "Option c").
|
||||
opt( :d, '-d=ForD', "Option d", default: "something").
|
||||
opt( :e, '-e', "Toggle e", default: false)
|
||||
opt( :d, '-dForD', "Option d", default: "something").
|
||||
opt( :e, '-e', "Toggle e", default: false).
|
||||
opt( :f, '--[no-]f', "Toggle f", default: false).
|
||||
opt( :g, '--long-option=sth', "Long option, no short option", default: "nothing").
|
||||
opt( :h, '-hsth', "No long option, only short option", default: "nothing")
|
||||
|
||||
cli.cmd( :example, "I have an example command") { $stderr.puts "This is an example" }
|
||||
cli.cmd( :help, "An example for help", aliases: [nil, '-h', '--help'], &lambda {|*commands, full:|
|
||||
if full
|
||||
cli.help_full *commands, output: $stderr
|
||||
else
|
||||
cli.help *commands, output: $stderr
|
||||
end
|
||||
}).
|
||||
opt( :full, '-f', '--[no-]full', "Print all commands and sub-commands.", default: false)
|
||||
|
||||
cli.sub( :more, "Sub-Commands are also possible with a new cli") do |sub|
|
||||
sub.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts sub.help(*args) }
|
||||
sub.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', *args) }
|
||||
sub.cmd( :example, "Here is an example, too") { STDERR.puts "This is an other example" }
|
||||
sub.cmd( :foo, "BAR") { STDERR.puts "FOO bar"}
|
||||
sub.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| $stderr.puts sub.help(*args) }
|
||||
sub.cmd( :example, "Here is an example, too") { $stderr.puts "This is an other example" }
|
||||
sub.cmd( :foo, "BAR") { $stderr.puts "FOO bar"}
|
||||
|
||||
sub.cmd( :args, "Expects and prints given arguments", &lambda {|a, b=1, c:, d: 5, e:|
|
||||
p a: a, b: b, c: c, d: d, e: e
|
||||
|
@ -29,19 +129,137 @@ cli.sub( :more, "Sub-Commands are also possible with a new cli") do |sub|
|
|||
opt( :e, '-e', "Toggle e")
|
||||
|
||||
sub.sub( :deeper, "You want to have Sub-Sub-Commands?") do |sub2|
|
||||
sub2.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*args| STDERR.puts sub2.help(*args) })
|
||||
sub2.cmd( :last, "The last example", &lambda { STDERR.puts "The last example" })
|
||||
sub2.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*commands| sub2.help( *commands, output: $stderr) })
|
||||
sub2.cmd( :last, "The last example", &lambda { $stderr.puts "The last example" })
|
||||
|
||||
sub2.sub( :'sub-commands', "Endless Sub-Sub- ...") do |sub3|
|
||||
sub3.cmd( :help, "") {|*args| STDERR.puts sub3.help( sub3, *args) }
|
||||
sub3.cmd( :hehe, "The real last example", min: 2) { STDERR.puts "Trust me!" }
|
||||
sub2.sub( :'sub-commands', "Endless Sub-Sub- ... with a special alias") do |sub3|
|
||||
# h -> help
|
||||
# he -> hehe
|
||||
# hel -> help
|
||||
# help -> help
|
||||
# heh -> hehe
|
||||
# hehe -> hehe
|
||||
sub3.cmd( :help, "", min: 3, aliases: [nil, :h]) {|*args| $stderr.puts sub3.help( *args) }
|
||||
sub3.cmd( :hehe, "The real last example", min: 2) { $stderr.puts "Trust me!" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cli.cmd( :cli, "Interactive shell", min: 3, &lambda {||
|
||||
cli.interactive( File.basename($0, '.rb')).run
|
||||
})
|
||||
|
||||
cli.sub :tests, "Some tests", noshortaliases: true do |tcli|
|
||||
tcli.cmd( :help, "", min: 4) {|*args| $stderr.puts tcli.help( *args) }
|
||||
OptionParser.accept IPAddr do |arg|
|
||||
begin
|
||||
IPAddr.new arg
|
||||
rescue IPAddr::InvalidAddressError
|
||||
raise OptionParser::InvalidArgument, "#{$!.message}: #{arg}"
|
||||
end
|
||||
end
|
||||
|
||||
tcli.cmd( :'-', "No arguments no options expected", &lambda {|| $capture.args = [] })
|
||||
tcli.cmd( :'arg', "", &lambda {|one| $capture.args = [one] })
|
||||
tcli.cmd( :'oar', "", &lambda {|one=nil| $capture.args = [one] })
|
||||
tcli.cmd( :'arg-arg', "", &lambda {|one, two| $capture.args = [one, two] })
|
||||
tcli.cmd( :'oar-oar', "", &lambda {|one=nil, two=nil| $capture.args = [one, two] })
|
||||
tcli.cmd( :'arg-oar', "expected", &lambda {|one, two=nil| $capture.args = [one, two] })
|
||||
tcli.cmd( :'oar-arg', "expected", &lambda {|one=nil, two| $capture.args = [one, two] })
|
||||
tcli.cmd( :'bool', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '')
|
||||
tcli.cmd( :'optbool', '', &lambda {|a: nil| $capture.args = [a] }).opt(:a, '-a', '')
|
||||
tcli.cmd( :'defbool', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '', default: 'default')
|
||||
tcli.cmd( :'str', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a=STR', '')
|
||||
tcli.cmd( :'lstr', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '--astr=STR', '')
|
||||
tcli.cmd( :'bstr', '', &lambda {|a:| $capture.args = [a] }).opt(:a, '-a', '--astr=STR', '')
|
||||
tcli.cmd( :'ipaddr', '', &lambda {|a:| $capture&.args = [a] }).opt(:a, '-a', '--addr=ADDR', IPAddr, '')
|
||||
|
||||
tcli.cmd( :run, "Run all tests") do |verbose:|
|
||||
$capture = Capture.new cli, verbose: verbose
|
||||
|
||||
$capture.should_fail DenCli::UnknownCommand, //, 'unknown-command'
|
||||
|
||||
$capture.should_ok [], '-'
|
||||
$capture.should_fail DenCli::UsageError, //, '-', 'unexpected'
|
||||
|
||||
$capture.should_ok %w[first], 'arg', 'first'
|
||||
$capture.should_fail DenCli::UsageError, //, 'arg'
|
||||
$capture.should_fail DenCli::UsageError, //, 'arg', 'first', 'unexpected'
|
||||
|
||||
$capture.should_ok %w[first], 'oar', 'first'
|
||||
$capture.should_ok [nil], 'oar'
|
||||
$capture.should_fail DenCli::UsageError, //, 'oar', 'first', 'unexpected'
|
||||
|
||||
$capture.should_ok %w[first two], 'oar-oar', 'first', 'two'
|
||||
$capture.should_ok ['first', nil], 'oar-oar', 'first'
|
||||
$capture.should_ok [nil,nil], 'oar-oar'
|
||||
$capture.should_fail DenCli::UsageError, //, 'oar-oar', 'first', 'two', 'unexpected'
|
||||
|
||||
$capture.should_ok %w[first two], 'arg-oar', 'first', 'two'
|
||||
$capture.should_ok ['first', nil], 'arg-oar', 'first'
|
||||
$capture.should_fail DenCli::UsageError, //, 'arg-oar'
|
||||
|
||||
$capture.should_ok [nil, 'first'], 'oar-arg', 'first'
|
||||
$capture.should_ok ['first', 'second'], 'oar-arg', 'first', 'second'
|
||||
$capture.should_fail DenCli::UsageError, //, 'oar-arg'
|
||||
$capture.should_fail DenCli::UsageError, //, 'oar-arg', 'first', 'two', 'unexpected'
|
||||
|
||||
$capture.should_ok [true], 'bool', '-a'
|
||||
$capture.should_fail DenCli::UsageError, //, 'bool'
|
||||
$capture.should_fail DenCli::UsageError, //, 'bool', '-a', 'unexpected'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'bool', '-b'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'bool', '--unexpected'
|
||||
$capture.should_fail DenCli::UsageError, //, 'bool', 'unexpected'
|
||||
|
||||
$capture.should_ok [true], 'optbool', '-a'
|
||||
$capture.should_ok [nil], 'optbool'
|
||||
$capture.should_fail DenCli::UsageError, //, 'optbool', '-a', 'unexpected'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'optbool', '-b'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'optbool', '--unexpected'
|
||||
$capture.should_fail DenCli::UsageError, //, 'optbool', 'unexpected'
|
||||
|
||||
$capture.should_ok [true], 'defbool', '-a'
|
||||
$capture.should_ok ['default'], 'defbool'
|
||||
$capture.should_fail DenCli::UsageError, //, 'defbool', '-a', 'unexpected'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'defbool', '-b'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'defbool', '--unexpected'
|
||||
$capture.should_fail DenCli::UsageError, //, 'defbool', 'unexpected'
|
||||
|
||||
$capture.should_ok %w[first], 'str', '-a', 'first'
|
||||
$capture.should_ok %w[first], 'str', '-afirst'
|
||||
$capture.should_fail OptionParser::MissingArgument, //, 'str', '-a'
|
||||
$capture.should_fail DenCli::UsageError, //, 'str'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'str', '-b'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'str', '--unexpected'
|
||||
$capture.should_fail DenCli::UsageError, //, 'str', 'unexpected'
|
||||
|
||||
$capture.should_ok %w[first], 'lstr', '--astr', 'first'
|
||||
$capture.should_ok %w[first], 'lstr', '--astr=first'
|
||||
$capture.should_fail OptionParser::MissingArgument, //, 'lstr', '--astr'
|
||||
$capture.should_fail DenCli::UsageError, //, 'lstr'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'lstr', '-b'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'lstr', '--unexpected'
|
||||
$capture.should_fail DenCli::UsageError, //, 'lstr', 'unexpected'
|
||||
|
||||
$capture.should_ok %w[first], 'bstr', '-a', 'first'
|
||||
$capture.should_ok %w[first], 'bstr', '-afirst'
|
||||
$capture.should_ok %w[first], 'bstr', '--astr', 'first'
|
||||
$capture.should_ok %w[first], 'bstr', '--astr=first'
|
||||
$capture.should_fail OptionParser::MissingArgument, //, 'bstr', '--astr'
|
||||
$capture.should_fail OptionParser::MissingArgument, //, 'bstr', '-a'
|
||||
$capture.should_fail DenCli::UsageError, //, 'bstr'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'bstr', '-b'
|
||||
$capture.should_fail OptionParser::InvalidOption, //, 'bstr', '--unexpected'
|
||||
$capture.should_fail DenCli::UsageError, //, 'bstr', 'unexpected'
|
||||
|
||||
$capture.should_ok [IPAddr.new('1.2.3.4')], 'ipaddr', '-a', '1.2.3.4'
|
||||
$capture.should_fail OptionParser::InvalidArgument, /invalid address/, 'ipaddr', '-a', '1.2.3.400'
|
||||
end.opt( :verbose, '-v', 'Prints additional information per test', default: false)
|
||||
end
|
||||
|
||||
begin
|
||||
cli.call *ARGV
|
||||
rescue DenCli::UsageError
|
||||
STDERR.puts $!
|
||||
$stderr.puts $!
|
||||
exit 1
|
||||
end
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
require 'optparse'
|
||||
|
||||
|
||||
class DenCli
|
||||
def self.assert_type klass, method, argument_name, object, *types
|
||||
unless types.any? {|type| object.is_a? type }
|
||||
raise ArgumentError, "#{klass.name}.#{method} expects #{types.map( &:to_s).join '|' } for #{argument_name}, not: #{object.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
class UsageError < ::RuntimeError
|
||||
end
|
||||
class UnknownCommand < UsageError
|
||||
|
@ -44,11 +51,16 @@ class DenCli
|
|||
# `g(:abc)` => `["a", "ab", "abc"]`
|
||||
# `g(:abcdef, 4)` => `["abcd", "abcde", "abcdef"]`
|
||||
def gen_aliases cmd, min = nil
|
||||
r = ((min||1)-1).upto cmd.length-1
|
||||
case min
|
||||
when false then min = cmd.length
|
||||
when nil then min = 1
|
||||
end
|
||||
r = ([min, 1].max - 1).upto cmd.length-2
|
||||
if block_given?
|
||||
r.each {|i| yield cmd[0..i] }
|
||||
yield cmd
|
||||
else
|
||||
r.map {|i| cmd[0..i] }
|
||||
r.map {|i| cmd[0..i] } + [cmd]
|
||||
end
|
||||
end
|
||||
alias g gen_aliases
|
||||
|
@ -68,6 +80,9 @@ class DenCli
|
|||
post
|
||||
end
|
||||
|
||||
def []( k) @subs[k] end
|
||||
def has?( k) @subs.has? k end
|
||||
|
||||
def sub *a, **o, &exe
|
||||
@subs.sub *a, **o, &exe
|
||||
end
|
||||
|
@ -80,12 +95,22 @@ class DenCli
|
|||
@subs.call *a
|
||||
end
|
||||
|
||||
def help *args
|
||||
@subs.help *args
|
||||
def usage *args, **opts
|
||||
@subs.usage *args, **opts
|
||||
end
|
||||
|
||||
def [] k
|
||||
@subs[k]
|
||||
def help *args, **opts
|
||||
@subs.help *args, **opts
|
||||
end
|
||||
|
||||
def help_full *args, output: nil
|
||||
output ||= $stdout
|
||||
x = @subs.goto *args
|
||||
_help_full output, x
|
||||
end
|
||||
|
||||
def _help_full output, subs
|
||||
Sub._help_commands output, subs.to_enum( :commands)
|
||||
end
|
||||
|
||||
def interactive *args, **opts
|
||||
|
|
|
@ -2,11 +2,17 @@ require_relative '../dencli'
|
|||
|
||||
|
||||
class DenCli::CMD
|
||||
attr_reader :parent, :name, :description, :exe, :completion, :options
|
||||
attr_reader :parent, :name, :description, :exe, :completion, :options, :defined_in
|
||||
|
||||
def initialize parent, name, description, exe
|
||||
def initialize parent, name, description, exe, defined_in
|
||||
raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe
|
||||
@parent, @name, @description, @exe = parent, name, description, lambda( &exe)
|
||||
@parent, @name, @description, @exe, @defined_in = parent, name, description, lambda( &exe), defined_in
|
||||
@parameters = @exe.parameters
|
||||
@arguments_required = @exe.parameters.select {|e| :req == e[0] }.map {|e| e[1] }
|
||||
@arguments_additional = @exe.parameters.select {|e| :opt == e[0] }.map {|e| e[1] }
|
||||
@arguments = @exe.parameters.select {|e| :req == e[0] or :opt == e[0] }.map {|e| e[1] }
|
||||
@options_required = @exe.parameters.select {|e| :keyreq == e[0] }.map {|e| e[1] }
|
||||
@options_additional = @exe.parameters.select {|e| :key == e[0] }.map {|e| e[1] }
|
||||
@options = {}
|
||||
completion {|*a| [] }
|
||||
end
|
||||
|
@ -14,19 +20,17 @@ class DenCli::CMD
|
|||
def _full_cmd( post) parent._full_cmd [@name]+post end
|
||||
def full_cmd() _full_cmd [] end
|
||||
|
||||
def parameters() @exe.parameters end
|
||||
def arguments_required() @exe.parameters.select {|e| :req == e[0] }.map {|e| e[1] } end
|
||||
attr_reader :parameters,
|
||||
:arguments_required, :arguments_additional, :arguments,
|
||||
:options_required, :options_additional
|
||||
alias required arguments_required
|
||||
def arguments_additional() @exe.parameters.select {|e| :opt == e[0] }.map {|e| e[1] } end
|
||||
alias additional arguments_additional
|
||||
def arguments() @exe.parameters.select {|e| :req == e[0] or :opt == e[0] }.map {|e| e[1] } end
|
||||
def options_required() @exe.parameters.select {|e| :keyreq == e[0] }.map {|e| e[1] } end
|
||||
def options_additional() @exe.parameters.select {|e| :key == e[0] }.map {|e| e[1] } end
|
||||
|
||||
def help() "Usage: #{usage}\n#{description}\n#{options_help}" end
|
||||
def complete( *pre, str) @completion.call *pre, str end
|
||||
def complete *pre, str
|
||||
@completion.call *pre, str
|
||||
end
|
||||
|
||||
def call( *as)
|
||||
def call *as
|
||||
os = {}
|
||||
unless @options.empty?
|
||||
# options like --abc | -x will be provided in os
|
||||
|
@ -53,25 +57,70 @@ class DenCli::CMD
|
|||
end
|
||||
kr = @options.select {|_, o| o.required? and not os.has_key? o.name }
|
||||
unless kr.empty?
|
||||
raise DenCli::UsageError, "Missing argument(s): #{kr.map {|o| o.as.first }.join ', '}"
|
||||
raise DenCli::UsageError, "Missing argument(s): #{kr.map {|_, o| o.long || o.short }.join ', '}"
|
||||
end
|
||||
end
|
||||
if os.empty?
|
||||
@exe.call *as
|
||||
else
|
||||
@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 " "}]")
|
||||
) : '...')
|
||||
end
|
||||
|
||||
def options_help
|
||||
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
|
||||
|
@ -89,11 +138,11 @@ class DenCli::CMD
|
|||
end
|
||||
dc = d if dc < d
|
||||
end
|
||||
format = " %-#{sc}s%s %-#{lc}s %s"
|
||||
@options.map {|_, o|
|
||||
format = " %-#{sc}s%s %-#{lc}s %s\n"
|
||||
@options.map do |_, o|
|
||||
s, l, v, y = o.short, o.long, o.val, ','
|
||||
if l.nil?
|
||||
s += "=#{v}" if v
|
||||
s += "#{v}" if v
|
||||
y = ' '
|
||||
elsif s.nil?
|
||||
l += "=#{v}" if v
|
||||
|
@ -101,8 +150,8 @@ class DenCli::CMD
|
|||
end
|
||||
d = o.desc || ''
|
||||
d += " (#{o.default})" if o.default?
|
||||
format % [ s, y, l, d ]
|
||||
}.join "\n"
|
||||
output << format % [ s, y, l, d ]
|
||||
end
|
||||
end
|
||||
|
||||
def completion &exe
|
||||
|
@ -111,39 +160,35 @@ class DenCli::CMD
|
|||
end
|
||||
|
||||
class Opt
|
||||
attr_reader :name, :long, :short, :val, :desc, :os, :conv, :req
|
||||
attr_reader :name, :long, :short, :type, :val, :desc, :conv, :req
|
||||
def required?() @req end
|
||||
def default?() NilClass != @default end
|
||||
def default() NilClass == @default ? nil : @default end
|
||||
|
||||
def initialize cmd, name, opt, *alts, desc, default: NilClass, **os, &conv
|
||||
long = short = val = nil
|
||||
def parse_opt_string opt
|
||||
case opt
|
||||
when /\A(--[^=-]+)=(.+)\z/
|
||||
long, val = $1, $2
|
||||
when /\A(--[^=-]+)\z/
|
||||
long, val = $1, nil
|
||||
when /\A(-[^=-]+)=(.+)\z/
|
||||
short, val = $1, $2
|
||||
when /\A(-[^=-]+)\z/
|
||||
short, val = $1, nil
|
||||
else raise ArgumentError, "Unexpected format for option: #{opt.inspect}"
|
||||
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}"
|
||||
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
|
||||
@name, @short, @long, @val, @desc, @default, @os, @conv =
|
||||
name.to_s.to_sym, short, long, val, desc, default, os, conv || lambda{|v|v}
|
||||
private :parse_opt_string
|
||||
|
||||
def initialize cmd, name, opt, *args, desc, default: NilClass, &conv
|
||||
@name, @desc, @default, @conv, @val, @type =
|
||||
name.to_s.to_sym, desc, default, conv || lambda{|v|v}, nil, nil
|
||||
parse_opt_string opt
|
||||
@type = args.pop if OptionParser.top.atype.has_key? args.last
|
||||
args.each &method( :parse_opt_string)
|
||||
@req =
|
||||
if NilClass != default
|
||||
false
|
||||
|
@ -155,30 +200,33 @@ class DenCli::CMD
|
|||
end
|
||||
|
||||
def on parser, store
|
||||
parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val|
|
||||
store[@name] = @default if default?
|
||||
short = "#{@short}#{@val ? ?= : ''}#{@val}"
|
||||
long = "#{@long}#{@val ? ?= : ''}#{@val}"
|
||||
parser.on short, long, *[@type].compact do |val|
|
||||
store[@name] = @conv[val]
|
||||
end
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<%s:0x%016x %s %s %s %s (%p) %p os=%p conv=%s>" % [
|
||||
"#<%s:0x%x %s %s %s %s (%p) %p conv=%s>" % [
|
||||
self.class.name, object_id, @req ? "<#{@name}>" : "[#{@name}]",
|
||||
@short, @long, @val, @default, @desc, @os,
|
||||
@short, @long, @val, @default, @desc, @type ? " type=#{type}" : '',
|
||||
@exe ? "<#{@exe.lambda? ? :lambda: :proc} ##{@exe.arity}>" : "nil"
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def opt name, opt, *alts, desc, **os, &conv
|
||||
r = Opt.new( self, name, opt, *alts, desc, **os, &conv)
|
||||
def opt name, opt, *args, desc, default: NilClass, &conv
|
||||
r = Opt.new( self, name, opt, *args, desc, default: default, &conv)
|
||||
@options[r.name] = r
|
||||
self
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<%s:0x%x %s @name=%p @description=%p @parent=<%s:0x%x %s> @exe=<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, @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
|
||||
|
|
|
@ -17,12 +17,15 @@ class DenCli::Interactive
|
|||
end
|
||||
read_history if @histfile
|
||||
|
||||
Readline.vi_editing_mode rescue NotImplementedError
|
||||
begin
|
||||
Readline.vi_editing_mode
|
||||
rescue NotImplementedError
|
||||
end
|
||||
Readline.completion_append_character = " "
|
||||
Readline.completion_proc = method :complete
|
||||
|
||||
prepare_sub cl.subs
|
||||
cl.cmd :exit, "exit", min: 2 do
|
||||
cl.cmd :exit, "exit", min: cl.has?(:ex) ? cl.has?(:exi) ? 4 : 3 : 2 do
|
||||
exit 0
|
||||
end
|
||||
cl.subs.aliases['?'] = cl.subs.subs['help']
|
||||
|
@ -90,14 +93,16 @@ class DenCli::Interactive
|
|||
c.subs.values.each do |n|
|
||||
case n
|
||||
when DenCli::Sub
|
||||
n.cmd :exit, "<- #{n.parent.full_cmd.join ' '} - #{n.parent.description[3..-1]}", min: 2 do
|
||||
@cur = n.parent
|
||||
end
|
||||
n.cmd :exit,
|
||||
"<- #{n.parent.full_cmd.join ' '} - #{n.parent.description[3..-1]}",
|
||||
min: n.has?(:ex) ? n.has?( :exi) ? 4 : 3 : 2,
|
||||
&lambda {|| @cur = n.parent }
|
||||
n.aliases.delete nil
|
||||
n.subs.delete nil
|
||||
n.cmd '', "", min: 2, aliases: [nil] do
|
||||
@cur = n
|
||||
end
|
||||
n.subs.delete ''
|
||||
n.aliases['?'] = n.subs['help']
|
||||
n.aliases['?'] = n[:help] if n.has? :help and not n.has? '?'
|
||||
prepare_sub n
|
||||
when DenCli::CMD
|
||||
else raise "Unsupported sub-type: #{x}"
|
||||
|
@ -121,7 +126,7 @@ class DenCli::Interactive
|
|||
begin
|
||||
cur.call *line
|
||||
rescue ::DenCli::UsageError
|
||||
STDERR.puts "! #$!"
|
||||
$stderr.puts "! #$!"
|
||||
end
|
||||
true
|
||||
end
|
||||
|
|
|
@ -1,30 +1,105 @@
|
|||
require_relative '../dencli'
|
||||
|
||||
class DenCli::Sub
|
||||
attr_reader :parent, :name, :description, :subs, :aliases
|
||||
attr_reader :parent, :name, :description, :subs, :aliases, :defined_in
|
||||
|
||||
def initialize parent, name, description
|
||||
def initialize parent, name, description, noshortaliases: nil, defined_in: nil
|
||||
#DenCli::assert_type self, __method__, :name, name, Symbol
|
||||
#DenCli::assert_type self, __method__, :parent, parent, DenCli, DenCli::Sub
|
||||
#DenCli::assert_type self, __method__, :description, description, String
|
||||
@parent, @name, @description, @subs, @aliases = parent, name, "-> #{description}", {}, {}
|
||||
@noshortaliases, @defined_in = ! ! noshortaliases, defined_in || Kernel.caller
|
||||
end
|
||||
|
||||
def _full_cmd( post) parent._full_cmd [@name]+post end
|
||||
def full_cmd() _full_cmd [] end
|
||||
def []( k) @aliases[k] end
|
||||
def []( name) @aliases[name&.to_s] end
|
||||
def has?( name) @aliases.has_key? name&.to_s end
|
||||
|
||||
def usage
|
||||
"#{full_cmd.join ' '} ..."
|
||||
|
||||
def usage output: nil
|
||||
output ||= ''
|
||||
_usage output
|
||||
output
|
||||
end
|
||||
|
||||
def help n = nil, *a
|
||||
def _usage output
|
||||
output << full_cmd.join( ' ')
|
||||
if @aliases.has_key? nil
|
||||
output << " [<command> ...]"
|
||||
else
|
||||
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?
|
||||
r = "#{full_cmd.join ' '}: #{description}\n\n"
|
||||
m = @subs.map {|k,c| k.nil? ? 0 : c.usage.length }.max
|
||||
@subs.each do |k, c|
|
||||
r += " % -#{m}s %s\n" % [c.usage, c.description] unless k.nil?
|
||||
output << "#{full_cmd.join ' '}: #{description}\n\n"
|
||||
self.class._help_commands output, @subs
|
||||
elsif has? n
|
||||
self[n]._help output, *a
|
||||
else
|
||||
raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
|
||||
end
|
||||
r
|
||||
elsif @aliases.has_key? n
|
||||
@aliases[n].help *a
|
||||
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
|
||||
|
@ -32,31 +107,85 @@ class DenCli::Sub
|
|||
|
||||
def call *a
|
||||
n, *a = *a
|
||||
if @aliases.has_key? n
|
||||
@aliases[n].call *a
|
||||
if has? n
|
||||
self[n].call *a
|
||||
else
|
||||
raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
|
||||
end
|
||||
end
|
||||
|
||||
def _add name, min, obj, aliases
|
||||
name = name.to_s unless name.nil?
|
||||
#DenCli::assert_type self, __method__, :name, name, Symbol, String
|
||||
#DenCli::assert_type self, __method__, :min, min, Integer, NilClass
|
||||
#DenCli::assert_type self, __method__, :obj, obj, DenCli::Sub, DenCli::CMD
|
||||
#DenCli::assert_type self, __method__, :aliases, aliases, Array, NilClass
|
||||
name = name.to_s
|
||||
@subs[name] = obj
|
||||
DenCli.gen_aliases( name, min) {|a| @aliases[a] = obj }
|
||||
if @noshortaliases
|
||||
warn "Command/Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{name}. Used by #{@aliases[name].full_cmd} defined in #{@aliases[name].defined_in}" if @aliases.has_key? name
|
||||
@aliases[name] = obj
|
||||
else
|
||||
DenCli.gen_aliases name, min do |a|
|
||||
warn "Command/Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{a}. Used by #{@aliases[a].full_cmd} defined in #{@aliases[a].defined_in}" if @aliases.has_key? a
|
||||
@aliases[a] ||= obj
|
||||
end
|
||||
end
|
||||
if aliases
|
||||
[*aliases].each {|a| @aliases[a] = obj }
|
||||
[*aliases].each do |a|
|
||||
a = a&.to_s
|
||||
raise ArgumentError, "Alias for #{obj.full_cmd} defined in #{obj.defined_in} already exists: #{full_cmd.join ' '} #{a}. Used by #{@aliases[a].full_cmd} defined in #{@aliases[a].defined_in}" if @aliases.has_key? a
|
||||
@aliases[a] = obj
|
||||
end
|
||||
end
|
||||
obj
|
||||
end
|
||||
private :_add
|
||||
|
||||
def sub name, description, min: nil, aliases: nil, &exe
|
||||
r = _add name, min, DenCli::Sub.new( self, name, description), aliases
|
||||
# Define a new sub-menu:
|
||||
#
|
||||
# DenCli.new {|c|
|
||||
# c.sub( 'sub-command') {|s|
|
||||
# s.cmd( :hello, 'Greetings', &lambda {|| puts 'hello world' })
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# # ./prog sub-command hello
|
||||
#
|
||||
# name should be a string/symbol. It will be converted to string.
|
||||
# If provided, aliases must be a list of different aliases. It will be converted to string.
|
||||
def sub name, description, min: nil, aliases: nil, noshortaliases: nil, defined_in: nil, &exe
|
||||
r = _add name.to_s, min, DenCli::Sub.new( self, name, description, noshortaliases: noshortaliases, defined_in: defined_in || Kernel.caller.first), aliases
|
||||
block_given? ? yield( r) : r
|
||||
end
|
||||
|
||||
def cmd name, description, min: nil, aliases: nil, &exe
|
||||
_add name, min, DenCli::CMD.new( self, name, description, exe), aliases
|
||||
# Define a new command:
|
||||
#
|
||||
# DenCli.new {|c|
|
||||
# c.cmd( :hello, 'Greetings', &lambda {|| puts 'hello world' })
|
||||
# }
|
||||
#
|
||||
# # ./prog hello
|
||||
# hello world
|
||||
#
|
||||
# name should be a string/symbol. It will be converted to string.
|
||||
# If provided, aliases must be a list of different aliases. Except of nil, any alias will be converted to string.
|
||||
# nil is an alias for a default command for sub-commands, but interactive shells.
|
||||
#
|
||||
# DenCli.new {|c|
|
||||
# c.sub( :greetings, 'Hello, Welcome, ...') do |s|
|
||||
# s.cmd( :hello, 'A simple Hello', aliases: %w[hello-world hello_world], &lambda {|| puts 'Hello World' })
|
||||
# s.cmd( :welcome, 'More gracefull', aliases: [nil, 'welcome-world', :hello_world], &lambda {|| puts 'Welcome World' })
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# # ./prog greetings
|
||||
# Welcome World
|
||||
# # ./prog greetings welcome
|
||||
# Welcome World
|
||||
# # ./prog greetings hello
|
||||
# Hello World
|
||||
def cmd name, description, min: nil, aliases: nil, defined_in: nil, &exe
|
||||
_add name, min, DenCli::CMD.new( self, name, description, exe, defined_in || Kernel.caller.first), aliases
|
||||
end
|
||||
|
||||
def complete *pre, str
|
||||
|
@ -65,7 +194,7 @@ class DenCli::Sub
|
|||
elsif sub = @subs[pre[0]]
|
||||
sub.complete *pre[1..-1], str
|
||||
else
|
||||
STDOUT.print "\a"
|
||||
$stdout.print "\a"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
class DenCli
|
||||
VERSION = '0.4.0'
|
||||
VERSION = '0.5.5'
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue