lib/backticks/command.rb in backticks-0.5.0 vs lib/backticks/command.rb in backticks-1.0.0rc1

- old
+ new

@@ -18,16 +18,22 @@ CHUNK = 1_024 # @return [Integer] child process ID attr_reader :pid - # @return [String] all data captured (so far) from child's stdin/stdout/stderr - attr_reader :captured_input, :captured_output, :captured_error - # @return [nil,Process::Status] result of command if it has ended; nil if still running attr_reader :status + # @return [String] all input that has been captured so far + attr_reader :captured_input + + # @return [String] all output that has been captured so far + attr_reader :captured_output + + # @return [String] all output to stderr that has been captured so far + attr_reader :captured_error + # Watch a running command. def initialize(pid, stdin, stdout, stderr) @pid = pid @stdin = stdin @stdout = stdout @@ -36,14 +42,27 @@ @captured_input = String.new.force_encoding(Encoding::BINARY) @captured_output = String.new.force_encoding(Encoding::BINARY) @captured_error = String.new.force_encoding(Encoding::BINARY) end + # @return [String] + def to_s + "#<Backticks::Command(@pid=#{pid},@status=#{@status || 'nil'})>" + end + def interactive? !@stdin.nil? end + # Provide a callback to monitor input and output in real time. + # @yield + # @yieldparam + def tap(&block) + raise StandardError.new("Tap is already set (#{@tap}); cannot set twice") if @tap && @tap != block + @tap = block + end + # Block until the command exits, or until limit seconds have passed. If # interactive is true, pass user input to the command and print its output # to Ruby's output streams. If the time limit expires, return `nil`; # otherwise, return self. # @@ -71,57 +90,66 @@ # Block until one of the following happens: # - the command produces fresh output on stdout or stderr # - the user passes some input to the command (if interactive) # - the process exits - # - the time limit elapses (if provided) + # - the time limit elapses (if provided) OR 60 seconds pass # # Return up to CHUNK bytes of fresh output from the process, or return nil # if no fresh output was produced # # @param [Float,Integer] number of seconds to wait before returning nil # @return [String,nil] fresh bytes from stdout/stderr, or nil if no output - private def capture(limit=nil) streams = [@stdout, @stderr] streams << STDIN if interactive? if limit tf = Time.now + limit else tf = FOREVER end - ready, _, _ = IO.select(streams, [], [], 1) + ready, _, _ = IO.select(streams, [], [], 0) # proxy STDIN to child's stdin if ready && ready.include?(STDIN) - input = STDIN.readpartial(CHUNK) rescue nil - if input - @captured_input << input - @stdin.write(input) + data = STDIN.readpartial(CHUNK) rescue nil + if data + data = @tap.call(:stdin, data) if @tap + if data + @captured_input << data + @stdin.write(data) + end else + @tap.call(:stdin, nil) if @tap # our own STDIN got closed; proxy this fact to the child @stdin.close unless @stdin.closed? end end # capture child's stdout and maybe proxy to STDOUT if ready && ready.include?(@stdout) data = @stdout.readpartial(CHUNK) rescue nil if data - @captured_output << data - STDOUT.write(data) if interactive? - fresh_output = data + data = @tap.call(:stdout, data) if @tap + if data + @captured_output << data + STDOUT.write(data) if interactive? + fresh_output = data + end end end # capture child's stderr and maybe proxy to STDERR if ready && ready.include?(@stderr) data = @stderr.readpartial(CHUNK) rescue nil if data - @captured_error << data - STDERR.write(data) if interactive? + data = @tap.call(:stderr, data) if @tap + if data + @captured_error << data + STDERR.write(data) if interactive? + end end end fresh_output rescue Interrupt # Proxy Ctrl+C to the child