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,754 @@
1
+ require 'delegate'
2
+ require 'command-set/result-list'
3
+
4
+ module Kernel
5
+ def puts(*args)
6
+ $stdout.puts(*args)
7
+ end
8
+ end
9
+
10
+
11
+ module Command
12
+ class << self
13
+ #Call anywhere to be sure that $stdout is replaced by an OutputStandin that
14
+ #delegates to the original STDOUT IO. This by itself won't change output behavior.
15
+ #Requiring 'command-set/command-set' does this for you. Multiple calls are safe though.
16
+ def wrap_stdout
17
+ return if $stdout.respond_to?(:add_dispatcher)
18
+ $stdout = OutputStandin.new($stdout)
19
+ end
20
+
21
+ #If you need the actual IO for /dev/stdout, you can call this to get it. Useful inside of
22
+ #Results::Formatter subclasses, for instance, so that they can actually send messages out to
23
+ #the user.
24
+ def raw_stdout
25
+ if $stdout.respond_to?(:__getobj__)
26
+ $stdout.__getobj__
27
+ else
28
+ $stdout
29
+ end
30
+ end
31
+
32
+ #See Command::wrap_stdout
33
+ def wrap_stderr
34
+ return if $stdout.respond_to?(:add_dispatcher)
35
+ $stderr = OutputStandin.new($stderr)
36
+ end
37
+
38
+ #See Command::raw_stdout
39
+ def raw_stderr
40
+ if $stderr.respond_to?(:__getobj__)
41
+ $stderr.__getobj__
42
+ else
43
+ $stderr
44
+ end
45
+ end
46
+ end
47
+
48
+ #Wraps an IO using DelegateClass. Dispatches all calls to the IO, until a
49
+ #Collector is registered, at which point, methods that the Collector
50
+ #handles will get sent to it.
51
+ class OutputStandin < IO
52
+ def initialize(io)
53
+ @_dc_obj = io
54
+ @dispatch_stack = nil
55
+ unless io.fileno.nil?
56
+ super(io.fileno,"w")
57
+ end
58
+ end
59
+
60
+ def method_missing(m, *args) # :nodoc:
61
+ unless @_dc_obj.respond_to?(m)
62
+ super(m, *args)
63
+ end
64
+ @_dc_obj.__send__(m, *args)
65
+ end
66
+
67
+ def respond_to?(m) # :nodoc:
68
+ return true if super
69
+ return @_dc_obj.respond_to?(m)
70
+ end
71
+
72
+ def __getobj__ # :nodoc:
73
+ @_dc_obj
74
+ end
75
+
76
+ def __setobj__(obj) # :nodoc:
77
+ raise ArgumentError, "cannot delegate to self" if self.equal?(obj)
78
+ @_dc_obj = obj
79
+ end
80
+
81
+ def clone # :nodoc:
82
+ super
83
+ __setobj__(__getobj__.clone)
84
+ end
85
+
86
+ def dup # :nodoc:
87
+ super
88
+ __setobj__(__getobj__.dup)
89
+ end
90
+
91
+ #This looks gnarly, but DelegateClass takes out methods defined by
92
+ #Kernel -- Which usually makes sense, but IO has a bunch of methods that
93
+ #Kernel defines to basically delegate to an IO.... I have a headache
94
+ #now.
95
+ methods = IO.public_instance_methods(false)
96
+ methods -= self.public_instance_methods(false)
97
+ methods |= ['class']
98
+ methods.each do |method|
99
+ begin
100
+ module_eval <<-EOS
101
+ def #{method}(*args, &block)
102
+ begin
103
+ @_dc_obj.__send__(:#{method}, *args, &block)
104
+ rescue
105
+ $@[0,2] = nil
106
+ raise
107
+ end
108
+ end
109
+ EOS
110
+ rescue SyntaxError
111
+ raise NameError, "invalid identifier %s" % method, caller(3)
112
+ end
113
+ end
114
+
115
+ def thread_stack_index
116
+ "standin_dispatch_stack_#{self.object_id}"
117
+ end
118
+
119
+ def relevant_collector
120
+ Thread.current[thread_stack_index] || @dispatch_stack
121
+ end
122
+
123
+ def dispatched_method(method, *args)# :nodoc:
124
+ collector = relevant_collector
125
+ if not collector.nil? and collector.respond_to?(method)
126
+ return collector.__send__(method, *args)
127
+ end
128
+ return __getobj__.__send__(method, *args)
129
+ end
130
+
131
+ def add_thread_local_dispatcher(collector)
132
+ Thread.current[thread_stack_index]=collector
133
+ define_dispatch_methods(collector)
134
+ end
135
+ alias set_thread_collector add_thread_local_dispatcher
136
+
137
+ #Puts the dispatcher in place to handle normal IO methods.
138
+ def add_dispatcher(collector)
139
+ @dispatch_stack = collector
140
+ define_dispatch_methods(collector)
141
+ end
142
+ alias set_default_collector add_dispatcher
143
+
144
+ def define_dispatch_methods(dispatcher)# :nodoc:
145
+ dispatcher.dispatches.each do |dispatch|
146
+ (class << self; self; end).module_eval <<-EOS
147
+ def #{dispatch}(*args)
148
+ dispatched_method(:#{dispatch.to_s}, *args)
149
+ end
150
+ EOS
151
+ end
152
+ end
153
+
154
+ #Unregisters the dispatcher.
155
+ def remove_dispatcher(dispatcher)
156
+ @dispatch_stack = nil if @dispatch_stack == dispatcher
157
+ end
158
+ alias remove_collector remove_dispatcher
159
+
160
+ def remove_thread_local_dispatcher(dispatcher)
161
+ if Thread.current[thread_stack_index] == dispatcher
162
+ Thread.current[thread_stack_index] = nil
163
+ end
164
+ end
165
+ alias remove_thread_collector remove_thread_local_dispatcher
166
+ end
167
+
168
+ #This is the output management module for CommandSet. With an eye towards
169
+ #being a general purpose UI library, and motivated by the need to manage
170
+ #pretty serious output management, the Results module provides a
171
+ #reasonably sophisticated output train that runs like this:
172
+ #
173
+ #0. An OutputStandin intercepts normal output and feeds it to ...
174
+ #0. A Collector aggregates output from OutputStandins and explicit #item
175
+ # and #list calls and feeds to to ...
176
+ #0. A Presenter handles the stream of output from Collector objects and
177
+ # emits +saw+ and +closed+ events to one or more ...
178
+ #0. Formatter objects, which interpret those events into user-readable
179
+ # output.
180
+ module Results
181
+ #Collects the events spawned by dispatchers and sends them to the presenter.
182
+ #Responsible for maintaining it's own place within the larger tree, but doesn't
183
+ #understand that other Collectors could be running at the same time - that's the
184
+ #Presenter's job.
185
+ class Collector
186
+ def initialize(presenter)
187
+ @presenter = presenter
188
+ @nesting = []
189
+ end
190
+
191
+ def initialize_copy(original)
192
+ @presenter = original.instance_variable_get("@presenter")
193
+ @nesting = original.instance_variable_get("@nesting").dup
194
+ end
195
+
196
+ def items(*objs)
197
+ if Hash === objs.last
198
+ options = objs.pop
199
+ else
200
+ options = {}
201
+ end
202
+ objs.each do |obj|
203
+ item(obj, options)
204
+ end
205
+ end
206
+
207
+ def item( obj, options={} )
208
+ @presenter.item(@nesting.dup + [obj], options)
209
+ end
210
+
211
+ def begin_list( name, options={} )
212
+ @nesting.push(name)
213
+ @presenter.begin_list(@nesting.dup, options)
214
+ if block_given?
215
+ yield
216
+ end_list
217
+ end
218
+ end
219
+
220
+ def open_list(name, options={})
221
+ @nesting.push(name)
222
+ unless @presenter.list_open?(@nesting)
223
+ @presenter.begin_list(@nesting.dup, options)
224
+ end
225
+ if block_given?
226
+ yield
227
+ end_list
228
+ end
229
+ end
230
+
231
+ def end_list
232
+ @presenter.end_list(@nesting.dup)
233
+ @nesting.pop
234
+ end
235
+
236
+ @dispatches = {}
237
+
238
+ def self.inherited(sub)
239
+ sub.instance_variable_set("@dispatches", @dispatches.dup)
240
+ end
241
+
242
+ def self.dispatches
243
+ @dispatches.keys
244
+ end
245
+
246
+ def dispatches
247
+ self.class.dispatches
248
+ end
249
+
250
+ #Use to register an IO +method+ to handle. The block will be passed a Collector and the
251
+ #arguments passed to +method+.
252
+ def self.dispatch(method, &block)
253
+ @dispatches[method] = true
254
+ define_method(method, &block)
255
+ end
256
+
257
+ dispatch :puts do |*args|
258
+ args.each do |arg|
259
+ item arg
260
+ end
261
+ end
262
+
263
+ dispatch :write do |*args|
264
+ args.each do |arg|
265
+ item arg
266
+ end
267
+ end
268
+
269
+ dispatch :p do |*args|
270
+ args.each do |arg|
271
+ item(arg, :string => :inspect, :timing => :immediate)
272
+ end
273
+ end
274
+ end
275
+
276
+ #Gets item and list events from Collectors, and emits two kinds of
277
+ #events to Formatters:
278
+ #[+saw+ events] occur in chronological order, with no guarantee regarding timing.
279
+ #[+closed+ events] occur in tree order.
280
+ #
281
+ #In general, +saw+ events are good for immediate feedback to the user,
282
+ #not so good in terms of making sense of things. They're generated as
283
+ #soon as the relevant output element enters the system.
284
+ #
285
+ #On the other hand, +closed+ events will be generated in the natural
286
+ #order you'd expect the output to appear in. Most Formatter subclasses use
287
+ #+closed+ events.
288
+ #
289
+ #A list which has not received a "list_end" event from upstream will
290
+ #block lists later in tree order until it closes. A Formatter that
291
+ #listens only to +closed+ events can present them to the user in a way
292
+ #that should be reasonable, although output might be halting for any
293
+ #process that takes noticeable time.
294
+ class Presenter
295
+ class Exception < ::Exception; end
296
+
297
+ def initialize
298
+ @results = List.new("")
299
+ @leading_edge = @results
300
+ @formatters = []
301
+ end
302
+
303
+ def create_collector
304
+ return Collector.new(self)
305
+ end
306
+
307
+ def item( item_path, options={} )
308
+ item = item_path.pop
309
+ home = get_collection(item_path)
310
+ item = home.add item
311
+ item.options = home.options.merge(options)
312
+ item.depth = item_path.length
313
+ notify(:saw, item)
314
+ advance_leading_edge
315
+ end
316
+
317
+ def leading_edge?(list)
318
+ return list == @leading_edge
319
+ end
320
+
321
+ def register_formatter(formatter)
322
+ @formatters << formatter
323
+
324
+ formatter.notify(:start, nil)
325
+ end
326
+
327
+ def begin_list( list_path, options={} )
328
+ list = list_path.pop
329
+ home = get_collection(list_path)
330
+ list = List.new(list)
331
+ list.options = home.options.merge(options)
332
+ list.depth = list_path.length
333
+ notify(:saw_begin, list)
334
+ home.add(list)
335
+ advance_leading_edge
336
+ end
337
+
338
+ def list_open?(list_path)
339
+ begin
340
+ get_collection(list_path)
341
+ return true
342
+ rescue Exception
343
+ return false
344
+ end
345
+ end
346
+
347
+ def end_list( list_path )
348
+ list = get_collection( list_path )
349
+ notify(:saw_end, list)
350
+ list.close
351
+ advance_leading_edge
352
+ end
353
+
354
+ def done
355
+ @results.close
356
+ advance_leading_edge
357
+ notify(:done, nil)
358
+ end
359
+
360
+ #Returns the current list of results. A particularly advanced Formatter might treat +saw_*+ events
361
+ #like notifications, and then use the List#filter functionality to discover the specifics about the
362
+ #item or list just closed.
363
+ def output
364
+ @results
365
+ end
366
+
367
+ protected
368
+ def advance_leading_edge
369
+ iter = ListIterator.new(@leading_edge.tree_order_next)
370
+ iter.each do |forward|
371
+ case forward
372
+ when ListEnd
373
+ break if forward.end_of.open?
374
+ break if forward.end_of.name.empty?
375
+ notify(:leave, forward.end_of)
376
+ when List
377
+ notify(:arrive, forward)
378
+ when ListItem
379
+ notify(:arrive, forward)
380
+ end
381
+
382
+ @leading_edge = forward
383
+ end
384
+ end
385
+
386
+ def notify(msg, item)
387
+ @formatters.each do |f|
388
+ f.notify(msg, item)
389
+ end
390
+ end
391
+
392
+ def get_collection(path)
393
+ thumb = @results.values
394
+ list = @results
395
+ path.each do |step|
396
+ list = thumb.find{|member| List === member && member.open? && member.name == step}
397
+ if list.nil?
398
+ raise Exception, "#{step} in #{path.inspect} missing from #{thumb}!"
399
+ end
400
+ thumb = list.values
401
+ end
402
+ return list
403
+ end
404
+ end
405
+
406
+ #The end of the Results train. Formatter objects are supposed to output to the user events that they
407
+ #receive from their presenters. To simplify this process, a number of common IO functions are delegated
408
+ #to an IO object - usually Command::raw_stdout.
409
+ #
410
+ #This class in particular is pretty quiet - probably not helpful for everyday use.
411
+ #Of course, for some purposes, singleton methods might be very useful
412
+ class Formatter
413
+ extend Forwardable
414
+
415
+ class FormatAdvisor
416
+ def initialize(formatter)
417
+ @advisee = formatter
418
+ end
419
+
420
+ def list(&block)
421
+ @advisee.advice[:list] << proc(&block)
422
+ end
423
+
424
+ def item(&block)
425
+ @advisee.advice[:item] << proc(&block)
426
+ end
427
+
428
+ def output(&block)
429
+ @advisee.advice[:output] << proc(&block)
430
+ end
431
+ end
432
+
433
+ def notify(msg, item)
434
+ if msg == :start
435
+ start
436
+ return
437
+ end
438
+ if msg == :done
439
+ finish
440
+ return
441
+ end
442
+
443
+ apply_advice(item)
444
+
445
+ if List === item
446
+ case msg
447
+ when :saw_begin
448
+ saw_begin_list(item)
449
+ when :saw_end
450
+ saw_end_list(item)
451
+ when :arrive
452
+ closed_begin_list(item)
453
+ when :leave
454
+ closed_end_list(item)
455
+ end
456
+ else
457
+ case msg
458
+ when :arrive
459
+ closed_item(item)
460
+ when :saw
461
+ saw_item(item)
462
+ end
463
+ end
464
+ end
465
+
466
+ def initialize(io)
467
+ @out_to = io
468
+ @advisor = FormatAdvisor.new(self)
469
+ @advice = {:list => [], :item => [], :output => []}
470
+ end
471
+
472
+ def apply_advice(item)
473
+ type = List === item ? :list : :item
474
+
475
+ item.options[:format_advice] =
476
+ @advice[type].inject(default_advice(type)) do |advice, advisor|
477
+ result = advisor[item]
478
+ break if result == :DONE
479
+ advice.merge!(result) if Hash === result
480
+ advice
481
+ end
482
+ end
483
+
484
+ attr_reader :advice
485
+
486
+ def receive_advice(&block)
487
+ @advisor.instance_eval(&block)
488
+ end
489
+
490
+ def default_advice(type)
491
+ {}
492
+ end
493
+
494
+ def_delegators :@out_to, :p, :puts, :print, :printf, :putc, :write, :write_nonblock, :flush
495
+
496
+ def self.inherited(sub)
497
+ sub.extend Forwardable
498
+ sub.class_eval do
499
+ def_delegators :@out_to, :p, :puts, :print, :printf, :putc, :write, :write_nonblock, :flush
500
+ end
501
+ end
502
+
503
+ #Presenter callback: output is beginning
504
+ def start; end
505
+
506
+ #Presenter callback: a list has just started
507
+ def saw_begin_list(list); end
508
+
509
+ #Presenter callback: an item has just been added
510
+ def saw_item(item); end
511
+
512
+ #Presenter callback: a list has just ended
513
+ def saw_end_list(list); end
514
+
515
+ #Presenter callback: a list opened, tree order
516
+ def closed_begin_list(list); end
517
+
518
+ #Presenter callback: an item added, tree order
519
+ def closed_item(item); end
520
+
521
+ #Presenter callback: an list closed, tree order
522
+ def closed_end_list(list); end
523
+
524
+ #Presenter callback: output is done
525
+ def finish; end
526
+ end
527
+
528
+ #The simplest useful Formatter: it outputs the value of every item in tree order. Think of
529
+ #it as what would happen if you just let puts and p go directly to the screen, without the
530
+ #annoying consequences of threading, etc.
531
+ class TextFormatter < Formatter
532
+ def closed_item(value)
533
+ puts value
534
+ end
535
+ end
536
+
537
+ class StrategyFormatter < Formatter
538
+ class FormatStrategy
539
+ extend Forwardable
540
+
541
+ def initialize(name, formatter)
542
+ @name = name
543
+ @formatter = formatter
544
+ setup
545
+ end
546
+
547
+ def setup; end
548
+
549
+ def_delegators :@formatter, :p, :puts, :print, :printf, :putc, :write, :write_nonblock, :flush
550
+
551
+ attr_reader :name
552
+
553
+ def switch_to(name)
554
+ @formatter.push_strategy(name)
555
+ end
556
+
557
+ def finish
558
+ @formatter.pop_strategy(self.name)
559
+ end
560
+
561
+ #Presenter callback: a list has just started
562
+ def saw_begin_list(list); end
563
+
564
+ #Presenter callback: an item has just been added
565
+ def saw_item(item); end
566
+
567
+ #Presenter callback: a list has just ended
568
+ def saw_end_list(list); end
569
+
570
+ #Presenter callback: a list opened, tree order
571
+ def closed_begin_list(list);
572
+ unless list.options[:strategy_start] == self or list.options[:format_advice].nil?
573
+ switch_to(list.options[:format_advice][:type])
574
+ if (next_strat = @formatter.current_strategy) != self
575
+ list.options[:strategy_start] = next_strat
576
+ next_strat.closed_begin_list(list)
577
+ end
578
+ end
579
+ end
580
+
581
+ #Presenter callback: an item added, tree order
582
+ def closed_item(item); end
583
+
584
+ #Presenter callback: an list closed, tree order
585
+ def closed_end_list(list);
586
+ if list.options[:strategy_start] == self
587
+ finish
588
+ end
589
+ end
590
+ end
591
+
592
+ @strategies = {:default => FormatStrategy}
593
+
594
+ class << self
595
+ def strategy(name, base_klass = FormatStrategy, &def_block)
596
+ @strategies[name.to_sym] = Class.new(base_klass, &def_block)
597
+ end
598
+
599
+ def inherited(sub)
600
+ self.instance_variables.each do |var|
601
+ value = self.instance_variable_get(var)
602
+ if value.nil?
603
+ sub.instance_variable_set(var, nil)
604
+ else
605
+ sub.instance_variable_set(var, value.dup)
606
+ end
607
+ end
608
+ end
609
+
610
+ def strategy_set(formatter)
611
+ set = {}
612
+ @strategies.each_pair do |name, klass|
613
+ set[name] = klass.new(name, formatter)
614
+ end
615
+ return set
616
+ end
617
+ end
618
+
619
+ def initialize(io)
620
+ super(io)
621
+ @strategies = self.class.strategy_set(self)
622
+ @strategy_stack = [@strategies[:default]]
623
+ end
624
+
625
+ def_delegators :current_strategy, :saw_begin_list, :saw_item, :saw_end_list,
626
+ :closed_begin_list, :closed_item, :closed_end_list
627
+
628
+ def current_strategy
629
+ @strategy_stack.last
630
+ end
631
+
632
+ def push_strategy(name)
633
+ if @strategies.has_key?(name)
634
+ @strategy_stack.push(@strategies[name])
635
+ end
636
+ end
637
+
638
+ def pop_strategy(name)
639
+ if current_strategy.name == name
640
+ @strategy_stack.pop
641
+ end
642
+ end
643
+
644
+ strategy :default do
645
+ def closed_item(value)
646
+ puts value
647
+ end
648
+ end
649
+
650
+ strategy :progress do
651
+ def closed_begin_list(list)
652
+ print list.to_s.ljust(50)
653
+ end
654
+
655
+ def closed_item(item)
656
+ print "."
657
+ flush
658
+ end
659
+
660
+ def finish
661
+ super
662
+ puts
663
+ end
664
+ end
665
+
666
+ strategy :indent do
667
+ def setup
668
+ @indent_level = 0
669
+ end
670
+
671
+ def indent
672
+ return " " * [0, @indent_level].max
673
+ end
674
+
675
+ def closed_begin_list(list)
676
+ puts indent + list.to_s
677
+ @indent_level += 1
678
+ super
679
+ end
680
+
681
+ def closed_item(item)
682
+ item.to_s.split(/\s*\n\s*/).each do |line|
683
+ puts indent + line
684
+ end
685
+ super
686
+ end
687
+
688
+ def closed_end_list(list)
689
+ @indent_level -= 1
690
+ super
691
+ end
692
+ end
693
+
694
+ strategy :invisible do
695
+ def closed_item(value)
696
+ end
697
+ end
698
+
699
+ strategy :skip do
700
+ def closed_begin_list(list)
701
+ finish
702
+ end
703
+ end
704
+
705
+ strategy :chatty do
706
+ def saw_begin_list(list); $stderr.print "B"; end
707
+ def saw_item(list); $stderr.print "."; end
708
+ def saw_end_list(list); $stderr.print "E"; end
709
+ def closed_begin_list(list);
710
+ clean_options = list.options.dup
711
+ clean_options.delete(:strategy_start)
712
+ puts "> #{list.to_s} (depth=#{list.depth} #{clean_options.inspect})"
713
+ end
714
+ def closed_item(list); puts " " + list.to_s; end
715
+ def closed_end_list(list); puts "< " + list.to_s; end
716
+ end
717
+ end
718
+
719
+ #A trivial and obvious Formatter: produces well-formed XML fragments based on events. It even
720
+ #indents. Might be handy for more complicated output processing, since you could feed the document
721
+ #to a XSLT processor.
722
+ class XMLFormatter < Formatter
723
+ def initialize(io, indent=" ", newline="\n")
724
+ super(io)
725
+ @indent = indent
726
+ @newline = newline
727
+ @indent_level=0
728
+ end
729
+
730
+ def line(string)
731
+ print "#{@indent * @indent_level}#{string}#@newline"
732
+ end
733
+
734
+ def closed_begin_list(name)
735
+ line "<#{name}>"
736
+ @indent_level += 1
737
+ end
738
+
739
+ def closed_item(value)
740
+ line "<item value=\"#{value}\" />"
741
+ end
742
+
743
+ def closed_end_list(name)
744
+ @indent_level -= 1
745
+ if @indent_level < 0
746
+ @indent_level = 0
747
+ return
748
+ end
749
+ line "</#{name}>"
750
+ end
751
+ end
752
+ end
753
+ end
754
+