brush 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +21 -0
  2. data/bin/brush +5 -0
  3. data/lib/brush.rb +46 -0
  4. data/lib/brush/pipeline.rb +527 -0
  5. 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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'brush'
4
+
5
+ Brush::Shell.new.start
@@ -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
+