Pretending a String Is a File

Problem

You want to call code that expects to read from an open file object, but your source is a string in memory. Alternatively, you want to call code that writes its output to a file, but have it actually write to a string.

Solution

The StringIO class wraps a string in the interface of the IO class. You can treat it like a file, then get everything that's been "written" to it by calling its string method.

Here's a StringIO used as an input source:

	require 'stringio'
	s = StringIO.new %{I am the very model of a modern major general.
	I've information vegetable, animal, and mineral.}
	
	s.pos # => 0
	s.each_line { |x| puts x }
	# I am the very model of a modern major general.
	# I've information vegetable, animal, and mineral.
	s.eof? # => true
	s.pos # => 95
	s.rewind
	s.pos # => 0
	s.grep /general/
	# => ["I am the very model of a modern major general.
"]

Here are StringIO objects used as output sinks:

	s = StringIO.new
	s.write('Treat it like a file.')
	s.rewind
	s.write("Act like it's")
	s.string							 # => "Act like it's a file."
	
	require 'yaml'
	s = StringIO.new
	YAML.dump(['A list of', 3, :items], s)
	puts s.string
	# ---
	# - A list of
	# - 3
	# - :items

 

Discussion

The Adapter is a common design pattern: to make an object acceptable as input to a method, it's wrapped in another object that presents the appropriate interface. The StringIO class is an Adapter between String and File (or IO), designed for use with methods that work on File or IO instances. With a StringIO, you can disguise a string as a file and use those methods without them ever knowing they haven't really been given a file.

For instance, if you want to write unit tests for a library that reads from a file, the simplest way is to pass in predefined StringIO objects that simulate files with various contents. If you need to modify the output of a method that writes to a file, a StringIO can capture the output, making it easy to modify and send on to its final destination.

StringIO-type functionality is less necessary in Ruby than in languages like Python, because in Ruby, strings and files implement a lot of the same methods to begin with. Often you can get away with simply using these common methods. For instance, if all you're doing is writing to an output sink, you don't need a StringIO object, because String#<< and File#<< work the same way:

	def make_more_interesting(io)
	 io << "… OF DOOM!"
	end

	make_more_interesting("Cherry pie") # => "Cherry pie… OF DOOM!"
	
	open('interesting_things', 'w') do |f|
	 f.write("Nightstand")
 make_more_interesting(f)
 end
	open('interesting_things') { |f| f.read } # => "Nightstand… OF DOOM!"

Similarly, File and String both include the Enumerable mixin, so in a lot of cases you can read from an object without caring what type it is. This is a good example of Ruby's duck typing.

Here's a string:

	poem = %{The boy stood on the burning deck
	Whence all but he had fled
	He'd stayed above to wash his neck
	Before he went to bed}

and a file containing that string:

	output = open("poem", "w")
	output.write(poem)
	output.close
	input = open("poem")

will give the same result when you call an Enumerable method:

	poem.grep /ed$/
	# => ["Whence all but he had fled
", "Before he went to bed"]
	input.grep /ed$/
	# => ["Whence all but he had fled
", "Before he went to bed"]

Just remember that, unlike a string, you can't iterate over a file multiple times without calling rewind:

	input.grep /ed$/							# => []
	input.rewind
	input.grep /ed$/
	# => ["Whence all but he had fled
", "Before he went to bed"]

StringIO comes in when the Enumerable methods and << aren't enough. If a method you're writing needs to use methods specific to IO, you can accept a string as input and wrap it in a StringIO. The class also comes in handy when you need to call a method someone else wrote, not anticipating that anyone would ever need to call it with anything other than a file:

	def fifth_byte(file)
	 file.seek(5)
	 file.read(1)
	end
	
	fifth_byte("123456")
	# NoMethodError: undefined method `seek' for "123456":String
	fifth_byte(StringIO.new("123456")) # => "6"

When you write a method that accepts a file as an argument, you can silently accommodate callers who pass in strings by wrapping in a StringIO any string that gets passed in:

	def file_operation(io)
	 io = 
StringIO(io) if io.respond_to? :to_str && !io.is_a? StringIO
	 #Do the file operation…
	end

A StringIO object is always open for both reading and writing:

	s = StringIO.new
	s << "A string"
	s.read # => ""
	s << ", and more."
	s.rewind
	s.read # => "A string, and more."

Memory access is faster than disk access, but for large amounts of data (more than about 10 kilobytes), StringIO objects are slower than disk files. If speed is your aim, your best bet is to write to and read from temp files using the tempfile module. Or you can do what the open-uri library does: start off by writing to a StringIO and, if it gets too big, switch to using a temp file.

See Also

  • Recipe 6.8, "Writing to a Temporary File"
  • Recipe 6.11, "Performing Random Access on "Read-Once" Input Streams"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net