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.
@@ -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