rubish 0.0.1
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 +24 -0
- data/README.markdown +651 -0
- data/README.textile +651 -0
- data/Rakefile +52 -0
- data/VERSION.yml +4 -0
- data/bin/rubish +5 -0
- data/lib/rubish.rb +24 -0
- data/lib/rubish/awk.rb +54 -0
- data/lib/rubish/batch_executable.rb +27 -0
- data/lib/rubish/command.rb +91 -0
- data/lib/rubish/command_builder.rb +116 -0
- data/lib/rubish/context.rb +75 -0
- data/lib/rubish/executable.rb +251 -0
- data/lib/rubish/job.rb +90 -0
- data/lib/rubish/job_control.rb +62 -0
- data/lib/rubish/pipe.rb +68 -0
- data/lib/rubish/repl.rb +47 -0
- data/lib/rubish/sed.rb +37 -0
- data/lib/rubish/streamer.rb +347 -0
- data/lib/rubish/stub.rb +83 -0
- data/lib/rubish/unix_executable.rb +74 -0
- data/lib/rubish/workspace.rb +211 -0
- data/test/slowcat.rb +5 -0
- data/test/test.rb +896 -0
- data/test/test_dev.rb +50 -0
- metadata +81 -0
@@ -0,0 +1,251 @@
|
|
1
|
+
class Rubish::Executable
|
2
|
+
|
3
|
+
class ExecutableIO
|
4
|
+
class << self
|
5
|
+
# transactionally open a bunch of ios
|
6
|
+
# if one fails to open, close all others.
|
7
|
+
def ios(*specs)
|
8
|
+
success = false
|
9
|
+
begin
|
10
|
+
exe_ios = []
|
11
|
+
specs.each do |(spec,mode)|
|
12
|
+
exe_ios << prepare_io(spec,mode)
|
13
|
+
end
|
14
|
+
success =true
|
15
|
+
return exe_ios
|
16
|
+
ensure
|
17
|
+
unless success
|
18
|
+
exe_ios.each { |exe_io|
|
19
|
+
exe_io.close
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# sorry, this is pretty hairy. This method
|
27
|
+
# instructs how exec should handle IO. (whether
|
28
|
+
# IO is done in a thread. whether it needs to be
|
29
|
+
# closed. (so on))
|
30
|
+
#
|
31
|
+
# an io could be
|
32
|
+
# String: interpreted as file name
|
33
|
+
# Number: file descriptor
|
34
|
+
# IO: Ruby IO object
|
35
|
+
# Block: executed in a thread, and a pipe connects the executable and the thread.
|
36
|
+
def prepare_io(io,mode)
|
37
|
+
# if the io given is a block, we execute it in a thread (passing it a pipe)
|
38
|
+
raise "invalid io mode: #{mode}" unless mode == "w" || mode == "r"
|
39
|
+
result =
|
40
|
+
case io
|
41
|
+
when $stdin, $stdout, $stderr
|
42
|
+
[io, false, nil]
|
43
|
+
when String
|
44
|
+
path = File.expand_path(io)
|
45
|
+
raise "path is a directory" if File.directory?(path)
|
46
|
+
[File.new(path,mode), true, nil]
|
47
|
+
when IO
|
48
|
+
[io, false,nil]
|
49
|
+
when Proc
|
50
|
+
proc = io
|
51
|
+
r,w = IO.pipe
|
52
|
+
# if we want to use a block to
|
53
|
+
# (1) input into executable
|
54
|
+
# - return "r" end from prepare_io, and
|
55
|
+
# the executable use this and standard
|
56
|
+
# input.
|
57
|
+
# - let the thread block writes to the "w" end
|
58
|
+
# (2) read from executable
|
59
|
+
# - return "w" from prepare_io
|
60
|
+
# - let thread reads from "r"
|
61
|
+
return_io, thread_io =
|
62
|
+
case mode
|
63
|
+
# case 1
|
64
|
+
when "r"
|
65
|
+
[r,w]
|
66
|
+
when "w"
|
67
|
+
# case 2
|
68
|
+
[w,r]
|
69
|
+
end
|
70
|
+
thread = Thread.new do
|
71
|
+
begin
|
72
|
+
proc.call(thread_io)
|
73
|
+
ensure
|
74
|
+
thread_io.close
|
75
|
+
end
|
76
|
+
end
|
77
|
+
[return_io, true, thread]
|
78
|
+
else
|
79
|
+
raise "not a valid input: #{io}"
|
80
|
+
end
|
81
|
+
return self.new(*result)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_reader :thread
|
87
|
+
attr_reader :io
|
88
|
+
attr_reader :auto_close
|
89
|
+
|
90
|
+
def initialize(io,auto_close,thread)
|
91
|
+
@io = io
|
92
|
+
@auto_close = auto_close
|
93
|
+
@thread = thread
|
94
|
+
end
|
95
|
+
|
96
|
+
def close
|
97
|
+
if auto_close
|
98
|
+
io.close
|
99
|
+
else
|
100
|
+
#io.flush if io.stat.writable?
|
101
|
+
#io.flush rescue true # try flushing
|
102
|
+
end
|
103
|
+
if thread
|
104
|
+
begin
|
105
|
+
thread.join
|
106
|
+
ensure
|
107
|
+
if thread.alive?
|
108
|
+
thread.kill
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
attr_reader :working_directory
|
118
|
+
|
119
|
+
# only changes working directory for this executable
|
120
|
+
def cd(dir)
|
121
|
+
@working_directory = dir
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
def awk(a=nil,b=nil,&block)
|
126
|
+
if block
|
127
|
+
Rubish::Awk.new(self).act(a,b,&block)
|
128
|
+
else
|
129
|
+
Rubish::Awk.new(self)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def sed(a=nil,b=nil,&block)
|
134
|
+
if block
|
135
|
+
Rubish::Sed.new(self).act(a,b,&block)
|
136
|
+
else
|
137
|
+
Rubish::Sed.new(self)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def exec
|
142
|
+
raise "abstract"
|
143
|
+
end
|
144
|
+
|
145
|
+
def exec!
|
146
|
+
raise "abstract"
|
147
|
+
end
|
148
|
+
|
149
|
+
# methods for io redirection
|
150
|
+
def i(io=nil,&block)
|
151
|
+
return @__io_in unless io || block
|
152
|
+
@__io_in = io || block
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
def o(io=nil,&block)
|
157
|
+
return @__io_out unless io || block
|
158
|
+
@__io_out = io || block
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
def err(io=nil,&block)
|
163
|
+
return @__io_err unless io || block
|
164
|
+
@__io_err = io || block
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def io(i=nil,o=nil)
|
169
|
+
i(i); o(o)
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
def i=(io)
|
174
|
+
@__io_in = io
|
175
|
+
end
|
176
|
+
|
177
|
+
def o=(io)
|
178
|
+
@__io_out = io
|
179
|
+
end
|
180
|
+
|
181
|
+
def err=(io)
|
182
|
+
@__io_err = io
|
183
|
+
end
|
184
|
+
|
185
|
+
def each!
|
186
|
+
self.o do |pipe|
|
187
|
+
pipe.each_line do |line|
|
188
|
+
line.chomp!
|
189
|
+
yield(line)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
job = self.exec!
|
193
|
+
return job
|
194
|
+
end
|
195
|
+
|
196
|
+
def each(&block)
|
197
|
+
job = self.each! &block
|
198
|
+
job.wait
|
199
|
+
end
|
200
|
+
|
201
|
+
# acc is the accumulator passed in by reference, and updated destructively.
|
202
|
+
def map!(acc,&block)
|
203
|
+
acc = Array.new unless acc
|
204
|
+
job = self.each! do |l|
|
205
|
+
acc << (block.nil? ? l : block.call(l))
|
206
|
+
end
|
207
|
+
return job
|
208
|
+
end
|
209
|
+
|
210
|
+
def map(&block)
|
211
|
+
acc = []
|
212
|
+
job = self.map!(acc,&block)
|
213
|
+
job.wait
|
214
|
+
return acc
|
215
|
+
end
|
216
|
+
|
217
|
+
def head(n=1,&block)
|
218
|
+
raise "n should be greater than 0: #{n}" unless n > 0
|
219
|
+
self.map do |l|
|
220
|
+
if n == 0
|
221
|
+
break
|
222
|
+
else
|
223
|
+
n -= 1
|
224
|
+
block ? block.call(l) : l
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def tail(n=1,&block)
|
230
|
+
raise "n should be greater than 0: #{n}" unless n > 0
|
231
|
+
acc = []
|
232
|
+
self.each do |l|
|
233
|
+
acc << (block ? block.call(l) : l)
|
234
|
+
if acc.size > n
|
235
|
+
acc.shift
|
236
|
+
end
|
237
|
+
end
|
238
|
+
return acc
|
239
|
+
end
|
240
|
+
|
241
|
+
def first
|
242
|
+
head(1).first
|
243
|
+
end
|
244
|
+
|
245
|
+
def last
|
246
|
+
tail(1).first
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
|
251
|
+
end
|
data/lib/rubish/job.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
# a job is not necessarily registered with job control.
|
3
|
+
class Rubish::Job
|
4
|
+
class Failure < Rubish::Error
|
5
|
+
attr_reader :job, :reason
|
6
|
+
def initialize(job,reason=nil)
|
7
|
+
raise "failure reason should be an Exception" unless reason.is_a?(Exception)
|
8
|
+
@job = job
|
9
|
+
@reason = reason
|
10
|
+
set_backtrace(reason.backtrace)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
@reason.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :result
|
19
|
+
attr_reader :job_control
|
20
|
+
|
21
|
+
# subclass initializer MUST call __start
|
22
|
+
def initialize(*args)
|
23
|
+
raise "abstract"
|
24
|
+
__start
|
25
|
+
end
|
26
|
+
|
27
|
+
def __start
|
28
|
+
@result = nil
|
29
|
+
@done = false
|
30
|
+
# when wait is called, the job control may or
|
31
|
+
# may not be the current job_control.
|
32
|
+
@job_control = Rubish::JobControl.current
|
33
|
+
@job_control.submit(self)
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def __finish
|
38
|
+
@job_control.remove(self)
|
39
|
+
@done = true
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# MUST call __finish in wait
|
44
|
+
# should return Job#result
|
45
|
+
def wait
|
46
|
+
raise "abstract"
|
47
|
+
__finish
|
48
|
+
end
|
49
|
+
|
50
|
+
# MUST result in calling __finish
|
51
|
+
def stop
|
52
|
+
raise "abstract"
|
53
|
+
end
|
54
|
+
|
55
|
+
def done?
|
56
|
+
@done
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
# carry out some computation in a thread.
|
62
|
+
class Rubish::Job::ThreadJob < Rubish::Job
|
63
|
+
attr_reader :thread
|
64
|
+
def initialize(&block)
|
65
|
+
# run block in a thread
|
66
|
+
@thread = ::Thread.new {
|
67
|
+
block.call
|
68
|
+
}
|
69
|
+
__start
|
70
|
+
end
|
71
|
+
|
72
|
+
def wait
|
73
|
+
# wait thread to completeggg64
|
74
|
+
begin
|
75
|
+
@thread.join
|
76
|
+
@result = @thread.value
|
77
|
+
return self.result
|
78
|
+
rescue => e
|
79
|
+
raise Rubish::Job::Failure.new(self,e)
|
80
|
+
ensure
|
81
|
+
__finish
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def stop
|
86
|
+
@thread.kill
|
87
|
+
wait
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
# A job is not necessarily registered with job
|
3
|
+
# control.
|
4
|
+
|
5
|
+
# TODO extend this for bg/fg (maybe?)
|
6
|
+
# Assume that job control is used by a single thread...
|
7
|
+
|
8
|
+
require 'thread'
|
9
|
+
class Rubish::JobControl
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def current
|
13
|
+
Rubish::Context.current.job_control
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@mutex = Mutex.new
|
19
|
+
@jobs = { }
|
20
|
+
end
|
21
|
+
|
22
|
+
def jobs
|
23
|
+
@jobs.values
|
24
|
+
end
|
25
|
+
|
26
|
+
# need to synchronize access to the jobs hash
|
27
|
+
def submit(job)
|
28
|
+
raise "expects a Rubish::JobControl::Job" unless job.is_a?(Rubish::Job)
|
29
|
+
@mutex.synchronize {
|
30
|
+
@jobs[job.object_id] = job
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove(job)
|
35
|
+
raise "expects a Rubish::JobControl::Job" unless job.is_a?(Rubish::Job)
|
36
|
+
raise Rubish::Error.new("Job not found: #{job}") unless @jobs.include?(job.object_id)
|
37
|
+
@mutex.synchronize {
|
38
|
+
@jobs.delete(job.object_id)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait(*jobs)
|
43
|
+
rss = jobs.map do |job|
|
44
|
+
job.wait
|
45
|
+
if block_given?
|
46
|
+
yield(job)
|
47
|
+
else
|
48
|
+
job
|
49
|
+
end
|
50
|
+
end
|
51
|
+
return *rss
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO handle interrupt
|
55
|
+
def waitall(&block)
|
56
|
+
wait(*@jobs.values,&block)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
data/lib/rubish/pipe.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
class Rubish::Pipe < Rubish::UnixExecutable
|
3
|
+
attr_reader :cmds
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def build(workspace=nil,&block)
|
7
|
+
workspace ||= Rubish::Context.current.workspace
|
8
|
+
cmds = []
|
9
|
+
workspace.command_factory_hook = Proc.new { |cmd| cmds << cmd; cmd}
|
10
|
+
workspace.eval &block
|
11
|
+
self.new(cmds)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def initialize(cmds)
|
17
|
+
# dun wanna handle special case for now
|
18
|
+
raise "pipe length less than 2" if cmds.length < 2
|
19
|
+
raise "should build pipe only from Rubish::Command instances" unless cmds.all? { |c| c.is_a?(Rubish::Command)}
|
20
|
+
@cmds = cmds
|
21
|
+
end
|
22
|
+
|
23
|
+
def exec_with(pipe_in,pipe_out,pipe_err)
|
24
|
+
@cmds.each do |cmd|
|
25
|
+
cmd.normalize_args!
|
26
|
+
if cmd.i || cmd.o || cmd.err
|
27
|
+
raise "It's weird to redirect stdioe for command in a pipeline. Don't."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# pipes == [i0,o1,i1,o2,i2...in,o0]
|
31
|
+
# i0 == $stdin
|
32
|
+
# o0 == $stdout
|
33
|
+
pipe = nil # [r, w]
|
34
|
+
pids = []
|
35
|
+
@cmds.each_index do |index|
|
36
|
+
tail = index == (@cmds.length - 1) # tail
|
37
|
+
head = index == 0 # head
|
38
|
+
if head
|
39
|
+
i = pipe_in
|
40
|
+
pipe = IO.pipe
|
41
|
+
o = pipe[1] # w
|
42
|
+
elsif tail
|
43
|
+
i = pipe[0]
|
44
|
+
o = pipe_out
|
45
|
+
else # middle
|
46
|
+
i = pipe[0] # r
|
47
|
+
pipe = IO.pipe
|
48
|
+
o = pipe[1]
|
49
|
+
end
|
50
|
+
|
51
|
+
cmd = @cmds[index]
|
52
|
+
if child = fork # children
|
53
|
+
#parent
|
54
|
+
pids << child
|
55
|
+
# it's important to close the pipes held
|
56
|
+
# by spawning parent, otherwise the pipes
|
57
|
+
# would not close after a program ends.
|
58
|
+
i.close unless head
|
59
|
+
o.close unless tail
|
60
|
+
else
|
61
|
+
# Rubish.set_stdioe((cmd.i || i),(cmd.o || o),(cmd.err || pipe_err))
|
62
|
+
cmd.system_exec(i,o,pipe_err)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
return pids
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|