coque 0.1.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/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