3

I want to make a method that writes some CSV output to a filename if given and stdout if not given.

It seems like I need to treat my calls to CSV differently depending on if it's a file or to stdout, but I'd really like to treat the output stream z as something I can write to and not have to whether it's a file on disk or the stdout stream.

Is this possible?

Below is my attempt and errors:

require 'csv'
require 'pathname'

require 'csv'
require 'pathname'

def write_to_csv_or_stdout foo, bar, z=nil
  z = Pathname.new(z) if z
  z ||= $stdout

  res = [[foo, bar, "baz"]]
  CSV(z) do |csv|
    res.each do |row|
      csv << row
    end
  end
end


write_to_csv_or_stdout "foo", "bar"
# foo
# bar
#=> foo,bar,baz
# write_to_csv_or_stdout "foo", "bar", "baz"
# (NoMethodError)
mbigras
  • 7,664
  • 11
  • 50
  • 111
  • What is your goal? Please don't use generic names like `some_method`, `x`, `y`, `z`. Especially with unused variables. – Eric Duminil Jan 19 '17 at 09:48

3 Answers3

3

This works for stdout and filenames.

It uses $stdout.dup to be able to close io without closing $stdout.

require 'csv'

def write_csv(rows, filename = nil)
  io = filename ? File.open(filename, 'w+') : $stdout.dup
  CSV(io) do |csv|
    rows.each do |row|
      csv << row
    end
  end
ensure
  io.close
end

rows = [["foo", "bar", "baz"]]

write_csv(rows)
#=> foo,bar,baz

write_csv(rows, 'test.csv')
#=> 'test.csv' written
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • Thanks for the response! I checked out [another answer](http://stackoverflow.com/a/2192010/2909897). You don't need the `begin` and `end` keywords for your `ensure` to work :) – mbigras Jan 19 '17 at 17:55
  • @mbigras : Thanks. rubocop agrees with you. – Eric Duminil Jan 19 '17 at 20:36
  • You'd probably want to add a nil check to the close, in case the open failed: `io.close if io`. – Keith Bennett Oct 28 '20 at 02:35
  • @KeithBennett: Interesting. Can it really happen that `File.open` returns `nil`? I thought that it would be either raise an error or open the file. Do you have an example in which `io.close` would fail with a NoMethodError? – Eric Duminil Oct 28 '20 at 07:26
  • 1
    @EricDuminil That's a good point. The nil check would be safer in case the code changes in any of the following ways: 1) something is added prior to the file open that could raise an error, 2) a conditional file close (and set to nil) is added to the method body for another purpose. That said, this code is fine, thanks for the correction. – Keith Bennett Nov 08 '20 at 23:19
0

One solution would be not to use only the Pathname object since it isn't an IO object.

If you open the file then you can use it the same way as you would use the stdout object.

def some_method x, y, z=nil
  puts x
  puts y
  z = Pathname.new(z).open if z # <== here you get an IO Object back
  z ||= $stdout

  res = [["foo", "bar", "baz"]]
  CSV(z) do |csv|
    res.each do |row|
      csv << row
    end
  end
end
Shimu
  • 1,137
  • 11
  • 25
0

I'd use a helper method:

def write_csv(io_or_filename, &block)
  if io_or_filename.is_a?(IO)
    CSV.new(io_or_filename, &block)
  else
    CSV.open(io_or_filename, 'wb', &block)
  end
end

This can be used instead of CSV(...):

def some_method(x, y, z = $stdout)
  write_csv(z) do |csv|
    csv << # ...
  end
end
Stefan
  • 109,145
  • 14
  • 143
  • 218