magritte 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2013 MyDrive Solutions Limited, All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions
5
+ are met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above
11
+ copyright notice, this list of conditions and the following
12
+ disclaimer in the documentation and/or other materials provided
13
+ with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
18
+ FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
19
+ COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
20
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
25
+ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
+ POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,128 @@
1
+ Magritte
2
+ ========
3
+ This is a simple but powerful wrapper to Open3 pipes that makes it
4
+ easy to handle two-way piping of data into and out of a sub-process.
5
+ Various input IO wrappers are supported and output can either be
6
+ to an IO or to a block. A simple line buffer class is also provided,
7
+ to turn block writes to the output block into line-by-line output
8
+ to make interacting with the sub-process easier.
9
+
10
+ What it Does
11
+ ------------
12
+ You have a sub-command that you want to put data into and from which
13
+ you want to retrieve the output, much like a Unix command line pipe.
14
+ This is a non-trivial operation involving non-blocking reads and writes, the
15
+ checking of the state of the input and output IOs, etc. Magritte
16
+ abstracts all of that behind an easy to use, fluent interface.
17
+
18
+
19
+ Usage
20
+ -----
21
+
22
+ ####Simplest Use Case
23
+ For purposes of showing a simple example, let's say you wanted to
24
+ use the command line tool `grep` to filter some input data. Yes,
25
+ you can do this natively in Ruby, but it's a trivial and easy to
26
+ understand example. The normal use case would be wrapping an existing
27
+ custom command line tool with Ruby.
28
+
29
+ But, back to `grep`. To store the output into a `StringIO` you could
30
+ do the following:
31
+
32
+ ```ruby
33
+ buffer = StringIO.new
34
+ Magritte::Pipe.from_input_file('some.txt')
35
+ .out_to(buffer)
36
+ .filtering_with('grep "relistan"')
37
+ ```
38
+
39
+ This example will take the contents of `some.txt` and stream it through
40
+ `grep "relistan"`, storing the results in `buffer`.
41
+
42
+ ####String as Input
43
+
44
+ ```ruby
45
+ data = "foo\nfoo\nrelistan\n"
46
+ buffer = StringIO.new
47
+
48
+ Magritte::Pipe.from_input_string(data)
49
+ .out_to(buffer)
50
+ .filtering_with('grep "relistan"')
51
+ ```
52
+
53
+ ####IO Stream as Input
54
+
55
+ ```ruby
56
+ buffer = StringIO.new
57
+ socket = Socket.new(xxx)
58
+
59
+ Magritte::Pipe.from_input_stream(socket)
60
+ .out_to(buffer)
61
+ .filtering_with('grep "relistan"')
62
+ ```
63
+
64
+ ####Output to a Block
65
+
66
+ Rather than outputting the results to a stream, you can provide a block
67
+ to `out_to` which will be invoked on each read of output data from the
68
+ sub-process. This allows you to process the data in a stream-like
69
+ manner without having to buffer all of the output and then process it
70
+
71
+ NOTE: Like a call to `IO.write`, the block _must_ return the number of
72
+ bytes processed. This is fed back to the buffering process to make
73
+ sure that the next iteration of data will include any missed bytes
74
+ when sent to your output block.
75
+
76
+ ```ruby
77
+ Magritte::Pipe.from_input_file('some.txt')
78
+ .out_to { |data| $stdout.write data; data.size }
79
+ .filtering_with('grep "relistan"')
80
+ ```
81
+
82
+ ####Line Buffering
83
+
84
+ When passing data into your block, it's often much easier to work on
85
+ it if you can access it line-by-line rather than as a stream of data.
86
+ Magritte supports this with a provided `LineBuffer` class that is
87
+ wrapped into the API. You simply call `line_by_line` and your `out_to`
88
+ block will be invoked on each line, one at a time. Note that when
89
+ using the `LineBuffer` you do *not* need to specify the number of bytes
90
+ written by your `out_to` block as the `LineBuffer` handles this for you.
91
+
92
+ ```ruby
93
+ Magritte::Pipe.from_input_file('some.txt')
94
+ .line_by_line
95
+ .out_to { |data| puts data }
96
+ .filtering_with('grep "relistan"')
97
+ ```
98
+
99
+ Exit Status
100
+ -----------
101
+ Magritte will raise an `Errno::EPIPE` in the event of a non-zero
102
+ status code in the sub-process.
103
+
104
+ Limitations
105
+ -----------
106
+ To simplify implementation, Magritte uses `Open3.popen2e` which combines
107
+ `stderr` and `stdout` on the output stream. This means that in the event
108
+ of an error in the sub-process, any output will be contained in the same
109
+ output stream as the rest of the data. I've found that ordinarily this
110
+ is what you want, but it might not work for all situations. If there is
111
+ enough interest, I may implement a more complicated alternative later.
112
+
113
+ In line-by-line mode with an output block provded to `.out_to`, the output
114
+ *must* provide a terminating record separator or the last line will not
115
+ be passed to the block.
116
+
117
+ Credits
118
+ -------
119
+ This software was written by [Karl Matthias](https://github.com/relistan).
120
+ [The name](http://en.wikipedia.org/wiki/The_Treachery_of_Images)
121
+ was suggested by [Gavin Heavyside](https://github.com/gavinheavyside).
122
+ Magritte was developed with the support of
123
+ [MyDrive Solutions Limited](http://mydrivesolutions.com).
124
+
125
+ License
126
+ -------
127
+ This plugin is released under the BSD two clause license which is
128
+ available in both the Ruby Gem and the source repository.
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), 'magritte', '*')].each do |file|
2
+ require file
3
+ end
@@ -0,0 +1,32 @@
1
+ module Magritte
2
+ class LineBuffer
3
+ include Enumerable
4
+
5
+ attr_reader :buffer
6
+
7
+ def initialize(record_separator="\n")
8
+ @buffer = ""
9
+ @record_separator = record_separator
10
+ end
11
+
12
+ def write(data)
13
+ last_eol = data.rindex(@record_separator)
14
+ return 0 unless last_eol
15
+
16
+ data = data[0..(last_eol + @record_separator.size - 1)]
17
+ @buffer += data
18
+ data.size
19
+ end
20
+
21
+ def each_line(&block)
22
+ raise ArgumentError.new("No block passed to each_line!") unless block_given?
23
+ return if buffer.empty?
24
+
25
+ lines = @buffer.split(@record_separator)
26
+ lines.each(&block)
27
+ @buffer = ""
28
+ end
29
+
30
+ alias_method :each, :each_line
31
+ end
32
+ end
@@ -0,0 +1,171 @@
1
+ require 'open3'
2
+ require 'stringio'
3
+ require_relative 'line_buffer'
4
+
5
+ # Acts as a two way pipe like the shell command line. We put
6
+ # data into a sub-process and capture the output.
7
+
8
+ module Magritte
9
+ class Pipe
10
+ READ_BLOCK_SIZE = 512
11
+
12
+ def initialize(input, output=nil)
13
+ @input = input
14
+ @output = output
15
+ @line_by_line = false
16
+ end
17
+
18
+ def self.from_input_file(infile)
19
+ self.from_input_stream(File.open(infile))
20
+ end
21
+
22
+ def self.from_input_stream(io)
23
+ new(io)
24
+ end
25
+
26
+ def self.from_input_string(str)
27
+ new(StringIO.new(str || ""))
28
+ end
29
+
30
+ def out_to(io=nil, &block)
31
+ if block_given?
32
+ @output = Proc.new(&block)
33
+ else
34
+ @output = io if io
35
+ end
36
+
37
+ self
38
+ end
39
+
40
+ def line_by_line
41
+ @line_by_line = true
42
+ @buffer = LineBuffer.new
43
+ self
44
+ end
45
+
46
+ def filtering_with(command)
47
+ raise "No output IO is set! Invoke out_to first!" unless @output
48
+
49
+ Open3.popen2e(command) do |subproc_input, subproc_output, wait_thr|
50
+ @subproc_output = subproc_output
51
+ @subproc_input = subproc_input
52
+
53
+ clear_buffers
54
+
55
+ while true do
56
+ read_ready, write_ready, = select
57
+
58
+ read_from_input
59
+
60
+ if write_ready
61
+ write_to_subproc
62
+ end
63
+
64
+ if read_ready
65
+ read_from_subproc
66
+ send_output
67
+ end
68
+
69
+ # We close the input to signal to the sub-process that we are
70
+ # done sending data. It will close its output when processsing
71
+ # is completed. That signals us to stop piping data.
72
+ if ready_to_close?
73
+ @subproc_input.flush
74
+ @subproc_input.close
75
+ end
76
+
77
+ break if @subproc_output.closed?
78
+ end
79
+
80
+ raise Errno::EPIPE.new("sub-process dirty exit!") unless wait_thr.value == 0
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def clear_buffers
87
+ @write_data = ""
88
+ @read_data = ""
89
+ end
90
+
91
+ def select
92
+ output = descriptor_array_for(@subproc_output)
93
+ input = descriptor_array_for(@subproc_input)
94
+ IO.select(output, input, nil, 0.01)
95
+ end
96
+
97
+ def descriptor_array_for(stream)
98
+ stream.closed? ? nil : [stream]
99
+ end
100
+
101
+ def write_to(io, data)
102
+ return 0 if data.empty?
103
+ return 0 if io.closed?
104
+
105
+ begin
106
+ io.write_nonblock(data)
107
+ rescue Errno::EAGAIN
108
+ return 0
109
+ end
110
+ end
111
+
112
+ def write_to_subproc
113
+ bytes_written = write_to(@subproc_input, @write_data)
114
+ @write_data = @write_data[bytes_written..-1]
115
+ end
116
+
117
+ def read_from_subproc
118
+ @read_data += read_from(@subproc_output)
119
+ end
120
+
121
+ def read_from_input
122
+ @write_data += read_from(@input)
123
+ end
124
+
125
+ def send_output
126
+ bytes_written = if @line_by_line && @output.is_a?(Proc)
127
+ send_output_line_by_line
128
+ else
129
+ send_output_block
130
+ end
131
+
132
+ @read_data = @read_data[bytes_written..-1]
133
+ end
134
+
135
+ def send_output_line_by_line
136
+ bytes_written = @buffer.write(@read_data)
137
+ @buffer.each_line { |line| @output.call(line) }
138
+ bytes_written
139
+ end
140
+
141
+ def send_output_block
142
+ if @output.is_a?(Proc)
143
+ bytes_written = @output.call(@read_data)
144
+ raise 'output block must return number of bytes written!' unless bytes_written.is_a?(Fixnum)
145
+ bytes_written
146
+ else
147
+ write_to(@output, @read_data)
148
+ end
149
+
150
+ end
151
+
152
+ def read_from(io)
153
+ begin
154
+ data = io.read_nonblock(READ_BLOCK_SIZE) unless io.closed?
155
+ rescue EOFError
156
+ io.close
157
+ rescue Errno::EAGAIN
158
+ end
159
+
160
+ data || ""
161
+ end
162
+
163
+ def ready_to_close?
164
+ @input.closed? && @write_data.empty? && @read_data.empty? && !@subproc_input.closed?
165
+ end
166
+ end
167
+ end
168
+
169
+ #Magritte::Pipe.from_input_file(ARGV[0]).out_to($stdout).filtering_with("build/bin/snapper --dbname=nt2012q1")
170
+ #Magritte::Pipe.from_input_stream($stdin).out_to($stdout).filtering_with("build/bin/snapper --dbname=nt2012q1")
171
+ #Magritte::Pipe.from_input_stream($stdin).out_to { |input| asdf += input; input.size}.filtering_with("cat")
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: magritte
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Karl Matthias
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70302135231300 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70302135231300
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70302135230200 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70302135230200
36
+ description: ! ' Magritte is a simple but powerful wrapper to Open3 pipes that makes
37
+ it easy to handle two-way piping of data into and out of a sub-process. Various
38
+ input IO wrappers are supported and output can either be to an IO or to a block.
39
+ A simple line buffer class is also provided, to turn block writes to the output
40
+ block into line-by-line output to make interacting with the sub-process easier.
41
+
42
+ '
43
+ email: relistan@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/magritte/line_buffer.rb
49
+ - lib/magritte/pipe.rb
50
+ - lib/magritte.rb
51
+ - README.md
52
+ - LICENSE
53
+ homepage: http://github.com/mydrive/magritte
54
+ licenses: []
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.11
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Simple but powerful wrapper of two-way pipes to/from a sub-process.
77
+ test_files: []