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

Категории