brush 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/bin/brush +5 -0
- data/lib/brush.rb +46 -0
- data/lib/brush/pipeline.rb +527 -0
- metadata +67 -0
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2009, Michael H. Buselli
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
* Redistributions of source code must retain the above copyright
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
10
|
+
documentation and/or other materials provided with the distribution.
|
11
|
+
|
12
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ''AS IS'' AND ANY
|
13
|
+
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
14
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
15
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
|
16
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
17
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
18
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
19
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
20
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
21
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/bin/brush
ADDED
data/lib/brush.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2009, Michael H. Buselli
|
3
|
+
# See LICENSE for details on permitted use.
|
4
|
+
#
|
5
|
+
|
6
|
+
module Brush
|
7
|
+
class Shell
|
8
|
+
DEFAULT_EXEC_MATCH = %r"^(\S.*)"
|
9
|
+
DEFAULT_RUBY_MATCH = %r"^\s+(.*)"
|
10
|
+
DEFAULT_PS1 = "% "
|
11
|
+
DEFAULT_PS2 = "%-"
|
12
|
+
DEFAULT_RPS1 = "\#=> "
|
13
|
+
DEFAULT_RPS2 = "\# > "
|
14
|
+
|
15
|
+
def initialize (inp = $stdin, out = $stdout, err = $stderr, binding = nil)
|
16
|
+
@in, @out, @err = inp, out, err
|
17
|
+
@exec_match = DEFAULT_EXEC_MATCH
|
18
|
+
@ruby_match = DEFAULT_RUBY_MATCH
|
19
|
+
@ps1, @ps2 = DEFAULT_PS1, DEFAULT_PS2
|
20
|
+
@rps1, @rps2 = DEFAULT_RPS1, DEFAULT_RPS2
|
21
|
+
@binding ||= binding
|
22
|
+
@out.sync = true
|
23
|
+
@err.sync = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
while not @in.eof?
|
28
|
+
@out.print @ps1
|
29
|
+
line = @in.gets.chomp
|
30
|
+
if line =~ @ruby_match
|
31
|
+
repl line[@ruby_match, 1]
|
32
|
+
elsif line =~ @exec_match
|
33
|
+
system line[@exec_match, 1]
|
34
|
+
else
|
35
|
+
repl line
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def repl (code)
|
41
|
+
result = eval(code, @binding)
|
42
|
+
@out.puts "#{@rps1}#{result.inspect}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,527 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2009, Michael H. Buselli
|
3
|
+
# See LICENSE for details on permitted use.
|
4
|
+
#
|
5
|
+
|
6
|
+
module Brush; end
|
7
|
+
|
8
|
+
|
9
|
+
module Brush::Pipeline
|
10
|
+
|
11
|
+
#
|
12
|
+
# Create and execute a pipeline consisting of commands. Each
|
13
|
+
# element of the pipeline is an array of command arguments and
|
14
|
+
# options for that element of the pipeline.
|
15
|
+
#
|
16
|
+
# Options to each pipeline element include:
|
17
|
+
# :executable — specifies an alternative binary to run, instead of
|
18
|
+
# using the value for argv[0].
|
19
|
+
# :cd — change into this directory for this program.
|
20
|
+
# ---- probable future options
|
21
|
+
# :env — pass an alternative set of environment variables to the
|
22
|
+
# process.
|
23
|
+
# :stderr — file, pipe, or buffer to collect error information.
|
24
|
+
# :as_user — Array specifying user and credentials to run as.
|
25
|
+
#
|
26
|
+
# Options to the entire pipeline include:
|
27
|
+
# :stdin — file, pipe, or buffer to feed into the first element of
|
28
|
+
# the pipeline.
|
29
|
+
# :stdout — file, pipe, or buffer to collect the output of the
|
30
|
+
# last element of the pipeline.
|
31
|
+
#
|
32
|
+
# The return value is an Array reporting the success or failure of
|
33
|
+
# each element of the pipeline. Each element of the array is an
|
34
|
+
# Object that emulates a Process::Status object.
|
35
|
+
#
|
36
|
+
# Example:
|
37
|
+
# extracted_files = String.new
|
38
|
+
# Brush::Pipeline.pipeline(
|
39
|
+
# ['gzip', '-cd', 'filename.tar.gz', :cd => 'Downloads'],
|
40
|
+
# ['tar', 'xvf', '-', :cd => 'Extractions'],
|
41
|
+
# :stdout => extracted_files)
|
42
|
+
#
|
43
|
+
def pipeline (*elements)
|
44
|
+
options = {
|
45
|
+
:stdin => $stdin,
|
46
|
+
:stdout => $stdout
|
47
|
+
}
|
48
|
+
|
49
|
+
if elements[-1].respond_to?(:has_key?)
|
50
|
+
options.merge!(elements.pop)
|
51
|
+
end
|
52
|
+
|
53
|
+
if elements.size == 0
|
54
|
+
raise "invalid use of pipeline: no commands given"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Don't modify the originals, and make sure we have an options hash
|
58
|
+
# for each element.
|
59
|
+
elements = elements.collect do |argv|
|
60
|
+
argv = argv.dup
|
61
|
+
argv.push(Hash.new) if not argv[-1].respond_to?(:has_key?)
|
62
|
+
argv
|
63
|
+
end
|
64
|
+
|
65
|
+
# Feed the input and the output
|
66
|
+
elements[0][-1][:stdin] = options[:stdin]
|
67
|
+
elements[-1][-1][:stdout] = options[:stdout]
|
68
|
+
|
69
|
+
# Build up the structure for the call to #sys.
|
70
|
+
elements.each_with_index do |argv, index|
|
71
|
+
argv[-1][:stdout] = elements[index + 1] if index < elements.size - 1
|
72
|
+
argv[-1][:stderr] = options[:stderr] if options.has_key?(:stderr)
|
73
|
+
end
|
74
|
+
|
75
|
+
sys(*elements[0])
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
PARENT_PIPES = {}
|
80
|
+
|
81
|
+
SysInfo = Struct.new(:process_infos, :threads)
|
82
|
+
|
83
|
+
#
|
84
|
+
# Options to each pipeline element include:
|
85
|
+
# :stdin — file, pipe, buffer, or :console to feed into the first
|
86
|
+
# element of the pipeline.
|
87
|
+
# :stdout — file, pipe, or buffer to collect the output of the
|
88
|
+
# last element of the pipeline.
|
89
|
+
# :stderr — file, pipe, or buffer to collect error information.
|
90
|
+
# :executable — specifies an alternative binary to run, instead of
|
91
|
+
# using the value for argv[0].
|
92
|
+
# :cd — change into this directory for this program.
|
93
|
+
# :close — File or IO.fileno values to close after fork() or set
|
94
|
+
# un-inheritable prior to calling ProcessCreate().
|
95
|
+
# ---- probable future options
|
96
|
+
# :keep — File or IO.fileno values to keep open in child.
|
97
|
+
# :timeout — terminate after a given time.
|
98
|
+
# :env — pass an alternative set of environment variables.
|
99
|
+
# :as_user — Array specifying user and credentials to run as.
|
100
|
+
# process.
|
101
|
+
#
|
102
|
+
# Returns an array of arrays:
|
103
|
+
# [process objects, threads, pipes]
|
104
|
+
#
|
105
|
+
# Process objects contain the pid and any relevent process and thread
|
106
|
+
# handles. Threads returned need to be joined to guarentee their
|
107
|
+
# input or output is completely processed after the program
|
108
|
+
# terminates. Pipes returned need to be closed after the program
|
109
|
+
# terminates.
|
110
|
+
#
|
111
|
+
def sys_start (*argv)
|
112
|
+
options = {
|
113
|
+
:stdin => $stdin,
|
114
|
+
:stdout => $stdout,
|
115
|
+
:stderr => $stderr,
|
116
|
+
:executable => argv[0],
|
117
|
+
:cd => '.',
|
118
|
+
:close => []
|
119
|
+
}
|
120
|
+
|
121
|
+
if argv[-1].respond_to?(:has_key?)
|
122
|
+
options.merge!(argv.pop)
|
123
|
+
end
|
124
|
+
|
125
|
+
options[:executable] = find_in_path(options[:executable])
|
126
|
+
|
127
|
+
original_stdfiles = [:stdout, :stderr].collect do |io_sym|
|
128
|
+
options[io_sym]
|
129
|
+
end
|
130
|
+
|
131
|
+
upper_child_pipes = [] # pipes for children up the pipeline
|
132
|
+
lower_child_pipes = [] # pipes for children down the pipeline
|
133
|
+
threads = [] # threads handling special needs I/O
|
134
|
+
process_infos = [] # info for children down the pipeline
|
135
|
+
|
136
|
+
[:stdin, :stdout, :stderr].each do |io_sym|
|
137
|
+
pior = process_io(io_sym, options[io_sym], original_stdfiles,
|
138
|
+
upper_child_pipes + lower_child_pipes + options[:close])
|
139
|
+
options[io_sym] = pior.io
|
140
|
+
|
141
|
+
upper_child_pipes << pior.io if pior.threads
|
142
|
+
lower_child_pipes << pior.pipe if pior.pipe
|
143
|
+
threads.push *pior.threads if pior.threads
|
144
|
+
process_infos.push *pior.process_infos if pior.process_infos
|
145
|
+
end
|
146
|
+
|
147
|
+
process_infos.unshift(
|
148
|
+
create_process(argv, options, lower_child_pipes + options[:close]))
|
149
|
+
upper_child_pipes.each { |io| io.close }
|
150
|
+
lower_child_pipes.each { |io| io.close }
|
151
|
+
SysInfo.new(process_infos, threads)
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
def sys (*argv)
|
156
|
+
sysinfo = sys_start(*argv)
|
157
|
+
overall_result = nil
|
158
|
+
|
159
|
+
results = sysinfo.process_infos.collect do |pi|
|
160
|
+
status = sys_wait(pi)
|
161
|
+
overall_result = status if overall_result.nil? and not status.success?
|
162
|
+
status
|
163
|
+
end
|
164
|
+
|
165
|
+
sysinfo.threads.each { |t| t.join }
|
166
|
+
overall_result = results[-1] if overall_result.nil?
|
167
|
+
duck_type_status_object(results, overall_result)
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
ProcessIOResult = Struct.new(:io, :pipe, :threads, :process_infos)
|
172
|
+
|
173
|
+
# File or IO, String (empty), String (filename), String (data),
|
174
|
+
# StringIO, Integer, :stdout, :stderr, :null or nil, :zero, Symbol (other),
|
175
|
+
# Array (new command)
|
176
|
+
# ---- supported:
|
177
|
+
# File or IO,
|
178
|
+
# StringIO, Integer, :stdout, :stderr, :null or nil, :zero,
|
179
|
+
# Array (new command)
|
180
|
+
# ---- future:
|
181
|
+
# String (empty), String (filename), String (data),
|
182
|
+
# Symbol (other),
|
183
|
+
#
|
184
|
+
# Returns an array of stuff:
|
185
|
+
# [IO object, thread or threads, pipe or pipes, process info objects]
|
186
|
+
#
|
187
|
+
# The IO object is the processed IO object based on the input IO
|
188
|
+
# object (+taret+), which may not have actually been an IO object.
|
189
|
+
# Threads returned, if any, need to be joined after the process
|
190
|
+
# terminates. Pipes returned, if any, need to be closed after the
|
191
|
+
# process terminates. Process info objects, if any, refer to other
|
192
|
+
# processes running in the pipeline that this call to process_io
|
193
|
+
# created.
|
194
|
+
#
|
195
|
+
def process_io (io_sym, target, original_stdfiles, close_pipes)
|
196
|
+
|
197
|
+
# Firstly, any File or IO value can be returned as it is.
|
198
|
+
# We will duck-type recognize File and IO objects if they respond
|
199
|
+
# to :fileno and the result of calling #fileno is not nil.
|
200
|
+
if target.respond_to?(:fileno) and not target.fileno.nil?
|
201
|
+
return ProcessIOResult.new(target)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Integer (Fixnum in particular) arguments represent file
|
205
|
+
# descriptors to attach this IO to.
|
206
|
+
return ProcessIOResult.new(IO.new(target)) if target.is_a? Fixnum
|
207
|
+
|
208
|
+
# Handle special symbol values for :stdin. Valid symbols are
|
209
|
+
# :null and :zero. Using :null is the same as +nil+ (no input),
|
210
|
+
# and using :zero is like sending an endless stream of null
|
211
|
+
# characters to the process.
|
212
|
+
if io_sym == :stdin
|
213
|
+
if target.nil? or target == :null
|
214
|
+
return input_pipe {}
|
215
|
+
elsif target == :zero
|
216
|
+
return input_pipe { |w| w.syswrite("\x00" * 1024) while true }
|
217
|
+
elsif target.respond_to?(:sysread) # "fake" IO and StringIO
|
218
|
+
return input_pipe do |w|
|
219
|
+
data = nil; w.syswrite(data) while data = target.sysread(1024)
|
220
|
+
end
|
221
|
+
else
|
222
|
+
raise "Invalid input object in Brush#sys"
|
223
|
+
end
|
224
|
+
|
225
|
+
# Handle special symbol values for :stdout and :stderr. Valid
|
226
|
+
# symbols are :null, :zero, :stdout, and :stderr. The symbols
|
227
|
+
# :null and :zero mean the output is thrown away. :stdout means
|
228
|
+
# this output goes where standard output should go and :stderr
|
229
|
+
# means this output goes where standard error should go.
|
230
|
+
else # io_sym is :stdout or :stderr
|
231
|
+
if target.nil? or target == :null or target == :zero
|
232
|
+
return output_pipe { |r| r.sysread(1024) while true }
|
233
|
+
elsif target == :stdout
|
234
|
+
return process_io(io_sym, original_stdfiles[0], nil, close_pipes)
|
235
|
+
elsif target == :stderr
|
236
|
+
return process_io(io_sym, original_stdfiles[1], nil, close_pipes)
|
237
|
+
elsif target.respond_to?(:syswrite) # "fake" IO and StringIO
|
238
|
+
return output_pipe do |r|
|
239
|
+
data = nil; target.syswrite(data) while data = r.sysread(1024)
|
240
|
+
end
|
241
|
+
elsif target.is_a?(Array) # pipeline
|
242
|
+
return child_pipe do |r, w|
|
243
|
+
argv = target.dup
|
244
|
+
argv.push(Hash.new) if not argv[-1].respond_to?(:has_key?)
|
245
|
+
argv[-1].merge!(:stdin => r, :close => [w] + close_pipes)
|
246
|
+
sys_start(*argv)
|
247
|
+
end
|
248
|
+
else
|
249
|
+
raise "Invalid output object in Brush#sys"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
def generic_pipe (p_pipe, ch_pipe)
|
256
|
+
mark_parent_pipe(p_pipe)
|
257
|
+
t = Thread.new do
|
258
|
+
begin
|
259
|
+
yield p_pipe
|
260
|
+
rescue Exception
|
261
|
+
ensure
|
262
|
+
p_pipe.close
|
263
|
+
end
|
264
|
+
end
|
265
|
+
ProcessIOResult.new(ch_pipe, nil, [t])
|
266
|
+
end
|
267
|
+
|
268
|
+
def mark_parent_pipe (pipe)
|
269
|
+
class << pipe
|
270
|
+
def close
|
271
|
+
super
|
272
|
+
ensure
|
273
|
+
Brush::Pipeline::PARENT_PIPES.delete(self)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
Brush::Pipeline::PARENT_PIPES[pipe] = true
|
278
|
+
end
|
279
|
+
|
280
|
+
def input_pipe (&block)
|
281
|
+
generic_pipe *IO.pipe.reverse, &block
|
282
|
+
end
|
283
|
+
|
284
|
+
def output_pipe (&block)
|
285
|
+
generic_pipe *IO.pipe, &block
|
286
|
+
end
|
287
|
+
|
288
|
+
def child_pipe
|
289
|
+
r, w = *IO.pipe
|
290
|
+
sysinfo = yield r, w
|
291
|
+
ProcessIOResult.new(w, r, sysinfo.threads, sysinfo.process_infos)
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
def each_path_element
|
296
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir|
|
297
|
+
yield File.expand_path(dir)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
def duck_type_status_object (object, status_or_pid, status_integer = nil)
|
303
|
+
if status_integer.nil? and status_or_pid.respond_to?(:success?)
|
304
|
+
class << object
|
305
|
+
# Act like the Process::Status @status.
|
306
|
+
(Process::Status.instance_methods - self.instance_methods).each do |m|
|
307
|
+
eval("def #{m} (*args); @status.#{m}(*args); end")
|
308
|
+
end
|
309
|
+
end
|
310
|
+
object.instance_variable_set(:@status, status_or_pid)
|
311
|
+
|
312
|
+
else
|
313
|
+
class << object
|
314
|
+
attr_reader :to_i, :pid
|
315
|
+
|
316
|
+
# We have no idea if we exited normally, coredumped, etc. Just
|
317
|
+
# pretend it's normal.
|
318
|
+
def coredump?; false; end
|
319
|
+
def exited?; true; end
|
320
|
+
def signaled?; false; end
|
321
|
+
def stopped?; false; end
|
322
|
+
def stopsig; nil; end
|
323
|
+
def success?; @to_i.zero?; end
|
324
|
+
def termsig; nil; end
|
325
|
+
alias exitstatus to_i
|
326
|
+
alias to_int to_i
|
327
|
+
|
328
|
+
# Shore up the other methods by acting on the Fixnum in @to_i.
|
329
|
+
(Process::Status.instance_methods - self.instance_methods).each do |m|
|
330
|
+
eval("def #{m} (*args); @to_i.#{m}(*args); end")
|
331
|
+
end
|
332
|
+
end
|
333
|
+
object.instance_variable_set(:@to_i, status_integer)
|
334
|
+
object.instance_variable_set(:@pid, status_or_pid)
|
335
|
+
end
|
336
|
+
|
337
|
+
object
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
module Brush::Pipeline::POSIX
|
343
|
+
ProcessInfo = Struct.new(:process_id)
|
344
|
+
|
345
|
+
def sys_wait (process_info)
|
346
|
+
#system("ls -l /proc/#{$$}/fd /proc/#{process_info.process_id}/fd")
|
347
|
+
Process.waitpid2(process_info.process_id)[1]
|
348
|
+
end
|
349
|
+
|
350
|
+
def create_process (argv, options, close_pipes)
|
351
|
+
|
352
|
+
# The following is used for manual specification testing to verify
|
353
|
+
# that pipes are correctly closed after fork. This is extremely
|
354
|
+
# difficult to write an RSpec test for, and it is only possible on
|
355
|
+
# platforms that have a /proc filesystem anyway. Regardless, this
|
356
|
+
# will be moved into an RSpec test at some point.
|
357
|
+
#
|
358
|
+
#$stderr.puts "===== P #{$$}: #{ %x"ls -l /proc/#{$$}/fd" }"
|
359
|
+
|
360
|
+
pid = fork do # child process
|
361
|
+
[:stdin, :stdout, :stderr].each do |io_sym|
|
362
|
+
io = options[io_sym]
|
363
|
+
if io != Kernel.const_get(io_sym.to_s.upcase)
|
364
|
+
Kernel.const_get(io_sym.to_s.upcase).reopen(io)
|
365
|
+
io.close
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
close_pipes.each { |io| io.close }
|
370
|
+
Brush::Pipeline::PARENT_PIPES.each_key { |io| io.close }
|
371
|
+
|
372
|
+
# This is the second half of the manual specification testing
|
373
|
+
# started above. See comment above for more information.
|
374
|
+
#
|
375
|
+
#$stderr.puts "===== C #{$$}: #{ %x"ls -l /proc/#{$$}/fd" }"
|
376
|
+
|
377
|
+
Dir.chdir(options[:cd]) do
|
378
|
+
exec [options[:executable], argv[0]], *argv[1..-1]
|
379
|
+
end
|
380
|
+
|
381
|
+
raise Error, "failed to exec"
|
382
|
+
end
|
383
|
+
|
384
|
+
ProcessInfo.new(pid)
|
385
|
+
end
|
386
|
+
|
387
|
+
def find_in_path (name)
|
388
|
+
if name.index(File::SEPARATOR) # Path is absolute or relative.
|
389
|
+
return File.expand_path(name)
|
390
|
+
else
|
391
|
+
each_path_element do |dir|
|
392
|
+
chkname = nil
|
393
|
+
return chkname if File.exists?(chkname = File.join(dir, name))
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
nil # Didn't find a match. :(
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
|
402
|
+
module Brush::Pipeline::Win32
|
403
|
+
|
404
|
+
def sys_wait (process_info)
|
405
|
+
numeric_status = Process.waitpid2(process_info.process_id)[1]
|
406
|
+
Process.CloseHandle(process_info.process_handle)
|
407
|
+
Process.CloseHandle(process_info.thread_handle)
|
408
|
+
duck_type_status_object(Object.new, process_info.process_id, numeric_status)
|
409
|
+
end
|
410
|
+
|
411
|
+
|
412
|
+
def make_handle_inheritable (io, inheritable = true)
|
413
|
+
if not SetHandleInformation(
|
414
|
+
get_osfhandle(io.fileno), Windows::Handle::HANDLE_FLAG_INHERIT,
|
415
|
+
inheritable ? Windows::Handle::HANDLE_FLAG_INHERIT : 0) \
|
416
|
+
then
|
417
|
+
raise Error, get_last_error
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
|
422
|
+
def win_quote (argv)
|
423
|
+
argv.collect { |arg| win_quote_word(arg) }.join(' ')
|
424
|
+
end
|
425
|
+
|
426
|
+
def win_quote_word (arg)
|
427
|
+
# Rules
|
428
|
+
# (1) Surround in d-quotes if arg contains space, tab, d-quote, or is empty.
|
429
|
+
# (2) Backslashes preceding d-quotes are doubled.
|
430
|
+
# (3) d-quotes are escaped with a backslash (so d-quotes always are preceded
|
431
|
+
# by an odd number of backslashes).
|
432
|
+
# (4) Backslashes at the end of the string, if escaped in d-quotes, are
|
433
|
+
# doubled (creating the situation where a d-quote—termination character—is
|
434
|
+
# preceded by an even number of backslashes).
|
435
|
+
|
436
|
+
if arg.empty?
|
437
|
+
'""'
|
438
|
+
elsif arg =~ /[ \t"]/
|
439
|
+
['"',
|
440
|
+
arg.gsub(/(\\)*("|\Z)/) { $1.to_s * 2 + ($2 == '"' ? '\\"' : '') },
|
441
|
+
'"'].join
|
442
|
+
else
|
443
|
+
arg
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
|
448
|
+
def create_process (argv, options, close_pipes)
|
449
|
+
close_pipes.each do |io|
|
450
|
+
make_handle_inheritable(io, false)
|
451
|
+
end
|
452
|
+
|
453
|
+
Brush::Pipeline::PARENT_PIPES.each_key do |io|
|
454
|
+
make_handle_inheritable(io, false)
|
455
|
+
end
|
456
|
+
|
457
|
+
[:stdin, :stdout, :stderr].each do |io_sym|
|
458
|
+
make_handle_inheritable(options[io_sym]) if options[io_sym]
|
459
|
+
end
|
460
|
+
|
461
|
+
Dir.chdir(options[:cd]) do
|
462
|
+
return Process.create(
|
463
|
+
:app_name => options[:executable],
|
464
|
+
:command_line => win_quote(argv),
|
465
|
+
:inherit => true,
|
466
|
+
:close_handles => false,
|
467
|
+
:startup_info => {
|
468
|
+
:startf_flags => Process::STARTF_USESTDHANDLES,
|
469
|
+
:stdin => options[:stdin],
|
470
|
+
:stdout => options[:stdout],
|
471
|
+
:stderr => options[:stderr]
|
472
|
+
})
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
|
477
|
+
def find_in_path (name)
|
478
|
+
chkname = nil
|
479
|
+
|
480
|
+
if name =~ %r"[:/\\]" # Path is absolute or relative.
|
481
|
+
basename = File.basename(name)
|
482
|
+
fullname = File.expand_path(name)
|
483
|
+
|
484
|
+
if basename =~ /\./ # Path comes with extension.
|
485
|
+
return fullname
|
486
|
+
else # Need to find extension.
|
487
|
+
each_pathext_element do |ext|
|
488
|
+
return chkname if File.exists?(chkname = [fullname, ext].join)
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
elsif name =~ /\./ # Given extension.
|
493
|
+
each_path_element do |dir|
|
494
|
+
return chkname if File.exists?(chkname = File.join(dir, name))
|
495
|
+
end
|
496
|
+
|
497
|
+
else # Just a name—no path or extension.
|
498
|
+
each_path_element do |dir|
|
499
|
+
each_pathext_element do |ext|
|
500
|
+
if File.exists?(chkname = File.join(dir, [name, ext].join))
|
501
|
+
return chkname
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
nil # Didn't find a match. :(
|
508
|
+
end
|
509
|
+
|
510
|
+
|
511
|
+
def each_pathext_element
|
512
|
+
ENV['PATHEXT'].split(File::PATH_SEPARATOR).each { |ext| yield ext }
|
513
|
+
end
|
514
|
+
|
515
|
+
end
|
516
|
+
|
517
|
+
|
518
|
+
module Brush::Pipeline
|
519
|
+
if RUBY_PLATFORM =~ /-(mswin|mingw)/
|
520
|
+
gem 'win32-process', '>= 0.6.1'
|
521
|
+
require 'win32/process'
|
522
|
+
include Windows::Handle
|
523
|
+
include Brush::Pipeline::Win32
|
524
|
+
else
|
525
|
+
include Brush::Pipeline::POSIX
|
526
|
+
end
|
527
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: brush
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Michael H Buselli
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-09-11 00:00:00 -05:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: " Brush is intended to be an interactive shell with the power of\n Ruby. As it is in its infancy, it is very basic and much of the\n functionality is implemented by scaffolding that will later be\n replaced. For instance, presently commands are passed off to\n another shell for execution, but eventually all globing, pipe\n setup, forking, and execing will be handled directly by Brush.\n"
|
22
|
+
email:
|
23
|
+
- cosine@cosine.org
|
24
|
+
- michael@buselli.com
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- LICENSE
|
33
|
+
- bin/brush
|
34
|
+
- lib/brush.rb
|
35
|
+
- lib/brush/pipeline.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://cosine.org/ruby/brush/
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
segments:
|
50
|
+
- 0
|
51
|
+
version: "0"
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project: brush
|
62
|
+
rubygems_version: 1.3.6
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: "Brush \xE2\x80\x94 the Bourne RUby SHell"
|
66
|
+
test_files: []
|
67
|
+
|