command-set 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/README +2 -0
- data/doc/Specifications +219 -0
- data/doc/argumentDSL +36 -0
- data/lib/command-set/arguments.rb +547 -0
- data/lib/command-set/batch-interpreter.rb +0 -0
- data/lib/command-set/command-set.rb +282 -0
- data/lib/command-set/command.rb +456 -0
- data/lib/command-set/dsl.rb +526 -0
- data/lib/command-set/interpreter.rb +196 -0
- data/lib/command-set/og.rb +615 -0
- data/lib/command-set/quick-interpreter.rb +91 -0
- data/lib/command-set/result-list.rb +300 -0
- data/lib/command-set/results.rb +754 -0
- data/lib/command-set/standard-commands.rb +243 -0
- data/lib/command-set/subject.rb +91 -0
- data/lib/command-set/text-interpreter.rb +171 -0
- data/lib/command-set.rb +3 -0
- metadata +70 -0
@@ -0,0 +1,526 @@
|
|
1
|
+
module Command
|
2
|
+
=begin rdoc
|
3
|
+
This module collects the domain specific languages for CommandSet.
|
4
|
+
This is the first and best place to start if you want to try to understand
|
5
|
+
how to make use of CommandSet.
|
6
|
+
|
7
|
+
The sub-modules here are (in rough "nesting" order):
|
8
|
+
|
9
|
+
CommandSetDefinition:: the commands with a CommandSet::define_commands block
|
10
|
+
CommandDefinition:: the commands with a command setup block that describe how a command functions
|
11
|
+
Argument:: the chained descriptors that describe how arguments are interpreted by command definitions
|
12
|
+
Action:: utility functions within the command action block
|
13
|
+
=end
|
14
|
+
module DSL
|
15
|
+
#These are the commands available within the CommandSet::define_commands
|
16
|
+
#block.
|
17
|
+
module CommandSetDefinition
|
18
|
+
#Allows other command sets to be composited into this one. And
|
19
|
+
#optional list of command names will cherry-pick the commands of the
|
20
|
+
#other set, otherwise they're all folded in, with preference given to
|
21
|
+
#the new commands.
|
22
|
+
def include_commands(set, *commands)
|
23
|
+
new_commands = set.command_list
|
24
|
+
|
25
|
+
commands.map!{|c| c.to_s}
|
26
|
+
unless commands.empty?
|
27
|
+
new_commands.delete_if do |name,command|
|
28
|
+
not commands.include? name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
new_commands.each_pair do|name, command|
|
33
|
+
if(CommandSet === @command_list[name])
|
34
|
+
next unless CommandSet === command
|
35
|
+
@command_list[name].include_commands(command)
|
36
|
+
else
|
37
|
+
@command_list[name] = command.dup
|
38
|
+
@command_list[name].parent=self
|
39
|
+
end
|
40
|
+
#paths_update(command, name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#Defines a command. Either:
|
45
|
+
#- pass a name and a block, which will create the command on
|
46
|
+
# the fly - within the block, use methods from
|
47
|
+
# Command::DSL::CommandDefinition.
|
48
|
+
#- pass a Command subclass - which will be added to the set based on it's name.
|
49
|
+
#- pass a Command subclass, a name, and a block. The new command will
|
50
|
+
# be a subclass of the class you passed in, which is great for a
|
51
|
+
# series of related commands.
|
52
|
+
def command(name_or_command_class, name_or_nil=nil, &block)
|
53
|
+
@subject_template = nil
|
54
|
+
|
55
|
+
if Class === name_or_command_class && Command > name_or_command_class
|
56
|
+
if block.nil?
|
57
|
+
command = name_or_command_class.dup
|
58
|
+
command.parent = self
|
59
|
+
name = command.name
|
60
|
+
else
|
61
|
+
name = name_or_nil.to_s
|
62
|
+
command = name_or_command_class.setup(self, name, &block)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
if name_or_command_class.nil?
|
66
|
+
name = nil
|
67
|
+
elsif String === name_or_command_class or Symbol === name_or_command_class
|
68
|
+
name = name_or_command_class.to_s
|
69
|
+
else
|
70
|
+
raise RuntimeError, "#{name_or_command_class} is neither a Command class nor a name!"
|
71
|
+
end
|
72
|
+
command = Command.setup(self, name, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
@command_list[name] = command
|
76
|
+
end
|
77
|
+
|
78
|
+
#Defines a nested CommandSet. Commands within the nested set will be
|
79
|
+
#referenced by preceding them with the name of the set.
|
80
|
+
#DSL::CommandSetDefinition will be available within the block to be
|
81
|
+
#used on the subcommand
|
82
|
+
def sub_command(name, &block)
|
83
|
+
@subject_template = nil
|
84
|
+
name = name.to_s
|
85
|
+
|
86
|
+
if (@command_list.has_key? name) && (CommandSet === @command_list[name])
|
87
|
+
command = @command_list[name]
|
88
|
+
else
|
89
|
+
command = CommandSet.new(self, name)
|
90
|
+
@command_list[name] = command
|
91
|
+
end
|
92
|
+
|
93
|
+
command.define_commands(&block)
|
94
|
+
#paths_update(command, name)
|
95
|
+
end
|
96
|
+
|
97
|
+
#Occasionally it makes sense for a subcommand to do something when
|
98
|
+
#invoked on it's own. Define that command using root_command as you
|
99
|
+
#would a normal command
|
100
|
+
def root_command(&block)
|
101
|
+
command(nil, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
#This is the method that makes DSL::CommandSetDefinition available.
|
105
|
+
#It's just a wrapper on instance_eval, honestly.
|
106
|
+
def define_commands(&block)
|
107
|
+
instance_eval(&block)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
#These are the methods made available by Command::setup, and thus by
|
112
|
+
#DSL::CommandSetDefinition#command
|
113
|
+
module CommandDefinition
|
114
|
+
#See Command::Subject. If this command will make use of fields of the
|
115
|
+
#subject, it must declare them using subject_methods. You're then
|
116
|
+
#guaranteed that the subject will either have those fields defined, or
|
117
|
+
#an error will be thrown at runtime. Pass a list of symbols, as you
|
118
|
+
#would to Class#attribute
|
119
|
+
def subject_methods(*methods)
|
120
|
+
@subject_requirements += [*methods]
|
121
|
+
end
|
122
|
+
|
123
|
+
#The core of the Command. Define a block that performs the command.
|
124
|
+
#Within it, you can treat your arguments as readable private attributes
|
125
|
+
#and call methods from DSL::Action
|
126
|
+
def action(&block)
|
127
|
+
define_method(:execute, &block)
|
128
|
+
end
|
129
|
+
|
130
|
+
#Commands should either define an undo block (that will reverse
|
131
|
+
#whatever their action did) or else call doesnt_undo - for things that
|
132
|
+
#don't change any state.
|
133
|
+
#
|
134
|
+
#One particularly useful feature is that each invocation is it's own
|
135
|
+
#object, so you can set instance variables to save the old state if
|
136
|
+
#you want.
|
137
|
+
def undo(&block)
|
138
|
+
define_method(:undo, &block)
|
139
|
+
define_method(:undoable?) do
|
140
|
+
return true
|
141
|
+
end
|
142
|
+
subject_requirements << :undo_stack
|
143
|
+
end
|
144
|
+
|
145
|
+
#Lets the interpreter know that this command intentionally doesn't
|
146
|
+
#provide an undo block - that there's nothing to undo. Use it for
|
147
|
+
#informational commands, primarily. Commands that neither declare
|
148
|
+
#that they 'doesnt_undo' nor provide an undo block will raise a
|
149
|
+
#warning to the user whenever they're called.
|
150
|
+
def doesnt_undo
|
151
|
+
define_method(:undoable?) { return true }
|
152
|
+
define_method(:join_undo) {}
|
153
|
+
end
|
154
|
+
|
155
|
+
#Used to explain to the formatter subsystem how best to format your
|
156
|
+
#output. It can sometimes be useful to output lots of data, and then
|
157
|
+
#use format_advice to eliminate and shuffle it around.
|
158
|
+
#
|
159
|
+
#For more information see Command::Formatter::FormatAdvisor
|
160
|
+
def format_advice(&block)
|
161
|
+
@advice_block = proc &block
|
162
|
+
end
|
163
|
+
|
164
|
+
#Every command should provide a little text to describe what it does.
|
165
|
+
#This will be nicely formatted on output, so feel free to use heredocs
|
166
|
+
#and indent so that it looks nice in the code.
|
167
|
+
def document(text)
|
168
|
+
@doc_text = text.gsub(%r{\s+}, " ").strip
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
#The meta-programmatic machinery to create arguments quickly. Includes
|
173
|
+
#methods such that argument classes can register themselves into the DSL.
|
174
|
+
#Much of this module is unfortunately obtuse - it's designed so that
|
175
|
+
#argument types can be easily extended, which makes the actual DSL
|
176
|
+
#trickier to document.
|
177
|
+
#
|
178
|
+
#Ultimately, arguments are governed by their basic type (which descends
|
179
|
+
#from Argument) and the ArgumentDecorator objects that wrap it.
|
180
|
+
#
|
181
|
+
#Within a Command#setup or CommandSet#command block, you can make
|
182
|
+
#decorator and argument calls like:
|
183
|
+
#
|
184
|
+
# optional.named.string_argument :person, "A Person"
|
185
|
+
#
|
186
|
+
#Which will create a StringArgument and wrap it in the NamedArgument and
|
187
|
+
#OptionalArgument ArgumentDecorators. This sounds confusing, but the
|
188
|
+
#upshot is that the +person+ argument can be omitted, but if it's
|
189
|
+
#included, it must be preceded with the argument's name: "person" like
|
190
|
+
#so:
|
191
|
+
# > command person judson
|
192
|
+
#
|
193
|
+
#Which will assign "judson" to the +person+ argument for the command.
|
194
|
+
#
|
195
|
+
#:include: doc/argumentDSL
|
196
|
+
module Argument
|
197
|
+
#Sugar for creating an alternating argument. Basically, an
|
198
|
+
#alternating argument is a series of arguments, any of which could be
|
199
|
+
#set. They either need to be of distinct types, or use +named+ to
|
200
|
+
#distinguish between them.
|
201
|
+
def alternating_argument(name, &block)
|
202
|
+
arg = AlternatingArgument.new(self, &block)
|
203
|
+
arg.name(name)
|
204
|
+
embed_argument(arg)
|
205
|
+
end
|
206
|
+
|
207
|
+
@@decorator_map={}
|
208
|
+
#The ArgumentDecorator#register method calls back to this, so that
|
209
|
+
#decorators can quickly register a method to wrap an argument with
|
210
|
+
#themselves.
|
211
|
+
def self.register_decorator(klass, method)
|
212
|
+
@@decorator_map[method] = klass
|
213
|
+
alias_method method, :create_decorator
|
214
|
+
end
|
215
|
+
|
216
|
+
@@argmap={}
|
217
|
+
@@shorthand_map={}
|
218
|
+
#The Argument#register method calls back to this, which creates
|
219
|
+
#methods like +funky_argument+ that are responsible for embedding the
|
220
|
+
#actual arguments in the Commands they're declared for.
|
221
|
+
def self.register_argument(klass, shorthand, type=nil)
|
222
|
+
unless type.nil? or not Class === type or @@argmap.has_key?(type)
|
223
|
+
@@argmap[type]=klass
|
224
|
+
end
|
225
|
+
|
226
|
+
method_name = shorthand + "_argument"
|
227
|
+
@@shorthand_map[method_name] = klass
|
228
|
+
|
229
|
+
alias_method method_name, :special_argument
|
230
|
+
end
|
231
|
+
|
232
|
+
#Generates rdoc ready documentation of the decorator and argument
|
233
|
+
#methods created by #register calls. Output is included in this
|
234
|
+
#module's documentation. Also useful if you want to document your own
|
235
|
+
#argument class' contributions. Try something like:
|
236
|
+
#
|
237
|
+
# > ruby -r"lib/command-set/arguments.rb" -e "Command::DSL::Argument::document"
|
238
|
+
def self.document
|
239
|
+
docs = <<-EOD
|
240
|
+
There are two kinds of methods available for #{self.name}.
|
241
|
+
First there are decorators. They mark up arguments with extra
|
242
|
+
meaning, like being optional. The second are actual argument
|
243
|
+
creation calls, which are shorthand for something like
|
244
|
+
argument FiddlyArgument {}
|
245
|
+
|
246
|
+
In general, you'll use these something like
|
247
|
+
|
248
|
+
decorator.decorator.shorthand_argument "name"
|
249
|
+
|
250
|
+
For instance
|
251
|
+
|
252
|
+
named.optional.file_argument "config"
|
253
|
+
|
254
|
+
Decorator methods, and the classes they add:
|
255
|
+
|
256
|
+
EOD
|
257
|
+
|
258
|
+
@@decorator_map.each_pair do |method, klass|
|
259
|
+
docs += "+#{method}+:: #{klass.name.sub("Command::","")}\n"
|
260
|
+
end
|
261
|
+
|
262
|
+
docs += <<-EOD
|
263
|
+
|
264
|
+
The shorthand argument methods are:
|
265
|
+
|
266
|
+
EOD
|
267
|
+
|
268
|
+
@@shorthand_map.each_pair do |method, klass|
|
269
|
+
docs += "+#{method}+:: #{klass.name.sub("Command::","")}\n"
|
270
|
+
end
|
271
|
+
|
272
|
+
docs += <<-EOD
|
273
|
+
|
274
|
+
Don't forget about #alternating_argument and #argument itself!
|
275
|
+
EOD
|
276
|
+
|
277
|
+
indent = /^\s+/.match(docs)[0]
|
278
|
+
docs.gsub!(/^#{indent}/, "")
|
279
|
+
|
280
|
+
puts docs
|
281
|
+
end
|
282
|
+
|
283
|
+
def self.argument_typemap #:nodoc:
|
284
|
+
@@argmap
|
285
|
+
end
|
286
|
+
|
287
|
+
#When an ArgumentDecorator calls self.register, this method is aliased
|
288
|
+
#with the name the decorator passes It takes care of instantiating the
|
289
|
+
#decorator such that it's available to decorate the eventual argument.
|
290
|
+
#
|
291
|
+
#Don't look to closely at the source. It does bad things.
|
292
|
+
def create_decorator(&block)
|
293
|
+
me = /:in `([^']*)/.match(caller(0)[0])[1]
|
294
|
+
decorator = @@decorator_map[me].allocate
|
295
|
+
decorator.extend DSL::Argument
|
296
|
+
embed_in = self
|
297
|
+
decorator.instance_eval do
|
298
|
+
initialize(embed_in, &block)
|
299
|
+
end
|
300
|
+
return decorator
|
301
|
+
end
|
302
|
+
|
303
|
+
#This method functions analogously to create_decorator, except it
|
304
|
+
#works for arguments, not decorators. It's worth looking at as the
|
305
|
+
#call signature for all +funky_argument+ style calls.
|
306
|
+
def special_argument(arg, values=nil, &get_values)
|
307
|
+
me = /:in `([^']*)/.match(caller(0)[0])[1]
|
308
|
+
name = nil
|
309
|
+
argument = nil
|
310
|
+
if(::Command::Argument === arg)
|
311
|
+
name = arg.name
|
312
|
+
argument = arg
|
313
|
+
else
|
314
|
+
name = arg
|
315
|
+
argument = @@shorthand_map[me].new(name, get_values||values)
|
316
|
+
end
|
317
|
+
return self.embed_argument(argument)
|
318
|
+
end
|
319
|
+
|
320
|
+
#The basic argument definition. If +arg+ is an Argument object, it'll
|
321
|
+
#be used - which means that you can explicitly create and argument and
|
322
|
+
#embed it. Otherwise, the values of +values+ or +get_values+ will be
|
323
|
+
#used to create the argument
|
324
|
+
def argument(arg, values=nil, &get_values)
|
325
|
+
name = nil
|
326
|
+
argument = nil
|
327
|
+
if(::Command::Argument === arg)
|
328
|
+
name = arg.name
|
329
|
+
argument = arg
|
330
|
+
else
|
331
|
+
name = arg
|
332
|
+
argument = create(name, get_values||values)
|
333
|
+
end
|
334
|
+
|
335
|
+
return self.embed_argument(argument)
|
336
|
+
end
|
337
|
+
|
338
|
+
#The method used to instantiate arguments based on their values.
|
339
|
+
#Searches all registered Argument classes, from children up, until one
|
340
|
+
#admits to being able to create arguments based on the value.
|
341
|
+
def create(name, basis)
|
342
|
+
@@argmap.keys.sort{|r,l|(r>l)?1:-1}.each do |type| #Check child classes first
|
343
|
+
if type === basis
|
344
|
+
return @@argmap[type].new(name, basis)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
raise TypeError, "Don't know how to base an argument " +
|
348
|
+
"on #{basis.class}"
|
349
|
+
end
|
350
|
+
|
351
|
+
def named_optionals #:nodoc:
|
352
|
+
raise NotImplementedException
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
#The methods available within the DSL::CommandDefinition#action method
|
357
|
+
#
|
358
|
+
#The trickiest thing to realize about writing Commands is that a
|
359
|
+
#CommandSet is an _object_ that contains several Command _subclasses_;
|
360
|
+
#Commad::setup creates a subclass, and so CommandSet#command does too.
|
361
|
+
#It's when a command is invoked that it's actually instantiated.
|
362
|
+
#
|
363
|
+
#Also note that you can access the arguments of a command as read-only
|
364
|
+
#attributes, and you can write to and read from instance variables,
|
365
|
+
#which will be local to the invocation of the command. This is
|
366
|
+
#especially useful for undo and redo.
|
367
|
+
module Action
|
368
|
+
|
369
|
+
#:section: Basics
|
370
|
+
|
371
|
+
#Some commands sometimes cause side effects. When evaluating
|
372
|
+
#arguments, if you discover that undoing doesn't make sense, and will
|
373
|
+
#be confusing to the user, call dont_undo, and the interpreter will
|
374
|
+
#ignore the call for purposes of undoing
|
375
|
+
def dont_undo
|
376
|
+
@should_undo = false
|
377
|
+
return nil
|
378
|
+
end
|
379
|
+
|
380
|
+
#This is how you'll access the Command::Subject object that's the
|
381
|
+
#interface of every command to the program state.
|
382
|
+
def subject
|
383
|
+
@subject
|
384
|
+
end
|
385
|
+
|
386
|
+
#:section: Formatting
|
387
|
+
|
388
|
+
#To create lists and sublist of data, you can use #list to wrap code
|
389
|
+
#in a #begin_list / #end_list pair.
|
390
|
+
def list(name, options={}) #:yield:
|
391
|
+
begin_list(name, options)
|
392
|
+
yield if block_given?
|
393
|
+
end_list
|
394
|
+
end
|
395
|
+
|
396
|
+
#Tells the main collector to begin a list. Subsequent output will be
|
397
|
+
#gathered into that list. For more, check out Results::Collector
|
398
|
+
def begin_list(name, options={})
|
399
|
+
@main_collector.begin_list(name, options)
|
400
|
+
end
|
401
|
+
|
402
|
+
#Tells the main collector to end the current list. For more, check out
|
403
|
+
#Results::Collector
|
404
|
+
def end_list
|
405
|
+
@main_collector.end_list
|
406
|
+
end
|
407
|
+
|
408
|
+
#Clean way to create an item of output. Allows for various options to
|
409
|
+
#be added. The normal output method (#puts, #p, #write...) are all
|
410
|
+
#diverted within a command, and effectively create no-option items.
|
411
|
+
def item(name, options={})
|
412
|
+
@main_collector.item(name, options)
|
413
|
+
end
|
414
|
+
|
415
|
+
#:section: Pause and Resume
|
416
|
+
|
417
|
+
#Stop here. Return control to the user. If several commands are
|
418
|
+
#chained (c.f. #chain) and the pause is subsequently resumed
|
419
|
+
#(StandardCommands::Resume) the whole chain will be resumed.
|
420
|
+
def pause(deck = nil)
|
421
|
+
raise ResumeFrom, deck
|
422
|
+
end
|
423
|
+
|
424
|
+
#Stop here and return control to the user. If several commands are
|
425
|
+
#chained (c.f. #chain) and the pause is subsequently resumed
|
426
|
+
#(StandardCommands::Resume) the rest of the chain (not this command)
|
427
|
+
#will be dropped.
|
428
|
+
def defer(deck = nil)
|
429
|
+
raise ResumeFromOnlyThis, deck
|
430
|
+
end
|
431
|
+
|
432
|
+
#Allows for a command to be broken into pieces so that a resume can
|
433
|
+
#pick up within a command. The block will be executed normally, but
|
434
|
+
#if the command is resumed with a task id, all task blocks until that
|
435
|
+
#id will be skipped.
|
436
|
+
def task(id) #:yield:
|
437
|
+
if not @resume_from.nil?
|
438
|
+
if @resume_from == id
|
439
|
+
@resume_from = nil
|
440
|
+
end
|
441
|
+
return
|
442
|
+
end
|
443
|
+
yield if block_given?
|
444
|
+
@last_completed_task = id
|
445
|
+
end
|
446
|
+
|
447
|
+
#:section: Command compositing
|
448
|
+
|
449
|
+
#It frequently makes sense to offer shortcut chains to the user, or
|
450
|
+
#even commands that can only be run as part of another command.
|
451
|
+
#Calling chain with either a command class or a command path allows
|
452
|
+
#will cause that command to be invoked before returning control to the
|
453
|
+
#user.
|
454
|
+
def chain(klass_or_path, args)
|
455
|
+
setup = CommandSetup.new
|
456
|
+
setup.command = klass_or_path
|
457
|
+
setup.args_hash = args || {}
|
458
|
+
subject.chain_of_command.push(setup)
|
459
|
+
end
|
460
|
+
|
461
|
+
#Like #chain, but interjects the command being chained to the start of
|
462
|
+
#the queue, immediately after this command completes.
|
463
|
+
def chain_first(klass_or_path, args)
|
464
|
+
setup = CommandSetup.new
|
465
|
+
setup.command = klass_or_path
|
466
|
+
setup.args_hash = args
|
467
|
+
subject.chain_of_command.unshift(setup)
|
468
|
+
end
|
469
|
+
|
470
|
+
#:section: Miscellany
|
471
|
+
|
472
|
+
#Not normally called from within an #action block, this provides the
|
473
|
+
#default behavior for an undo (raise an exception)
|
474
|
+
def undo(box)
|
475
|
+
raise CommandException, "#{@name} cannot be undone"
|
476
|
+
end
|
477
|
+
|
478
|
+
#This returns a new Results::Collector, which can allow for some very
|
479
|
+
#sophisticated command output. Specifically, it can allow a command
|
480
|
+
#to loop over a large amount of data once, depositing output in
|
481
|
+
#multiple lists at once, for instance a status list (with hashmarks)
|
482
|
+
#and results(with useful data) list.
|
483
|
+
def sub_collector
|
484
|
+
@main_collector.dup
|
485
|
+
end
|
486
|
+
|
487
|
+
#This method is deprecated but remains as a nicety. As it stands, any
|
488
|
+
#command can be interrupted at the command line with Ctrl-C, and
|
489
|
+
#return to the prompt.
|
490
|
+
def interruptable
|
491
|
+
yield
|
492
|
+
end
|
493
|
+
|
494
|
+
#These methods are nodoc'd because at present, they don't work.
|
495
|
+
#They'd be awesome for big jobs - splitting them into subthreads and
|
496
|
+
#such. But they need to be debugged, and IIRC there's a deadlock
|
497
|
+
#condition
|
498
|
+
def action_thread(&block) #:nodoc:
|
499
|
+
return Thread.new do
|
500
|
+
collector = sub_collector
|
501
|
+
$stdout.set_thread_collector(collector)
|
502
|
+
block.call
|
503
|
+
$stdout.remove_thread_collector(collector)
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
require 'thwait'
|
508
|
+
def farm_out(array, thread_count, &block) #:nodoc:
|
509
|
+
first_batch = (array[0...thread_count]||[]).map do |item|
|
510
|
+
action_thread { block.call(item) }
|
511
|
+
end
|
512
|
+
|
513
|
+
rest = (array[thread_count..-1] || [])
|
514
|
+
|
515
|
+
waiter = ThreadsWait.new(*first_batch)
|
516
|
+
|
517
|
+
rest.each do |item|
|
518
|
+
waiter.next_wait
|
519
|
+
waiter.join_nowait(action_thread{block.call(item)})
|
520
|
+
end
|
521
|
+
|
522
|
+
waiter.join
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|