command-set 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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