require 'open3' require 'io/nonblock' # TODO - factor out err/out methods from send_command module Session VERSION = '2.1.2' class PipeError < StandardError; end class ExecutionError < StandardError; end class History #{{{ def initialize; @a = []; end def method_missing(m, *a, &b); @a.send(m, *a, &b); end def to_s @a.inject(''){|s,cmd| s << format("%d: %s\n", cmd.cmdno, cmd)} end alias to_str to_s def inspect @a.inject(''){|s,cmd| s << cmd.inspect} end #}}} end class Command #{{{ class << self @@cmdno = 0 def cmdno; @@cmdno; end def cmdno= n; @@cmdno = n; end end attr :cmd attr :cmdno attr :out,true attr :err,true attr :cid attr :begin attr :end attr :begin_pat attr :end_pat def initialize(command) @cmd = command.to_s @cmdno = self.class.cmdno self.class.cmdno += 1 @err = '' @out = '' @cid = "%d.%d.%d" % [$$, cmdno, Time.now.usec] @begin = "__COMMAND_%s_BEGIN__" % cid @end = "__COMMAND_%s_END__" % cid @begin_pat = %r/#{ Regexp.escape(@begin) }/ @end_pat = %r/#{ Regexp.escape(@end) }/ #@begin_pat = %r/^#{ Regexp.escape(@begin) }$/ #@end_pat = %r/^#{ Regexp.escape(@end) }$/ end def to_s; cmd end def to_str; cmd end def inspect format("cmd: <%s>\n\tcmdno: <%s>\n\tout: <%s>\n\terr: <%s>\n\tcid: <%s>\n", cmd, cmdno, out.inspect, err.inspect, cid) end #}}} end class AbstractSession #{{{ # class methods #{{{ class << self # def default_prog # return @@default_prog if defined? @@default_prog and @@default_prog # if defined? self::DEFAULT_PROG # return @@default_prog = self::DEFAULT_PROG # else # @@default_prog = ENV["SESSION_#{ self }_PROG"] # end # nil # end # def default_prog= prog # @@default_prog = prog # end def default_prog return @default_prog if defined? @default_prog and @default_prog if defined? self::DEFAULT_PROG return @default_prog = self::DEFAULT_PROG else @default_prog = ENV["SESSION_#{ self }_PROG"] end nil end def default_prog= prog @default_prog = prog end end #}}} # instance methods attr :opts attr :prog attr :stdin alias i stdin attr :stdout alias o stdout attr :stderr alias e stderr attr :history attr :track_history, true attr :outproc, true attr :errproc, true def initialize(*args) #{{{ @opts = hashify(*args) @prog = opts[:prog] || self.class.default_prog or raise(ArgumentError, "no program specified") @history = History.new @track_history = false @track_history = opts[:history] if opts.has_key? :history @track_history = opts[:track_history] if opts.has_key? :track_history @outproc = nil @errproc = nil @stdin, @stdout, @stderr = Open3.popen3(prog) clear if block_given? ret = nil begin ret = yield self ensure self.close! end return ret end return self #}}} end # abstract methods def clear raise NotImplementedError end alias flush clear def path raise NotImplementedError end def path= raise NotImplementedError end def send_command cmd raise NotImplementedError end # concrete methods def ready? #{{{ (stdin and stdout and stderr) and (IO === stdin and IO === stdout and IO === stderr) and (not (stdin.closed? or stdout.closed? or stderr.closed?))# and #(not (stdout.eof? or stderr.eof?)) #}}} end def close! #{{{ [stdin, stdout, stderr].each{|pipe| pipe.close} stdin, stdout, stderr = nil, nil, nil true #}}} end alias close close! def hashify(*a) #{{{ a.inject({}){|o,h| o.update(h)} #}}} end private :hashify def execute(command, redirects = {}) #{{{ raise(PipeError, command) unless ready? # clear buffers clear # setup redirects rerr = redirects[:e] || redirects[:err] || redirects[2] || redirects[:stderr] || redirects['stderr'] rout = redirects[:o] || redirects[:out] || redirects[1] || redirects[:stdout] || redirects['stdout'] # create cmd object and add to history cmd = Command.new(command.to_s) history << cmd if track_history # send command send_command cmd # process stdout/stderr err = { :io => stderr, :name => 'stderr', :buf => '', :begin => nil, :idx => nil, :end => nil, :done => false, :redirect => rerr, :proc => errproc, :yield => lambda{|b| yield(nil, b)}, } out = { :io => stdout, :name => 'stdout', :buf => '', :begin => nil, :idx => nil, :end => nil, :done => false, :redirect => rout, :proc => outproc, :yield => lambda{|b| yield(b, nil)}, } until err[:done] and out[:done] raise(PipeError, command) unless ready? # wait for data rfds, wfds, efds = select [stdout, stderr], nil, [stdout, stderr] unless efds.empty? msg = efds.map{|io| "err on <#{ io.inspect }>"}.join(',') raise PipeError, msg end rfds.each do |io| next if io.eof? iodat = case io when stderr err when stdout out else raise ExecutionError, "unknown IO #{ io.inspect }" end #raise ExecutionError, iodat.inspect if iodat[:done] iodat[:buf] << iodat[:io].nonblock{ iodat[:io].read } #p iodat[:buf] if iodat[:done] #if iodat[:buf] =~ %r/\s*/o #ignore chaff #next #else raise ExecutionError, iodat.inspect #end end if $DEBUG #{{{ puts puts '====' puts iodat[:name] p iodat[:buf] #}}} end unless iodat[:begin] m = cmd.begin_pat.match iodat[:buf] if m iodat[:begin] = m.end(0) iodat[:begin] += 1 # skip past newline iodat[:idx] = iodat[:begin] iodat[:end] = -1 if $DEBUG #{{{ puts 'FOUND BEGIN' #}}} end end end unless iodat[:done] m = cmd.end_pat.match iodat[:buf] if m iodat[:end] = m.begin(0) - 1 iodat[:end] -= 1 # rewind before newline iodat[:done] = true if $DEBUG #{{{ puts 'FOUND END' puts "m.begin <#{ m.begin 0 }>" puts "m.end <#{ m.end 0}>" #}}} end end end if $DEBUG #{{{ p iodat[:begin] p iodat[:idx] p iodat[:end] p iodat[:buf].size if iodat[:begin] and iodat[:idx] and iodat[:end] #s = iodat[:buf].dup.gsub %r/./, ' ' puts '----' p iodat[:buf] s = iodat[:buf].dup << ' ' s[iodat[:begin]] = '^' p s s = iodat[:buf].dup << ' ' s[iodat[:idx]] = '_' p s s = iodat[:buf].dup << ' ' s[iodat[:end]] = '.' p s puts '----' end #}}} end if iodat[:begin] and iodat[:idx] < iodat[:buf].size and iodat[:end] < iodat[:buf].size buf = iodat[:buf][iodat[:idx] .. iodat[:end]] if $DEBUG #{{{ puts "buf <#{ buf.inspect }>" #}}} end if buf and not buf.empty? iodat[:redirect] << buf if iodat[:redirect] iodat[:proc].call buf if iodat[:proc] iodat[:yield].call buf if block_given? end end iodat[:idx] = iodat[:buf].size if iodat[:begin] end # each end # loop raise ExecutionError, "stderr" unless err[:done] cmd.err = err[:buf][err[:begin] .. err[:end]] raise ExecutionError, "stdout" unless out[:done] cmd.out = out[:buf][out[:begin] .. out[:end]] err = out = nil # clear buffers clear # get the exit status get_status if self.respond_to? :get_status # clear buffers clear return [cmd.out, cmd.err] #}}} end #}}} end class IDL < AbstractSession #{{{ DEFAULT_PROG = 'idl' def clear #{{{ stdin.puts "retall" stdin.puts "printf, -2, '__clear__'" stdin.puts "printf, -1, '__clear__'" stdin.flush while((line = stderr.gets) and line !~ %r/__clear__/o); end while((line = stdout.gets) and line !~ %r/__clear__/o); end self #}}} end def send_command cmd #{{{ stdin.printf "retall\n" stdin.printf "printf, -2, '%s'\n", cmd.begin stdin.printf "printf, -1, '%s'\n", cmd.begin stdin.printf "retall\n" stdin.printf "%s\n", cmd.cmd stdin.printf "retall\n" stdin.printf "printf, -2, '%s'\n", cmd.end stdin.printf "printf, -1, '%s'\n", cmd.end stdin.flush #}}} end def path #{{{ stdout, stderr = execute "print, !path" stdout.strip.split %r/:/o #}}} end def path= arg #{{{ case arg when Array arg = arg.join ':' else arg = arg.to_s.strip end stdout, stderr = execute "!path='#{ arg }'" self.path #}}} end #}}} end class Sh < AbstractSession #{{{ DEFAULT_PROG = 'sh' ECHO = 'echo' attr :status alias exit_status status alias exitstatus status def clear #{{{ #stdin.printf "#{ ECHO } __clear__ 1>&2\n" #stdin.printf "#{ ECHO } __clear__\n" #stdin.flush #while((line = stderr.gets) and line !~ %r/__clear__/o); end #while((line = stdout.gets) and line !~ %r/__clear__/o); end stdout.nonblock{ stdout.read } stderr.nonblock{ stderr.read } self #}}} end def send_command cmd #{{{ stdin.printf "%s 1>&2; %s '%s' 1>&2\n", ECHO, ECHO, cmd.begin stdin.printf "%s ; %s '%s' \n", ECHO, ECHO, cmd.begin stdin.printf "%s\n", cmd.cmd stdin.printf "export __exit_status__=$?\n" stdin.printf "%s 1>&2; %s '%s' 1>&2\n", ECHO, ECHO, cmd.end stdin.printf "%s ; %s '%s' \n", ECHO, ECHO, cmd.end stdin.flush #}}} end def get_status #{{{ stdin.puts "#{ ECHO } $__exit_status__" stdin.flush @status = stdout.gets unless @status =~ /^\s*\d+\s*$/ raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>" end @status = Integer @status #}}} end def path #{{{ stdout, stderr = execute "#{ ECHO } $PATH" stdout.strip.split %r/:/o #}}} end def path= arg #{{{ case arg when Array arg = arg.join ':' else arg = arg.to_s.strip end stdout, stderr = execute "export PATH='#{ arg }'" self.path #}}} end #}}} end class Bash < Sh #{{{ DEFAULT_PROG = 'bash' #}}} end class Csh < AbstractSession #{{{ DEFAULT_PROG = 'csh' ECHO = 'echo' attr :status alias exit_status status alias exitstatus status def clear #{{{ stdin.printf "#{ ECHO } __clear__ > /dev/stderr\n" stdin.printf "#{ ECHO } __clear__\n" stdin.flush while((line = stderr.gets) and line !~ %r/__clear__/o); end while((line = stdout.gets) and line !~ %r/__clear__/o); end self #}}} end def send_command cmd #{{{ stdin.printf "%s > /dev/stderr; %s '%s' > /dev/stderr\n", ECHO, ECHO, cmd.begin stdin.printf "%s ; %s '%s' \n", ECHO, ECHO, cmd.begin stdin.printf "%s\n", cmd.cmd stdin.printf "set __exit_status__=$?\n" stdin.printf "%s > /dev/stderr; %s '%s' > /dev/stderr\n", ECHO, ECHO, cmd.end stdin.printf "%s ; %s '%s' \n", ECHO, ECHO, cmd.end stdin.flush #}}} end def get_status #{{{ stdin.puts "#{ ECHO } $__exit_status__" stdin.flush @status = stdout.gets unless @status =~ /^\s*\d+\s*$/ raise ExecutionError, "could not determine exit status from <#{ @status.inspect }>" end @status = Integer @status #}}} end def path #{{{ stdout, stderr = execute "#{ ECHO } $path" stdout.strip.split %r/\s+/o #}}} end def path= arg #{{{ case arg when Array arg = arg.join ' ' else arg = arg.to_s.strip end stdout, stderr = execute "set path=(#{ arg })" self.path #}}} end #}}} end class Tcsh < Csh #{{{ DEFAULT_PROG = 'tcsh' #}}} end class Shell < Bash; end end