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,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
+