diff --git a/.gitignore b/.gitignore index 9106b2a..ff7246f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /pkg/ /spec/reports/ /tmp/ +*.sw[o-t] diff --git a/lib/dencli.rb b/lib/dencli.rb index 745bdbb..0c09257 100755 --- a/lib/dencli.rb +++ b/lib/dencli.rb @@ -4,6 +4,11 @@ class DenCli class UnknownCommand < UsageError end + require_relative 'dencli/cmd' + require_relative 'dencli/sub' + require_relative 'dencli/interactive' + require_relative 'dencli/version' + class < @exe=>" % [ - self.class.name, self.object_id, self.full_cmd, - @name, @desc, @parent.class.name, @parent.class.object_id, @parent.full_cmd, - @exe.arity - ] - end - end - - class Sub - attr_reader :parent, :name, :desc, :subs - def initialize parent, name, desc - @parent, @name, @desc, @subs, @aliases = parent, name, desc, {}, {} - end - - def _full_cmd post - parent._full_cmd [@name]+post - end - - def full_cmd - _full_cmd [] - end - - def [] k - @aliases[k] - 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 - else - raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.inspect}" - end - end - - def call *a - n, *a = *a - if @aliases.has_key? n - @aliases[n].call *a - else - raise UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.inspect}" - end - end - - def _add name, min, obj, aliases - name = name.to_s unless name.nil? - @subs[name] = obj - DenCli.gen_aliases( name, min) {|a| @aliases[a] = obj } - if aliases - [*aliases].each {|a| @aliases[a] = obj } - end - obj - end - private :_add - - def sub name, desc, min: nil, aliases: nil, &exe - r = _add name, min, Sub.new( self, name, desc), aliases - block_given? ? yield( r) : r - end - - def cmd name, desc, min: nil, aliases: nil, &exe - _add name, min, CMD.new( self, name, desc, exe), aliases - end - - def inspect - "#<%s:0x%x %s @name=%p @desc=%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 - ] - end - end + attr_reader :subs def initialize progname, desc @subs = Sub.new self, progname, desc @@ -180,4 +85,8 @@ class DenCli def [] k @subs[k] end + + def interactive *args, **opts + Interactive.new self, *args, **opts + end end diff --git a/lib/dencli/cmd.rb b/lib/dencli/cmd.rb new file mode 100644 index 0000000..0633bfb --- /dev/null +++ b/lib/dencli/cmd.rb @@ -0,0 +1,31 @@ +require_relative '../dencli' + +class DenCli::CMD + attr_reader :parent, :name, :desc, :exe, :completion + + def initialize parent, name, desc, exe + raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe + @parent, @name, @desc, @exe = parent, name, desc, exe + 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 + + def completion &exe + @completion = exe + self + end + + def inspect + "#<%s:0x%x %s @name=%p @desc=%p @parent=<%s:0x%x %s> @exe=>" % [ + self.class.name, self.object_id, self.full_cmd, + @name, @desc, @parent.class.name, @parent.class.object_id, @parent.full_cmd, + @exe.arity + ] + end +end diff --git a/lib/dencli/interactive.rb b/lib/dencli/interactive.rb new file mode 100644 index 0000000..69eb07e --- /dev/null +++ b/lib/dencli/interactive.rb @@ -0,0 +1,133 @@ +require_relative '../dencli' + +class DenCli::Interactive + attr_reader :cl, :prompt, :cur, :histfile + + def initialize cl, prompt, histfile: nil + require 'readline' + @cl, self.prompt = cl, prompt + @cur = cl.subs + cur.instance_variable_set :@name, '' + + @histfile = + case histfile + when nil then nil + when Pathname then histfile + else Pathname.new histfile.to_s + end + read_history if @histfile + + Readline.vi_editing_mode rescue NotImplementedError + Readline.completion_append_character = " " + Readline.completion_proc = method :complete + + prepare_sub cl.subs + cl.cmd :exit, "exit", min: 2 do + exit 0 + end + cl.subs.aliases['?'] = cl.subs.subs['help'] + cl.subs.subs.delete 'cli' + + stty_save = %x`stty -g`.chomp + trap "INT" do + system "stty", stty_save + Readline.refresh_line + end + end + + def history_file file = nil + file = + case file + when Pathname then file + when nil then @histfile + else Pathname.new file.to_s + end + end + + def read_history file = nil + file = history_file file + return unless file and file.exist? + file.each_line do |line| + Readline::HISTORY.push line.chomp + end + end + + def write_history file = nil + file = history_file file + return unless file + file.open 'w+' do |f| + Readline::HISTORY.each do |line| + f.puts line + end + end + end + + def prompt= s + @prompt = s.to_s + end + + def complete s + ws = words Readline.line_buffer + @cur.complete *ws[0...-1], s + end + + def sub *args, cur: nil + args.inject( cur || @cur) do |r, a| + return nil unless r.is_a? DenCli::Sub + r[a] + end + end + private :sub + + def words line = nil + r = line.split " " + r.push '' if ' ' == line[-1] + r + end + private :words + + def prepare_sub c + 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 '', "", min: 2, aliases: [nil] do + @cur = n + end + n.subs.delete '' + n.aliases['?'] = n.subs['help'] + prepare_sub n + when DenCli::CMD + else raise "Unsupported sub-type: #{x}" + end + end + end + + def read + line = Readline.readline( "#{prompt}#{cur.full_cmd.join ' '}> ", true) + return nil if line.nil? + Readline::HISTORY.pop if /^\s*$/ =~ line + if 0 < Readline::HISTORY.length-2 and Readline::HISTORY[Readline::HISTORY.length-2] == line + Readline::HISTORY.pop + end + line.split " " + end + + def step + line = read + return nil if line.nil? + begin + cur.call *line + rescue ::UsageError + STDERR.puts "! #$!" + end + true + end + + def run + while step + end + end +end diff --git a/lib/dencli/sub.rb b/lib/dencli/sub.rb new file mode 100644 index 0000000..e822e1d --- /dev/null +++ b/lib/dencli/sub.rb @@ -0,0 +1,74 @@ +require_relative '../dencli' + +class DenCli::Sub + attr_reader :parent, :name, :desc, :subs, :aliases + + def initialize parent, name, desc + @parent, @name, @desc, @subs, @aliases = parent, name, "-> #{desc}", {}, {} + end + + def _full_cmd( post) parent._full_cmd [@name]+post end + def full_cmd() _full_cmd [] end + def []( k) @aliases[k] 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 + else + raise 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 + else + raise 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? + @subs[name] = obj + DenCli.gen_aliases( name, min) {|a| @aliases[a] = obj } + if aliases + [*aliases].each {|a| @aliases[a] = obj } + 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 + 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 + 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 @desc=%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 + ] + end +end diff --git a/lib/dencli/version.rb b/lib/dencli/version.rb index 3ee143f..a95042a 100644 --- a/lib/dencli/version.rb +++ b/lib/dencli/version.rb @@ -1,3 +1,3 @@ class DenCli - VERSION = '0.1.0' + VERSION = '0.2.0' end