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 +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: []
|