magritte 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/README.md +128 -0
- data/lib/magritte.rb +3 -0
- data/lib/magritte/line_buffer.rb +32 -0
- data/lib/magritte/pipe.rb +171 -0
- metadata +77 -0
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.
|
data/README.md
ADDED
@@ -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.
|
data/lib/magritte.rb
ADDED
@@ -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: []
|