command-set 0.8.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/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
|