lib/tee.rb

Summary

Maintainability
A
35 mins
Test Coverage
require 'tee/version'

# A class like tee(1)
class Tee
  class << self
    # @macro new
    #   @param ios [Array<IO, String>]
    #   @param options [Hash]
    #   @option options [String, Fixnum] :mode ('w')
    #   @option options [Fixnum] :perm (0666)
    #   @option options [IO, nil] :stdout ($stdout)
    #
    # @overload open(*ios, options = {})
    #   A synonym for Tee.new
    #   @macro new
    #   @return [Tee]
    #
    # @overload open(*ios, options = {}, &block)
    #   It will be passed the Tee as an argument,
    #   and the Tee will automatically be closed when the block terminates.
    #   @macro new
    #   @yieldparam tee [Tee]
    #   @return the value of the block
    def open(*args, &block)
      if block_given?
        tee = new(*args)
        begin
          yield tee
        ensure
          tee.send(:close_ios_opened_by_self)
        end
      else
        new(*args)
      end
    end
  end

  # @param value [IO, nil] Sets the attribute stdout
  # @return [IO, nil]      Returns the value of attribute stdout
  attr_accessor :stdout

  # @overload initialize(*ios, options = {})
  #   @macro new
  def initialize(*ios)
    @options = { mode: 'w' }
    @options.update(ios.pop) if ios.last.is_a?(Hash)

    @stdout = @options.key?(:stdout) ? @options.delete(:stdout) : $stdout

    @ios = []
    add(*ios)
  end

  # Add ios
  #
  # @param ios [Array<IO, String>]
  # @return [self]
  def add(*ios)
    open_args = [@options[:mode]]
    open_args << @options[:perm] if @options[:perm]

    _ios = []
    begin
      ios.each do |io|
        _ios << (
          io.respond_to?(:write) ?
            [io, false] :
            [File.open(io, *open_args), true]
        )
      end
    rescue => e
      close_ios_opened_by_self(_ios) rescue nil
      raise e
    end
    @ios.concat(_ios)

    self
  end

  # Delegates #<< to ios
  #
  # @param obj [Object]
  # @return [self]
  def <<(obj)
    each_ios_and_stdout(obj, &:<<)
  end

  # Closes all ios except stdout
  #
  # @return [nil]
  def close
    each_ios(&:close)
    nil
  end

  # Returns true if all ios except stdout is closed, false otherwise.
  #
  # @return [Boolean]
  def closed?
    each_ios.all?(&:closed?)
  end

  # Delegates #flush to ios
  #
  # @return [self]
  def flush
    each_ios_and_stdout(&:flush)
  end

  # Delegates #print to ios
  #
  # @param obj [Object]
  # @return [nil]
  def print(*obj)
    each_ios_and_stdout(*obj, &:print)
    nil
  end

  # Delegates #printf to ios
  #
  # @param format [String]
  # @param obj [Object]
  # @return [nil]
  def printf(format, *obj)
    each_ios_and_stdout(format, *obj, &:printf)
    nil
  end

  # Delegates #putc to ios
  #
  # @param char [Fixnum, String]
  # @return [Fixnum]
  # @return [String]
  def putc(char)
    each_ios_and_stdout(char, &:putc)
    char
  end

  # Delegates #puts to ios
  #
  # @param obj [Object]
  # @return [nil]
  def puts(*obj)
    each_ios_and_stdout(*obj, &:puts)
    nil
  end

  # Delegates #syswrite to ios
  #
  # @param string [String]
  # @return [Array<Integer>]
  def syswrite(string)
    each_ios_and_stdout(string).map(&:syswrite)
  end

  # Returns self
  #
  # @return [self]
  def to_io
    self
  end

  # Delegates #tty? to stdout
  #
  # @return [Boolean]
  def tty?
    @stdout ? @stdout.tty? : false
  end
  alias isatty tty?

  # Delegates #write to ios
  #
  # @param string [String]
  # @return [Array<Integer>]
  def write(string)
    each_ios_and_stdout(string).map(&:write)
  end

  # Delegates #write_nonblock to ios
  #
  # @param string [String]
  # @return [Array<Integer>]
  def write_nonblock(string)
    each_ios_and_stdout(string).map(&:write_nonblock)
  end

  private

  # @return [self]
  def each_ios(*args, &block)
    return to_enum(__method__, *args) unless block_given?
    @ios.each do |io,|
      yield io, *args
    end
    self
  end

  # @return [self]
  def each_ios_and_stdout(*args, &block)
    return to_enum(__method__, *args) unless block_given?
    yield @stdout, *args if @stdout
    each_ios(*args, &block)
  end

  # @return [nil]
  def close_ios_opened_by_self(ios = @ios)
    ios.each do |io, opened|
      io.close if opened && !io.closed?
    end
    nil
  end
end