coque 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/io.rb ADDED
@@ -0,0 +1,215 @@
1
+ require 'open3'
2
+
3
+ def banner(msg)
4
+ puts "******* #{msg} *******"
5
+ end
6
+
7
+ def read_from_spawned_pipe
8
+ banner("read_from_spawned_pipe")
9
+ pipe_me_in, pipe_peer_out = IO.pipe
10
+ pipe_peer_in, pipe_me_out = IO.pipe
11
+
12
+ pid = spawn('ls',
13
+ out: pipe_peer_out,
14
+ pipe_peer_out => pipe_peer_out,
15
+ in: pipe_peer_in,
16
+ pipe_peer_in => pipe_peer_in)
17
+ puts "Spawned #{pid}"
18
+
19
+ pipe_peer_out.close
20
+ puts pipe_me_in.read
21
+ end
22
+
23
+ def read_write_to_spawned
24
+ banner("read_write_to_spawned")
25
+ pipe_me_in, pipe_peer_out = IO.pipe
26
+ pipe_peer_in, pipe_me_out = IO.pipe
27
+
28
+ ['a', 'b', 'c', 'ab'].each { |l| pipe_me_out.puts(l) }
29
+
30
+ pid = spawn('grep a',
31
+ out: pipe_peer_out,
32
+ pipe_peer_out => pipe_peer_out,
33
+ in: pipe_peer_in,
34
+ pipe_peer_in => pipe_peer_in)
35
+ puts "Spawned #{pid}"
36
+ pipe_me_out.close
37
+
38
+ pipe_peer_out.close
39
+ puts pipe_me_in.read
40
+ end
41
+
42
+ def chain_two_native_processes
43
+ banner("chain_two_native_processes")
44
+ a_in_read, a_in_write = IO.pipe
45
+ a_out_read, a_out_write = IO.pipe
46
+
47
+ p1 = spawn('echo "a\nb\nc\nab\n"',
48
+ out: a_out_write,
49
+ a_out_write => a_out_write,
50
+ in: a_in_read,
51
+ a_in_read => a_in_read)
52
+
53
+ b_out_read, b_out_write = IO.pipe
54
+
55
+ p2 = spawn('grep a',
56
+ out: b_out_write,
57
+ b_out_write => b_out_write,
58
+ in: a_out_read,
59
+ a_out_read => a_out_read)
60
+
61
+ puts "Spawned a: #{p1}, b: #{p2}"
62
+ # Q: Why do we have to close these? do the spawned processes not close them?
63
+ b_out_write.close
64
+ a_out_write.close
65
+ puts b_out_read.read
66
+ end
67
+
68
+ def reading_large_output
69
+ banner("reading_large_output")
70
+ a_in_read, a_in_write = IO.pipe
71
+ a_out_read, a_out_write = IO.pipe
72
+
73
+ p1 = spawn('cat /usr/share/dict/words',
74
+ out: a_out_write,
75
+ a_out_write => a_out_write)
76
+
77
+ a_out_write.close
78
+ puts a_out_read.read
79
+ end
80
+
81
+ def pipe_to_forked_ruby
82
+ banner("pipe_to_forked_ruby")
83
+ a_in_read, a_in_write = IO.pipe
84
+ b_out_read, b_out_write = IO.pipe
85
+
86
+ ['a', 'b', 'c', 'ab'].each { |l| a_in_write.puts(l) }
87
+ a_in_write.flush
88
+ a_in_write.close
89
+
90
+ child = fork do
91
+ # change our stdin to be the read end of the pipe
92
+ STDOUT.puts "forked process to stdout (#{STDOUT.fileno})"
93
+ b_out_write.puts "******* forked process to pipe (#{b_out_write.fileno}) **********"
94
+
95
+ a_in_read.each_line { |l| b_out_write.puts "child - #{l}" }
96
+ end
97
+ puts "forked #{child}"
98
+
99
+ a_in_write.close
100
+ b_out_write.close
101
+
102
+ b_out_read.each_line { |l| puts "read from parent - #{l}" }
103
+ end
104
+
105
+
106
+ def native_process_to_ruby_block
107
+ banner("native_process_to_ruby_block")
108
+ # a_in_read, a_in_write = IO.pipe
109
+ a_out_read, a_out_write = IO.pipe
110
+ b_out_read, b_out_write = IO.pipe
111
+ puts "parent file descriptors"
112
+ puts "a read: #{a_out_read.fileno}"
113
+ puts "a write: #{a_out_write.fileno}"
114
+ puts "b read: #{b_out_read.fileno}"
115
+ puts "b write: #{b_out_write.fileno}"
116
+
117
+ # Receives copy of a_out_write; closes when done
118
+ p1 = spawn('echo "a\nb\nc\nab"', out: a_out_write)
119
+
120
+ # Receives copy of
121
+ # a_out_read - needs to read; closed automatically?
122
+ # a_out_write - doesn't need, close immediately
123
+ # b_out_read - doesn't need, close immediately
124
+ # b_out_write - needs to write; close when done
125
+ child = fork do
126
+ puts "child file descriptors:"
127
+ puts "a read: #{a_out_read.fileno}"
128
+ puts "a write: #{a_out_write.fileno}"
129
+ puts "b read: #{b_out_read.fileno}"
130
+ puts "b write: #{b_out_write.fileno}"
131
+ a_out_write.close
132
+ b_out_read.close
133
+
134
+ puts "******* IN FORK **********"
135
+
136
+ # while l = a_out_read.gets
137
+ # puts "working #{l}"
138
+ # b_out_write.puts "child - #{l}"
139
+ # end
140
+
141
+ a_out_read.each_line { |l| puts "working #{l}"; b_out_write.puts "child - #{l}" }
142
+ puts "child done - close writer"
143
+ a_out_read.close
144
+ b_out_write.close
145
+ puts "Fork work done"
146
+ end
147
+
148
+ puts "done forked"
149
+
150
+ a_out_write.close
151
+ b_out_write.close
152
+ a_out_read.close
153
+
154
+ puts "** Display child output:"
155
+ b_out_read.each_line { |l| puts "read from parent - #{l}" }
156
+ b_out_read.close
157
+ puts "done reading"
158
+ end
159
+
160
+ # read_from_spawned_pipe
161
+ # read_write_to_spawned
162
+ # pipe_to_forked_ruby
163
+ # reading_large_output
164
+ # chain_two_native_processes
165
+ # native_process_to_ruby_block
166
+
167
+ def run_fork(stdin, stdout, &block)
168
+ fork do
169
+ STDOUT.reopen(stdout)
170
+ stdin.each_line(&block)
171
+ end
172
+ end
173
+
174
+ def three_step
175
+ writers = []
176
+ banner("three step")
177
+ a_in_read, a_in_write = IO.pipe
178
+ a_out_read, a_out_write = IO.pipe
179
+
180
+ a_unused = [a_out_write]
181
+
182
+ p1 = spawn('echo "a\nb\nc\nab\n"',
183
+ out: a_out_write,
184
+ a_out_write => a_out_write,
185
+ in: a_in_read,
186
+ a_in_read => a_in_read)
187
+
188
+ b_out_read, b_out_write = IO.pipe
189
+ b_unused = [b_out_write]
190
+
191
+ a_unused.each(&:close)
192
+ run_fork(a_out_read, b_out_write) { |l| puts "~~ - #{l}" }
193
+
194
+ c_out_read, c_out_write = IO.pipe
195
+ c_unused = [c_out_write]
196
+
197
+ p2 = spawn('grep a',
198
+ out: c_out_write,
199
+ c_out_write => c_out_write,
200
+ in: b_out_read,
201
+ b_out_read => b_out_read)
202
+ b_unused.each(&:close)
203
+
204
+ # Q: Why do we have to close these? do the spawned processes not close them?
205
+ c_unused.each(&:close)
206
+ puts c_out_read.read
207
+ end
208
+
209
+ # three_step
210
+
211
+ def stdin_redirect
212
+ stdin = File.open("/usr/share/dict/words", "r")
213
+ spawn("head", in: stdin, stdin => stdin)
214
+ end
215
+ stdin_redirect
data/lib/coque/cmd.rb ADDED
@@ -0,0 +1,35 @@
1
+ module Coque
2
+ class Cmd
3
+ include Redirectable
4
+ attr_reader :context
5
+
6
+ def |(other)
7
+ verify_redirectable(other)
8
+ case other
9
+ when Cmd
10
+ Pipeline.new([self, other])
11
+ when Pipeline
12
+ Pipeline.new([self] + other.commands)
13
+ end
14
+ end
15
+
16
+ def clone
17
+ raise "Not Implemented - Override"
18
+ end
19
+
20
+ def ensure_default_fds
21
+ if self.stdin.nil?
22
+ inr, inw = IO.pipe
23
+ inw.close
24
+ self.stdin = inr
25
+ end
26
+
27
+ if self.stdout.nil?
28
+ outr, outw = IO.pipe
29
+ self.stdout = outw
30
+ # only used for Result if this is the last command in a pipe
31
+ @stdout_read = outr
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ module Coque
2
+ class Context
3
+ attr_reader :dir, :env
4
+ def initialize(dir = Dir.pwd, env = {}, disinherits_env = false)
5
+ @dir = dir
6
+ @env = env
7
+ @disinherits_env = disinherits_env
8
+ end
9
+
10
+ def disinherits_env?
11
+ @disinherits_env
12
+ end
13
+
14
+ def [](*args)
15
+ Sh.new(self, args)
16
+ end
17
+
18
+ def rb(&block)
19
+ Rb.new(self, &block)
20
+ end
21
+
22
+ def chdir(new_dir)
23
+ Context.new(new_dir, env, disinherits_env?)
24
+ end
25
+
26
+ def setenv(opts)
27
+ opts = opts.map { |k,v| [k.to_s, v.to_s] }.to_h
28
+ Context.new(dir, self.env.merge(opts), disinherits_env?)
29
+ end
30
+
31
+ def disinherit_env
32
+ Context.new(dir, {}, true)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ module Coque
2
+ class RedirectionError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,62 @@
1
+ module Coque
2
+ class Pipeline
3
+ include Redirectable
4
+
5
+ attr_reader :commands
6
+ def initialize(commands = [])
7
+ @commands = commands
8
+ end
9
+
10
+ def clone
11
+ self.class.new(commands)
12
+ end
13
+
14
+ def to_s
15
+ "<Pipeline #{commands.join(" | ")} >"
16
+ end
17
+
18
+ def |(other)
19
+ verify_redirectable(other)
20
+ case other
21
+ when Pipeline
22
+ Pipeline.new(commands + other.commands)
23
+ when Cmd
24
+ Pipeline.new(commands + [other])
25
+ end
26
+ end
27
+
28
+ def stitch
29
+ # Set head in
30
+ if commands.first.stdin.nil?
31
+ start_r, start_w = IO.pipe
32
+ start_w.close
33
+ commands.first.stdin = start_r
34
+ end
35
+
36
+ # Connect intermediate in/outs
37
+ commands.each_cons(2) do |left, right|
38
+ read, write = IO.pipe
39
+ left.stdout = write
40
+ right.stdin = read
41
+ end
42
+
43
+ # Set tail out
44
+ if self.stdout
45
+ commands.last.stdout = stdout
46
+ stdout
47
+ elsif commands.last.stdout
48
+ commands.last.stdout
49
+ else
50
+ next_r, next_w = IO.pipe
51
+ commands.last.stdout = next_w
52
+ next_r
53
+ end
54
+ end
55
+
56
+ def run
57
+ stdout = stitch
58
+ results = commands.map(&:run)
59
+ Result.new(results.last.pid, stdout)
60
+ end
61
+ end
62
+ end
data/lib/coque/rb.rb ADDED
@@ -0,0 +1,54 @@
1
+ module Coque
2
+ class Rb < Cmd
3
+ NOOP = Proc.new { }
4
+ attr_reader :block, :pre_block, :post_block
5
+ def initialize(context = Context.new, &block)
6
+ if block_given?
7
+ @block = block
8
+ else
9
+ @block = NOOP
10
+ end
11
+ @pre_block = nil
12
+ @post_block = nil
13
+ @context = context
14
+ end
15
+
16
+ def clone
17
+ self.class.new(context, &block).pre(&pre_block).post(&post_block)
18
+ end
19
+
20
+ def pre(&block)
21
+ if block_given?
22
+ @pre_block = block
23
+ end
24
+ self
25
+ end
26
+
27
+ def post(&block)
28
+ if block_given?
29
+ @post_block = block
30
+ end
31
+ self
32
+ end
33
+
34
+ def run
35
+ ensure_default_fds
36
+
37
+ pid = fork do
38
+ STDOUT.reopen(stdout)
39
+ Dir.chdir(context.dir)
40
+ if context.disinherits_env?
41
+ ENV.clear
42
+ end
43
+ context.env.each do |k,v|
44
+ ENV[k] = v
45
+ end
46
+ @pre_block.call if @pre_block
47
+ stdin.each_line(&@block)
48
+ @post_block.call if @post_block
49
+ end
50
+ stdout.close
51
+ Result.new(pid, stdout_read)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,96 @@
1
+ require "pathname"
2
+
3
+ module Coque
4
+ module Redirectable
5
+ attr_reader :stdin, :stdout, :stderr
6
+
7
+ def >(io)
8
+ clone.tap do |c|
9
+ c.stdout = io
10
+ end
11
+ end
12
+
13
+ def <(io)
14
+ clone.tap do |c|
15
+ c.stdin = io
16
+ end
17
+ end
18
+
19
+ def >=(io)
20
+ clone.tap do |c|
21
+ c.stderr = io
22
+ end
23
+ end
24
+
25
+ def getio(io, mode = "r")
26
+ case io
27
+ when String
28
+ File.open(io, mode)
29
+ when Pathname
30
+ File.open(io, mode)
31
+ when IO
32
+ io
33
+ when Tempfile
34
+ io
35
+ else
36
+ raise ArgumentError.new("Can't redirect stream to #{io}, must be String, Pathname, or IO")
37
+ end
38
+ end
39
+
40
+ def stdin_redirected?
41
+ defined? @stdin
42
+ end
43
+
44
+ def stdout_redirected?
45
+ defined? @stdout
46
+ end
47
+
48
+ def stderr_redirected?
49
+ defined? @stderr
50
+ end
51
+
52
+ def stderr=(s)
53
+ if stderr_redirected?
54
+ raise RedirectionError.new("Can't set stderr of #{self} to #{s}, is already set to #{stderr}")
55
+ else
56
+ @stderr = getio(s, "w")
57
+ end
58
+ end
59
+
60
+ def stdout=(s)
61
+ if stdout_redirected?
62
+ raise RedirectionError.new("Can't set stdout of #{self} to #{s}, is already set to #{stdout}")
63
+ else
64
+ @stdout = getio(s, "w")
65
+ end
66
+ end
67
+
68
+ def stdin=(s)
69
+ if stdin_redirected?
70
+ raise RedirectionError.new("Can't set stdin of #{self} to #{s}, is already set to #{stdin}")
71
+ else
72
+ @stdin = getio(s, "r")
73
+ end
74
+ end
75
+
76
+ def verify_redirectable(other)
77
+ if self.stdout_redirected?
78
+ raise RedirectionError.new("Can't pipe #{self} into #{other} -- #{self}'s STDIN is already redirected")
79
+ end
80
+
81
+ if other.stdin_redirected?
82
+ raise RedirectionError.new("Can't pipe #{self} into #{other} -- #{other}'s STDIN is already redirected")
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def stdout_read
89
+ if defined? @stdout_read
90
+ @stdout_read
91
+ else
92
+ nil
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,24 @@
1
+ class Coque::Result
2
+ attr_reader :pid, :exit_code
3
+ include Enumerable
4
+
5
+ def initialize(pid, out)
6
+ @pid = pid
7
+ @out = out
8
+ end
9
+
10
+ def each(&block)
11
+ @out.each_line do |line|
12
+ block.call(line.chomp)
13
+ end
14
+ unless defined? @exit_code
15
+ wait
16
+ end
17
+ end
18
+
19
+ def wait
20
+ _, status = Process.waitpid2(pid)
21
+ @exit_code = status.exitstatus
22
+ self
23
+ end
24
+ end
data/lib/coque/sh.rb ADDED
@@ -0,0 +1,45 @@
1
+ module Coque
2
+ class Sh < Cmd
3
+ attr_reader :args, :context
4
+ def initialize(context, args)
5
+ @context = context
6
+ @args = args
7
+ end
8
+
9
+ def clone
10
+ self.class.new(context, args)
11
+ end
12
+
13
+ def to_s
14
+ "<Coque::Sh #{args.inspect}>"
15
+ end
16
+
17
+ def inspect
18
+ to_s
19
+ end
20
+
21
+ def [](*new_args)
22
+ self.class.new(self.context, self.args + new_args)
23
+ end
24
+
25
+ def run
26
+ ensure_default_fds
27
+ opts = {in: stdin, stdin.fileno => stdin.fileno,
28
+ out: stdout, stdout.fileno => stdout.fileno,
29
+ chdir: context.dir, unsetenv_others: context.disinherits_env?}
30
+
31
+ # Redirect err to out: (e.g. for 2>&1)
32
+ # {err: [:child, :out]}
33
+ err_opts = if stderr
34
+ {err: stderr, stderr.fileno => stderr.fileno}
35
+ else
36
+ {}
37
+ end
38
+
39
+ pid = spawn(context.env, args.join(" "), opts.merge(err_opts))
40
+
41
+ stdout.close
42
+ Result.new(pid, stdout_read)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module Coque
2
+ VERSION = "0.1.0"
3
+ end
data/lib/coque.rb ADDED
@@ -0,0 +1,27 @@
1
+ require "coque/redirectable"
2
+ require "coque/cmd"
3
+ require "coque/sh"
4
+ require "coque/rb"
5
+ require "coque/context"
6
+ require "coque/errors"
7
+ require "coque/pipeline"
8
+ require "coque/result"
9
+ require "coque/version"
10
+
11
+ module Coque
12
+ def self.context(dir: Dir.pwd, env: {}, disinherits_env: false)
13
+ Context.new(dir, env, disinherits_env)
14
+ end
15
+
16
+ def self.[](*args)
17
+ Context.new[*args]
18
+ end
19
+
20
+ def self.rb(&block)
21
+ Rb.new(Context.new, &block)
22
+ end
23
+
24
+ def self.pipeline(*commands)
25
+ commands.reduce(:|)
26
+ end
27
+ end