magritte 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []