dencli/lib/dencli/cmd.rb

186 lines
5.4 KiB
Ruby

require_relative '../dencli'
class DenCli::CMD
attr_reader :parent, :name, :description, :exe, :completion, :options
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)
@options = {}
completion {|*a| [] }
end
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
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 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.as.first }.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 " "}]")
) : '...')
end
def options_help
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"
@options.map {|_, 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?
format % [ s, y, l, d ]
}.join "\n"
end
def completion &exe
@completion = exe
self
end
class Opt
attr_reader :name, :long, :short, :val, :desc, :os, :conv, :req
def required?() @req end
def default?() NilClass != @default end
def default() NilClass == @default ? nil : @default end
def initialize cmd, name, opt, *alts, desc, default: NilClass, **os, &conv
long = short = val = nil
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}"
end
end
@name, @short, @long, @val, @desc, @default, @os, @conv =
name.to_s.to_sym, short, long, val, desc, default, os, conv || lambda{|v|v}
@req =
if NilClass != default
false
elsif cmd.exe.lambda?
! cmd.exe.parameters.select {|e| [:keyreq, @name] == e[0..1] }.empty?
else
nil
end
end
def on parser, store
parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val|
store[@name] = @conv[val]
end
end
def inspect
"#<%s:0x%016x %s %s %s %s (%p) %p os=%p conv=%s>" % [
self.class.name, object_id, @req ? "<#{@name}>" : "[#{@name}]",
@short, @long, @val, @default, @desc, @os,
@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)
@options[r.name] = r
self
end
def inspect
"#<%s:0x%x %s @name=%p @description=%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,
@exe.arity
]
end
end