Module: Nuggets::IO::InteractMixin

Included in:
IO
Defined in:
lib/nuggets/io/interact_mixin.rb

Instance Method Summary collapse

Instance Method Details

#interact(input, output, timeout = nil, maxlen = 2 ** 16) ⇒ Object

call-seq:

IO.interact(input, output[, timeout[, maxlen]]) -> anArray | nil

Interact with both ends of a pipe in a non-blocking manner.

input represents the sending end and is a mapping from the actual input IO (to send into the pipe) to the pipe’s input handle (stdin). The input IO must support read_nonblock. If it’s a StringIO it will be extended appropriately. If it’s a String it will be converted to a StringIO.

output represents the receiving end and is a mapping from the pipe’s output handle (stdout) to the designated output IO (to receive data from the pipe), and, optionally, from the pipe’s error handle (stderr) to the designated error IO. The output and error IO must support << with a string argument. If either of them is a Proc it will be extended such that << delegates to call.

timeout, if given, will be passed to IO::select and nil is returned if the select call times out; in all other cases an empty array is returned.

maxlen is the chunk size for read_nonblock.

Examples:

require 'open3'

# simply prints 'input string' on STDOUT, ignores +stderr+
Open3.popen3('cat') { |stdin, stdout, stderr|
  IO.interact({ "input string\n" => stdin }, { stdout => STDOUT })
}

# prints lines you type in reverse order to a string
str = ''
Open3.popen3('tac') { |stdin, stdout, stderr|
  IO.interact({ STDIN => stdin }, { stdout => str })
}
puts str

# prints the IP adresses from /etc/hosts on STDOUT and their lengths
# on STDERR
cmd = %q{ruby -ne 'i = $_.split.first or next; warn i.length; puts i'}
Open3.popen3(cmd) { |stdin, stdout, stderr|
  File.open('/etc/hosts') { |f|
    IO.interact({ f => stdin }, { stdout => STDOUT, stderr => STDERR })
  }
}


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/nuggets/io/interact_mixin.rb', line 79

def interact(input, output, timeout = nil, maxlen = 2 ** 16)
  readers, writers = {}, {}

  output.each { |key, val|
    if val.is_a?(::Proc) && !val.respond_to?(:<<)
      class << val; alias_method :<<, :call; end
    end

    readers[key] = val
  }

  input.each { |key, val|
    if key.is_a?(::String)
      require 'stringio'
      key = ::StringIO.new(key)
    end

    unless key.respond_to?(:read_nonblock)
      def key.read_nonblock(*args)
        read(*args) or raise ::EOFError, 'end of string reached'
      end
    end

    writers[val] = [key, '']
  }

  close = lambda { |*args|
    container, item, read = args

    container.delete(item)
    read ? item.close_read : item.close_write
  }

  read = lambda { |*args|
    reader, buffer, writer = args

    container = writer ? writers : readers
    buffer ||= container[reader]

    begin
      buffer << reader.read_nonblock(maxlen)
    rescue ::Errno::EAGAIN
    rescue ::EOFError
      buffer.force_encoding(
        reader.internal_encoding  ||
        reader.external_encoding  ||
        Encoding.default_internal ||
        Encoding.default_external
      ) if buffer.respond_to?(:force_encoding)

      close[container, writer || reader, !writer]
    end
  }

  until readers.empty? && writers.empty?
    fhs = select(readers.keys, writers.keys, nil, timeout) or return

    fhs[0].each { |reader| read[reader] }

    fhs[1].each { |writer|
      reader, buffer = writers[writer]
      read[reader, buffer, writer] or next if buffer.empty?

      begin
        bytes = writer.write_nonblock(buffer)
      rescue ::Errno::EPIPE
        close[writers, writer]
      end

      if bytes
        buffer.force_encoding('BINARY') if buffer.respond_to?(:force_encoding)
        buffer.slice!(0, bytes)
      end
    }
  end

  []
end