dencli/lib/dencli/sub.rb

208 lines
6.2 KiB
Ruby

require_relative '../dencli'
class DenCli::Sub
attr_reader :parent, :name, :description, :subs, :aliases, :defined_in
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 []( name) @aliases[name&.to_s] end
def has?( name) @aliases.has_key? name&.to_s end
def usage output: nil
output ||= ''
_usage output
output
end
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?
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 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, 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
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 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
# 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
# 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
if pre.empty?
@subs.keys.grep /\A#{Regexp.escape str}/
elsif sub = @subs[pre[0]]
sub.complete *pre[1..-1], str
else
$stdout.print "\a"
end
end
def inspect
"#<%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, @description, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd
]
end
end