+# Methods common to IO::Readable, IO::Writable, and IO::Seekable.
+module IO::Common
+
+ # Calls both +close_read+ and +close_write+, and raises an exception unless
+ # at least one succeeds. It may be desirable to reimplement this.
+ def close
+ cr = respond_to?(:close_read)
+ cw = respond_to?(:close_write)
+ if cr && cw
+ failed = false
+ begin
+ close_read
+ rescue IOError
+ failed = $!
+ end
+ begin
+ close_write
+ rescue IOError
+ return unless failed
+ raise
+ end
+ elsif cr
+ close_read
+ elsif cw
+ close_write
+ else
+ raise NotImplementedError, "close_read and close_write unimplemented", caller
+ end
+ end
+
+ # Checks both +closed_read+? and +closed_write+?, raising an exception if
+ # neither method exists. This method must be redefined for IO::Readable
+ # and IO::Writable classes unless one of the previously mentioned methods
+ # has been provided.
+ def closed?
+ cr = respond_to?(:closed_read?)
+ cw = respond_to?(:closed_write?)
+ if cr && cw
+ return closed_read? && closed_write?
+ elsif cr
+ return closed_read?
+ elsif cw
+ return closed_write?
+ else
+ raise NotImplementedError, "closed_read? and closed_write? unimplemented", caller
+ end
+ end
+
+ # Do not override this method. Instead, use +isatty+.
+ def tty?
+ isatty
+ end
+
+ # Always +false+. May be redefined.
+ def isatty
+ false
+ end
+
+ # Always 0. May be redefined.
+ def fsync
+ 0
+ end
+
+ # Always +true+. May be redefined.
+ def sync
+ true
+ end
+
+ # Raises a +NotImplementedError+. May be redefined.
+ def fcntl(*args)
+ raise NotImplementedError, "fcntl not implemented", caller
+ end
+
+ # Always returns the object. May be redefined.
+ def binmode
+ self
+ end
+
+ [ :fileno, :pid, :sync=, :path ].each do |method|
+ define_method(method) {nil}
+ end
+
+end
+
+# The IO::Readable mixin provides classes with several methods for reading
+# a data stream, similar to those found in the IO class. The +sysread+ and
+# +close_read+ methods must be defined. Additionally, <tt>closed_read?</tt>,
+# <tt>closed?</tt>, and <tt>close</tt> are recommended.
+#
+# IO::Readable provides buffering, but only the absolute minimum needed to
+# provide a complete implementation. For example, #eof checks for the end of
+# file by reading and buffering a single byte.
+#
+# The example below illustrates the use of IO::Readable in a class that
+# concatenates multiple streams. Note that seeking is not supported but one
+# can start over with #rewind.
+#
+# :include:../../test/read_joiner.rb
+module IO::Readable
+
+ REQUIRED_METHODS = [ :sysread, :close_read ]
+
+ include IO::Common
+ include Enumerable
+
+ # This adds three identically named class methods that function like
+ # IO::read, IO::readlines, and IO::foreach. These may be factored into
+ # IO::Openable or removed entirely in a future release.
+ def self.included(base) #:nodoc:
+ unless base.respond_to?(:read)
+ def base.read(name,length=nil,offset=nil)
+ name = [name] unless name.respond_to?(:to_ary)
+ object = new(*name)
+ object.seek(offset) if offset
+ object.read(length)
+ ensure
+ object.close_read if object.respond_to?(:close_read)
+ end
+ end
+ unless base.respond_to?(:readlines)
+ def base.readlines(name,sep_string=$/)
+ name = [name] unless name.respond_to?(:to_ary)
+ object = new(*name)
+ object.readlines(sep_string)
+ ensure
+ object.close_read if object.respond_to?(:close_read)
+ end
+ end
+ unless base.respond_to?(:foreach)
+ def base.foreach(*args,&block)
+ readlines(*args).each(&block)
+ end
+ end
+ super
+ end
+
+ # As with with IO#gets, read and return a line, with lines separated by
+ # +sep_string+. This method may be overridden, although this is not
+ # recommended unless one is intimately familiar with the required behaviors
+ # and side effects.
+ def gets(sep_string=$/)
+ return read(nil) unless sep_string
+ line = ""
+ paragraph = false
+ if sep_string == ""
+ sep_string = "\n\n"
+ paragraph = true
+ end
+ sep = sep_string.dup
+ position = @_iom_pos
+ while (char = getc)
+ if paragraph && line.empty?
+ if char == ?\n
+ next
+ # else
+ # paragraph = false
+ end
+ end
+ if char == sep[0]
+ sep[0] = ""
+ else
+ sep = sep_string.dup
+ end
+ if sep == ""
+ if paragraph
+ ungetc char
+ else
+ line << char
+ end
+ break
+ end
+ line << char
+ if position && @_iom_pos == position
+ raise IOError, "loop encountered", caller
+ end
+ end
+ line = nil if line == ""
+ if line && respond_to?(:lineno=) && respond_to?(:lineno)
+ self.lineno += 1
+ $. = lineno
+ end
+ $_ = line
+ end
+
+ # This method returns the number of times IO::Readable#gets was called.
+ # The count is reset on calls to IO::Seekable#rewind.
+ def lineno
+ @_iom_lineno ||= 0
+ end
+
+ # This method and +lineno+ must be redefined together, or not at all.
+ def lineno=(num)
+ @_iom_lineno = num
+ end
+
+ # Like IO#read, read up to +length+ bytes and return them, optionally
+ # assigning them to +buffer+ as well.
+ def read(length = nil,buffer = "")
+ raise ArgumentError, "negative length #{length} given", caller if (length||0) < 0
+ return "" if length == 0 && (@_iom_buffer.nil? || @_iom_buffer.length > 0)
+ return (length ? nil : "") if eof
+ return "" if length == 0
+ @_iom_buffer ||= ""
+ if length
+ begin
+ @_iom_buffer << sysread(length-@_iom_buffer.length) if @_iom_buffer.length<length
+ # rescue EOFError
+ end
+ else
+ begin
+ while str = sysread(1024)
+ @_iom_buffer << str
+ end
+ rescue EOFError
+ nil # For coverage
+ end
+ end
+ buffer[0..-1] = @_iom_buffer.slice!(0..(length || 0)-1)
+ @_iom_pos ||= 0
+ @_iom_pos += buffer.length
+ return buffer
+ end
+
+ # See IO#getc. Implemented via #read.
+ def getc
+ read(1).to_s[0]
+ end
+
+ # See IO#ungetc.
+ def ungetc(char)
+ raise IOError, "unread stream", caller unless @_iom_buffer
+ @_iom_buffer[0,0] = char.chr
+ @_iom_pos -= 1
+ nil
+ end
+
+ # See IO#eof.
+ def eof
+ # tell if respond_to?(:tell)
+ @_iom_buffer ||= ""
+ return false unless @_iom_buffer.empty?
+ str = sysread(1)
+ if str
+ @_iom_buffer << str
+ @_iom_buffer.empty?
+ else
+ true
+ end
+ rescue EOFError
+ return true
+ end
+
+ # Reads a character as with IO#getc, but raises an +EOFError+ on end of
+ # file. Implemented via #getc.
+ def readchar()
+ getc or raise EOFError, "end of file reached", caller
+ end
+
+ # Reads a character as with #gets, but raises an +EOFError+ on
+ # end of file. Implemented via #gets.
+ def readline(sep_string = $/)
+ gets(sep_string) or raise EOFError, "end of file reached", caller
+ end
+
+ # See IO#readlines. Implemented via #gets.
+ def readlines(sep_string = $/)
+ array = []
+ line = nil
+ array << line while line = gets(sep_string)
+ array
+ end
+
+ # See IO#each_byte. Implemented via #getc.
+ def each_byte
+ while byte = getc
+ yield byte
+ end
+ self
+ end
+
+ # See IO#each_line. Implemented via #gets.
+ def each_line(sep_string = $/)
+ while line = gets(sep_string)
+ yield line
+ end
+ self
+ end
+
+ alias each each_line
+
+ # See IO#eof?. Implemented via #eof.
+ def eof?
+ eof
+ end
+
+end
+
+# The IO::Writable mixin provides classes with several methods for writing
+# a data stream, similar to those found in the IO class. The methods
+# +syswrite+ and +close_write+ must be provided. Additionally,
+# <tt>closed_write?</tt>, <tt>closed?</tt>, and +close+ are recommended.
+#
+# The following example illustrates the use of IO::Readable, IO::Writable, and
+# IO::Seekable. It wraps around another stream and applies rot13 to the
+# contents.
+#
+# :include:../../test/rot13_filter.rb
+module IO::Writable
+
+ REQUIRED_METHODS = [ :syswrite, :close_write ]
+ include IO::Common
+
+ # See IO#write.
+ def write(string)
+ @_iom_pos ||= 0
+ @_iom_pos += string.to_s.length
+ @_iom_buffer.slice!(0,string.to_s.length) if @_iom_buffer
+ syswrite(string.to_s)
+ end
+
+ # See IO#<<. Implemented via #write.
+ def <<(string)
+ # write(string.respond_to?(:to_int) ? string.to_int.chr : string)
+ write(string)
+ self
+ end
+
+ # See IO#print. Implemented via #write.
+ def print(obj = $_,*args)
+ ([obj]+args).each do |line|
+ write(line + ($\||''))
+ end
+ nil
+ end
+
+ # See IO#puts. Implemented via #write.
+ def puts(obj = "\n",*args)
+ [obj,args].flatten.each do |line|
+ line = "nil" if line.nil?
+ write(line.to_s + (line.to_s[-1] == ?\n ? "" : "\n"))
+ end
+ nil
+ end
+
+ # See IO#printf. Implemented via #write.
+ def printf(first="",*rest)
+ write(sprintf(first,*rest))
+ nil
+ end
+
+ # See IO#putc. Implemented via #write
+ def putc(char)
+ if char.respond_to?(:to_int)
+ write((char.to_int & 0xFF).chr)
+ elsif char.respond_to?(:to_str) && char.to_str.length > 0
+ write(char.to_str[0,1])
+ else
+ raise TypeError, "can't convert #{char.class} to Integer", caller
+ end
+ char
+ end
+
+ # Returns +self+. May be redefined.
+ def flush
+ self
+ end
+
+end
+
+# The IO::Seekable mixin provides classes with several methods for seeking
+# within a data stream, similar to those found in the IO class. The method
+# +sysseek+ must be provided. This method should operate identically to
+# IO#sysseek, seeking to the desired location and returning the final
+# absolute offset. One should also consider optimizing for
+# <tt>sysseek(0,IO::SEEK_CUR)</tt>. This particular call should simply return
+# the position in the stream.
+#
+# For an example implementation, see IO::Writable.
+module IO::Seekable
+
+ REQUIRED_METHODS = [ :sysseek ]
+ include IO::Common
+
+ # See IO#pos. Implemented via #tell.
+ def pos
+ tell
+ end
+
+ # See IO#pos=. Implemented via #seek.
+ def pos=(pos)
+ seek(pos)
+ tell
+ end
+
+ # See IO#rewind. Calls both #seek and IO::Readable#lineno=, if included.
+ def rewind
+ seek(0)
+ self.lineno = 0 if respond_to?(:lineno=)
+ 0
+ end
+
+ # See IO#seek. This method calls #sysseek and only should be overridden in a
+ # class that is taking responsibility for tracking its own position. If this
+ # is the case, the class should also override #seek, as well as
+ # IO::Readable#read, IO::Readable#ungetc, IO::Readable#getc,
+ # IO::Readable#eof, IO::Writable#write, and IO::Writable#putc, if the
+ # respective modules are included.
+ def seek(amount, whence = IO::SEEK_SET)
+ if whence == IO::SEEK_CUR && @_iom_buffer
+ amount -= @_iom_buffer.length
+ end
+ @_iom_pos ||= 0
+ @_iom_pos += @_iom_buffer.length if @_iom_buffer
+ @_iom_buffer = nil if @_iom_buffer
+ @_iom_pos = sysseek(amount, whence)
+ 0
+ end
+
+ # See IO#tell. See #seek for information on overriding this method.
+ def tell
+ @_iom_pos ||= 0
+ end
+
+end
+
+# The IO::Closable mixin works with IO::Readable, IO::Writable, and
+# IO::Seekable. It provides #closed_read?, #closed_write?, #close_read, and
+# #close_write methods, and wraps the required methods for the aforementioned
+# modules such that they raise an exception if applied to a closed object.
+# You must include _at_ _least_ one of IO::Readable or IO::Writable for this
+# mixin to be useful.
+#
+# This mixin is highly experimental. It currently works by trapping
+# method_added and wrapping the newly defined method.
+module IO::Closable
+ WRAPPED_READ_METHODS = [ :sysread, :getc, :ungetc, :eof, :read, :gets ]
+ WRAPPED_WRITE_METHODS = [ :syswrite, :putc, :write ]
+ WRAPPED_BOTH_METHODS = [ :sysseek, :tell, :seek, :reopen, :pos, :pos= ]
+ WRAPPED_METHODS = WRAPPED_READ_METHODS + WRAPPED_WRITE_METHODS + WRAPPED_BOTH_METHODS
+
+ # Either #close or #close_read was called.
+ def closed_read?
+ !respond_to?(:sysread) || !!@_closed_read
+ end
+
+ # Either #close or #close_write was called
+ def closed_write?
+ !respond_to?(:syswrite) || !!@_closed_write
+ end
+
+ # Object is closed for both reading and writing
+ def closed?
+ closed_read? && closed_write?
+ end
+
+ # Wrap an existing method in with a prerequisite that the file must not be
+ # closed.
+ #
+ # wrap_method_for_close(:printf, :closed_write?)
+ def self.wrap_method_for_close(method,condition = :closed?) #:nodoc:
+ class_eval(<<-EOF,__FILE__,__LINE__)
+ def with_io_sneakiness_do_#{method}(*args,&block)
+ raise IOError, "stream closed", caller if #{condition}
+ send(:without_io_sneakiness_do_#{method},*args,&block)
+ end
+ EOF
+ private "with_io_sneakiness_do_#{method}"
+ end
+
+ WRAPPED_READ_METHODS.each do |method|
+ wrap_method_for_close(method,:closed_read?)
+ end
+
+ WRAPPED_WRITE_METHODS.each do |method|
+ wrap_method_for_close(method,:closed_write?)
+ end
+
+ WRAPPED_BOTH_METHODS.each do |method|
+ wrap_method_for_close(method,:closed?)
+ end
+
+ def with_io_sneakiness_do_reopen(*args, &block) #:nodoc:
+ @_closed_read = nil
+ @_closed_write = nil
+ without_io_sneakiness_do_reopen(*args,&block)
+ end
+
+ # After this method is called, future read operations will fail, and
+ # #closed_read? will read return true. If this method is redefined, it
+ # will be transparently wrapped to preserve this behavior.
+ def close_read(*args,&block)
+ closed_io_error if closed_read?
+ @_closed_read = true
+ without_io_sneakiness_do_close_read(*args,&block) if respond_to?(:without_io_sneakiness_do_close_read)
+ without_io_sneakiness_do_close(*args,&block) if closed_write? && respond_to?(:without_io_sneakiness_do_close)
+ end
+
+ # After this method is called, future write operations will fail, and
+ # #closed_write? will write return true. If this method is redefined, it
+ # will be transparently wrapped to preserve this behavior.
+ def close_write(*args,&block)
+ closed_io_error if closed_write?
+ @_closed_write = true
+ without_io_sneakiness_do_close_write(*args,&block) if respond_to?(:without_io_sneakiness_do_close_write)
+ without_io_sneakiness_do_close(*args,&block) if closed_read? && respond_to?(:without_io_sneakiness_do_close)
+ end
+
+ # Calls both close_read and close_write.
+ def close(*args,&block)
+ closed_io_error if closed?
+ close_read(*args,&block) unless closed_read?
+ close_write(*args,&block) unless closed_write?
+ end
+
+ [:close_read, :close_write, :close].each do |method|
+ alias_method("with_io_sneakiness_do_#{method}", method)
+ end
+
+
+ def self.included(base) #:nodoc:
+ # p base.instance_methods.sort
+ WRAPPED_METHODS.each do |method|
+ if method_defined?(method) && !private_method_defined?(without) #&& private_method_defined?(with)
+ base.send(:alias_method, without, method)
+ base.send(:alias_method, method, with)
+ base.send(:private, with, without)
+ base.send(:public, method)
+ end
+ end
+ ::IO::Sneakiness.extend_into(base)
+end
+
+ private
+ def closed_io_error
+ raise IOError, "stream closed", caller[1..-1]
+ end
+
+end
+
+# The IO::Openable mixin is a set of _class_ methods concerning opening a
+# stream. As such, it should be +extend+ed, not +include+d into a class.
+#
+# class MyIO
+# include IO::Readable
+# include IO::Writable
+# include IO::Seekable
+# extend IO::Openable
+# # ...
+# end
+module IO::Openable
+ # Currently the only method, this method creates a new object, and yields
+ # it if a block is given, in a manner similar to IO::open.
+ def open(*args)
+ io = new(*args)
+ if block_given?
+ begin
+ yield io
+ ensure
+ io.close
+ end
+ else
+ io
+ end
+ end
+end
+
+# The IO::Editable mixin allows for full emulation of the IO class by
+# including IO::Readable, IO::Writable, IO::Seekable, and IO::Closable. The
+# following is a partial implementation of a looped IO class which writes to
+# its own input.
+#
+# :include:../../test/io_loop.rb
+module IO::Editable
+ REQUIRED_METHODS = IO::Readable::REQUIRED_METHODS + IO::Writable::REQUIRED_METHODS + IO::Seekable::REQUIRED_METHODS
+ if false # for rdoc
+ include Readable
+ include Writable
+ include Seekable
+ include Closable
+ end
+
+ # Adds an +open+ class method which acts like IO::open.
+ def self.included(base)
+ unless base.respond_to?(:open)
+ base.extend(IO::Openable)
+ [ IO::Readable, IO::Writable, IO::Seekable, IO::Closable ].each do |mod|
+ base.send(:include,mod)
+ # mod.send(:included,base)
+ end
+ end
+ super
+ end
+end
+
+# We nest these in IO because otherwise, :nodoc: is ignored.
+class IO #:nodoc:
+
+ MIXINS_VERSION = '0.3'
+ module Sneakiness #:nodoc:
+ def method_added(id)
+ method = id.id2name
+ with = "with_io_sneakiness_do_#{method}"
+ without = "without_io_sneakiness_do_#{method}"
+ if !@io_sneakiness_disabled && (method_defined?(with) || private_method_defined?(with))
+ alias_method without, method
+ disable_sneakiness do
+ alias_method method, with
+ private with, without
+ public method
+ end
+ end
+ method_added_without_io_sneakiness(method)
+ end
+
+ def self.extend_into(base)
+ unless base.respond_to?(:method_added_without_io_sneakiness)
+ class << base
+ alias_method :method_added_without_io_sneakiness, :method_added
+ end
+ base.extend(self)
+ end
+ end
+
+ protected
+ def disable_sneakiness
+ old_crit = Thread.critical
+ Thread.critical = true
+ @io_sneakiness_disabled = true
+ yield
+ ensure
+ @io_sneakiness_disabled = false
+ Thread.critical = old_crit if Thread.critical != old_crit
+ end
+
+ end
+
+ # Deprecated.
+ module Simple #:nodoc:
+ include IO::Editable
+
+ def sysread(length)
+ return "" if length == 0
+ @_simple_buffer ||= ""
+ while @_simple_buffer.length < length && chunk = (readchunk rescue nil)
+ break if chunk == ""
+ @_simple_buffer << chunk
+ end
+ out = @_simple_buffer.slice!(0,length)
+ if out.length == 0
+ raise EOFError, "end of file reached", caller
+ else
+ out
+ end
+ end
+
+ end
+end
+
+# end