1require 'abbrev'
2require 'optparse'
3
4begin
5  require 'readline'
6rescue LoadError
7end
8
9begin
10  require 'win32console'
11rescue LoadError
12end
13
14require 'rdoc'
15
16##
17# For RubyGems backwards compatibility
18
19require 'rdoc/ri/formatter'
20
21##
22# The RI driver implements the command-line ri tool.
23#
24# The driver supports:
25# * loading RI data from:
26#   * Ruby's standard library
27#   * RubyGems
28#   * ~/.rdoc
29#   * A user-supplied directory
30# * Paging output (uses RI_PAGER environment variable, PAGER environment
31#   variable or the less, more and pager programs)
32# * Interactive mode with tab-completion
33# * Abbreviated names (ri Zl shows Zlib documentation)
34# * Colorized output
35# * Merging output from multiple RI data sources
36
37class RDoc::RI::Driver
38
39  ##
40  # Base Driver error class
41
42  class Error < RDoc::RI::Error; end
43
44  ##
45  # Raised when a name isn't found in the ri data stores
46
47  class NotFoundError < Error
48
49    ##
50    # Name that wasn't found
51
52    alias name message
53
54    def message # :nodoc:
55      "Nothing known about #{super}"
56    end
57  end
58
59  ##
60  # Show all method documentation following a class or module
61
62  attr_accessor :show_all
63
64  ##
65  # An RDoc::RI::Store for each entry in the RI path
66
67  attr_accessor :stores
68
69  ##
70  # Controls the user of the pager vs $stdout
71
72  attr_accessor :use_stdout
73
74  ##
75  # Default options for ri
76
77  def self.default_options
78    options = {}
79    options[:interactive] = false
80    options[:profile]     = false
81    options[:show_all]    = false
82    options[:use_cache]   = true
83    options[:use_stdout]  = !$stdout.tty?
84    options[:width]       = 72
85
86    # By default all standard paths are used.
87    options[:use_system]     = true
88    options[:use_site]       = true
89    options[:use_home]       = true
90    options[:use_gems]       = true
91    options[:extra_doc_dirs] = []
92
93    return options
94  end
95
96  ##
97  # Dump +data_path+ using pp
98
99  def self.dump data_path
100    require 'pp'
101
102    open data_path, 'rb' do |io|
103      pp Marshal.load(io.read)
104    end
105  end
106
107  ##
108  # Parses +argv+ and returns a Hash of options
109
110  def self.process_args argv
111    options = default_options
112
113    opts = OptionParser.new do |opt|
114      opt.accept File do |file,|
115        File.readable?(file) and not File.directory?(file) and file
116      end
117
118      opt.program_name = File.basename $0
119      opt.version = RDoc::VERSION
120      opt.release = nil
121      opt.summary_indent = ' ' * 4
122
123      opt.banner = <<-EOT
124Usage: #{opt.program_name} [options] [names...]
125
126Where name can be:
127
128  Class | Module | Module::Class
129
130  Class::method | Class#method | Class.method | method
131
132  gem_name: | gem_name:README | gem_name:History
133
134All class names may be abbreviated to their minimum unambiguous form. If a name
135is ambiguous, all valid options will be listed.
136
137A '.' matches either class or instance methods, while #method
138matches only instance and ::method matches only class methods.
139
140README and other files may be displayed by prefixing them with the gem name
141they're contained in.  If the gem name is followed by a ':' all files in the
142gem will be shown.  The file name extension may be omitted where it is
143unambiguous.
144
145For example:
146
147    #{opt.program_name} Fil
148    #{opt.program_name} File
149    #{opt.program_name} File.new
150    #{opt.program_name} zip
151    #{opt.program_name} rdoc:README
152
153Note that shell quoting or escaping may be required for method names containing
154punctuation:
155
156    #{opt.program_name} 'Array.[]'
157    #{opt.program_name} compact\\!
158
159To see the default directories ri will search, run:
160
161    #{opt.program_name} --list-doc-dirs
162
163Specifying the --system, --site, --home, --gems or --doc-dir options will
164limit ri to searching only the specified directories.
165
166ri options may be set in the 'RI' environment variable.
167
168The ri pager can be set with the 'RI_PAGER' environment variable or the
169'PAGER' environment variable.
170      EOT
171
172      opt.separator nil
173      opt.separator "Options:"
174
175      opt.separator nil
176
177      opt.on("--[no-]interactive", "-i",
178             "In interactive mode you can repeatedly",
179             "look up methods with autocomplete.") do |interactive|
180        options[:interactive] = interactive
181      end
182
183      opt.separator nil
184
185      opt.on("--[no-]all", "-a",
186             "Show all documentation for a class or",
187             "module.") do |show_all|
188        options[:show_all] = show_all
189      end
190
191      opt.separator nil
192
193      opt.on("--[no-]list", "-l",
194             "List classes ri knows about.") do |list|
195        options[:list] = list
196      end
197
198      opt.separator nil
199
200      opt.on("--[no-]pager", "-T",
201             "Send output directly to stdout,",
202             "rather than to a pager.") do |use_pager|
203        options[:use_stdout] = !use_pager
204      end
205
206      opt.separator nil
207
208      opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger,
209             "Set the width of the output.") do |width|
210        options[:width] = width
211      end
212
213      opt.separator nil
214
215      opt.on("--server [PORT]", Integer,
216             "Run RDoc server on the given port.",
217             "The default port is 8214.") do |port|
218        options[:server] = port || 8214
219      end
220
221      opt.separator nil
222
223      formatters = RDoc::Markup.constants.grep(/^To[A-Z][a-z]+$/).sort
224      formatters = formatters.sort.map do |formatter|
225        formatter.to_s.sub('To', '').downcase
226      end
227      formatters -= %w[html label test] # remove useless output formats
228
229      opt.on("--format=NAME", "-f",
230             "Uses the selected formatter. The default",
231             "formatter is bs for paged output and ansi",
232             "otherwise. Valid formatters are:",
233             formatters.join(' '), formatters) do |value|
234        options[:formatter] = RDoc::Markup.const_get "To#{value.capitalize}"
235      end
236
237      opt.separator nil
238      opt.separator "Data source options:"
239      opt.separator nil
240
241      opt.on("--[no-]list-doc-dirs",
242             "List the directories from which ri will",
243             "source documentation on stdout and exit.") do |list_doc_dirs|
244        options[:list_doc_dirs] = list_doc_dirs
245      end
246
247      opt.separator nil
248
249      opt.on("--doc-dir=DIRNAME", "-d", Array,
250             "List of directories from which to source",
251             "documentation in addition to the standard",
252             "directories.  May be repeated.") do |value|
253        value.each do |dir|
254          unless File.directory? dir then
255            raise OptionParser::InvalidArgument, "#{dir} is not a directory"
256          end
257
258          options[:extra_doc_dirs] << File.expand_path(dir)
259        end
260      end
261
262      opt.separator nil
263
264      opt.on("--no-standard-docs",
265             "Do not include documentation from",
266             "the Ruby standard library, site_lib,",
267             "installed gems, or ~/.rdoc.",
268             "Use with --doc-dir") do
269        options[:use_system] = false
270        options[:use_site] = false
271        options[:use_gems] = false
272        options[:use_home] = false
273      end
274
275      opt.separator nil
276
277      opt.on("--[no-]system",
278             "Include documentation from Ruby's standard",
279             "library.  Defaults to true.") do |value|
280        options[:use_system] = value
281      end
282
283      opt.separator nil
284
285      opt.on("--[no-]site",
286             "Include documentation from libraries",
287             "installed in site_lib.",
288             "Defaults to true.") do |value|
289        options[:use_site] = value
290      end
291
292      opt.separator nil
293
294      opt.on("--[no-]gems",
295             "Include documentation from RubyGems.",
296             "Defaults to true.") do |value|
297        options[:use_gems] = value
298      end
299
300      opt.separator nil
301
302      opt.on("--[no-]home",
303             "Include documentation stored in ~/.rdoc.",
304             "Defaults to true.") do |value|
305        options[:use_home] = value
306      end
307
308      opt.separator nil
309      opt.separator "Debug options:"
310      opt.separator nil
311
312      opt.on("--[no-]profile",
313             "Run with the ruby profiler") do |value|
314        options[:profile] = value
315      end
316
317      opt.separator nil
318
319      opt.on("--dump=CACHE", File,
320             "Dumps data from an ri cache or data file") do |value|
321        options[:dump_path] = value
322      end
323    end
324
325    argv = ENV['RI'].to_s.split.concat argv
326
327    opts.parse! argv
328
329    options[:names] = argv
330
331    options[:use_stdout] ||= !$stdout.tty?
332    options[:use_stdout] ||= options[:interactive]
333    options[:width] ||= 72
334
335    options
336
337  rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
338    puts opts
339    puts
340    puts e
341    exit 1
342  end
343
344  ##
345  # Runs the ri command line executable using +argv+
346
347  def self.run argv = ARGV
348    options = process_args argv
349
350    if options[:dump_path] then
351      dump options[:dump_path]
352      return
353    end
354
355    ri = new options
356    ri.run
357  end
358
359  ##
360  # Creates a new driver using +initial_options+ from ::process_args
361
362  def initialize initial_options = {}
363    @paging = false
364    @classes = nil
365
366    options = self.class.default_options.update(initial_options)
367
368    @formatter_klass = options[:formatter]
369
370    require 'profile' if options[:profile]
371
372    @names = options[:names]
373    @list = options[:list]
374
375    @doc_dirs = []
376    @stores   = []
377
378    RDoc::RI::Paths.each(options[:use_system], options[:use_site],
379                         options[:use_home], options[:use_gems],
380                         *options[:extra_doc_dirs]) do |path, type|
381      @doc_dirs << path
382
383      store = RDoc::RI::Store.new path, type
384      store.load_cache
385      @stores << store
386    end
387
388    @list_doc_dirs = options[:list_doc_dirs]
389
390    @interactive = options[:interactive]
391    @server      = options[:server]
392    @use_stdout  = options[:use_stdout]
393    @show_all    = options[:show_all]
394
395    # pager process for jruby
396    @jruby_pager_process = nil
397  end
398
399  ##
400  # Adds paths for undocumented classes +also_in+ to +out+
401
402  def add_also_in out, also_in
403    return if also_in.empty?
404
405    out << RDoc::Markup::Rule.new(1)
406    out << RDoc::Markup::Paragraph.new("Also found in:")
407
408    paths = RDoc::Markup::Verbatim.new
409    also_in.each do |store|
410      paths.parts.push store.friendly_path, "\n"
411    end
412    out << paths
413  end
414
415  ##
416  # Adds a class header to +out+ for class +name+ which is described in
417  # +classes+.
418
419  def add_class out, name, classes
420    heading = if classes.all? { |klass| klass.module? } then
421                name
422              else
423                superclass = classes.map do |klass|
424                  klass.superclass unless klass.module?
425                end.compact.shift || 'Object'
426
427                superclass = superclass.full_name unless String === superclass
428
429                "#{name} < #{superclass}"
430              end
431
432    out << RDoc::Markup::Heading.new(1, heading)
433    out << RDoc::Markup::BlankLine.new
434  end
435
436  ##
437  # Adds "(from ...)" to +out+ for +store+
438
439  def add_from out, store
440    out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
441  end
442
443  ##
444  # Adds +extends+ to +out+
445
446  def add_extends out, extends
447    add_extension_modules out, 'Extended by', extends
448  end
449
450  ##
451  # Adds a list of +extensions+ to this module of the given +type+ to +out+.
452  # add_includes and add_extends call this, so you should use those directly.
453
454  def add_extension_modules out, type, extensions
455    return if extensions.empty?
456
457    out << RDoc::Markup::Rule.new(1)
458    out << RDoc::Markup::Heading.new(1, "#{type}:")
459
460    extensions.each do |modules, store|
461      if modules.length == 1 then
462        include = modules.first
463        name = include.name
464        path = store.friendly_path
465        out << RDoc::Markup::Paragraph.new("#{name} (from #{path})")
466
467        if include.comment then
468          out << RDoc::Markup::BlankLine.new
469          out << include.comment
470        end
471      else
472        out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
473
474        wout, with = modules.partition { |incl| incl.comment.empty? }
475
476        out << RDoc::Markup::BlankLine.new unless with.empty?
477
478        with.each do |incl|
479          out << RDoc::Markup::Paragraph.new(incl.name)
480          out << RDoc::Markup::BlankLine.new
481          out << incl.comment
482        end
483
484        unless wout.empty? then
485          verb = RDoc::Markup::Verbatim.new
486
487          wout.each do |incl|
488            verb.push incl.name, "\n"
489          end
490
491          out << verb
492        end
493      end
494    end
495  end
496
497  ##
498  # Adds +includes+ to +out+
499
500  def add_includes out, includes
501    add_extension_modules out, 'Includes', includes
502  end
503
504  ##
505  # Looks up the method +name+ and adds it to +out+
506
507  def add_method out, name
508    filtered   = lookup_method name
509
510    method_out = method_document name, filtered
511
512    out.concat method_out.parts
513  end
514
515  ##
516  # Adds documentation for all methods in +klass+ to +out+
517
518  def add_method_documentation out, klass
519    klass.method_list.each do |method|
520      begin
521        add_method out, method.full_name
522      rescue NotFoundError
523        next
524      end
525    end
526  end
527
528  ##
529  # Adds a list of +methods+ to +out+ with a heading of +name+
530
531  def add_method_list out, methods, name
532    return if methods.empty?
533
534    out << RDoc::Markup::Heading.new(1, "#{name}:")
535    out << RDoc::Markup::BlankLine.new
536
537    if @use_stdout and !@interactive then
538      out.concat methods.map { |method|
539        RDoc::Markup::Verbatim.new method
540      }
541    else
542      out << RDoc::Markup::IndentedParagraph.new(2, methods.join(', '))
543    end
544
545    out << RDoc::Markup::BlankLine.new
546  end
547
548  ##
549  # Returns ancestor classes of +klass+
550
551  def ancestors_of klass
552    ancestors = []
553
554    unexamined = [klass]
555    seen = []
556
557    loop do
558      break if unexamined.empty?
559      current = unexamined.shift
560      seen << current
561
562      stores = classes[current]
563
564      break unless stores and not stores.empty?
565
566      klasses = stores.map do |store|
567        store.ancestors[current]
568      end.flatten.uniq
569
570      klasses = klasses - seen
571
572      ancestors.concat klasses
573      unexamined.concat klasses
574    end
575
576    ancestors.reverse
577  end
578
579  ##
580  # For RubyGems backwards compatibility
581
582  def class_cache # :nodoc:
583  end
584
585  ##
586  # Builds a RDoc::Markup::Document from +found+, +klasess+ and +includes+
587
588  def class_document name, found, klasses, includes, extends
589    also_in = []
590
591    out = RDoc::Markup::Document.new
592
593    add_class out, name, klasses
594
595    add_includes out, includes
596    add_extends  out, extends
597
598    found.each do |store, klass|
599      comment = klass.comment
600      # TODO the store's cache should always return an empty Array
601      class_methods    = store.class_methods[klass.full_name]    || []
602      instance_methods = store.instance_methods[klass.full_name] || []
603      attributes       = store.attributes[klass.full_name]       || []
604
605      if comment.empty? and
606         instance_methods.empty? and class_methods.empty? then
607        also_in << store
608        next
609      end
610
611      add_from out, store
612
613      unless comment.empty? then
614        out << RDoc::Markup::Rule.new(1)
615
616        if comment.merged? then
617          parts = comment.parts
618          parts = parts.zip [RDoc::Markup::BlankLine.new] * parts.length
619          parts.flatten!
620          parts.pop
621
622          out.concat parts
623        else
624          out << comment
625        end
626      end
627
628      if class_methods or instance_methods or not klass.constants.empty? then
629        out << RDoc::Markup::Rule.new(1)
630      end
631
632      unless klass.constants.empty? then
633        out << RDoc::Markup::Heading.new(1, "Constants:")
634        out << RDoc::Markup::BlankLine.new
635        list = RDoc::Markup::List.new :NOTE
636
637        constants = klass.constants.sort_by { |constant| constant.name }
638
639        list.items.concat constants.map { |constant|
640          parts = constant.comment.parts if constant.comment
641          parts << RDoc::Markup::Paragraph.new('[not documented]') if
642            parts.empty?
643
644          RDoc::Markup::ListItem.new(constant.name, *parts)
645        }
646
647        out << list
648        out << RDoc::Markup::BlankLine.new
649      end
650
651      add_method_list out, class_methods,    'Class methods'
652      add_method_list out, instance_methods, 'Instance methods'
653      add_method_list out, attributes,       'Attributes'
654
655      add_method_documentation out, klass if @show_all
656    end
657
658    add_also_in out, also_in
659
660    out
661  end
662
663  ##
664  # Hash mapping a known class or module to the stores it can be loaded from
665
666  def classes
667    return @classes if @classes
668
669    @classes = {}
670
671    @stores.each do |store|
672      store.cache[:modules].each do |mod|
673        # using default block causes searched-for modules to be added
674        @classes[mod] ||= []
675        @classes[mod] << store
676      end
677    end
678
679    @classes
680  end
681
682  ##
683  # Returns the stores wherein +name+ is found along with the classes,
684  # extends and includes that match it
685
686  def classes_and_includes_and_extends_for name
687    klasses = []
688    extends = []
689    includes = []
690
691    found = @stores.map do |store|
692      begin
693        klass = store.load_class name
694        klasses  << klass
695        extends  << [klass.extends,  store] if klass.extends
696        includes << [klass.includes, store] if klass.includes
697        [store, klass]
698      rescue RDoc::Store::MissingFileError
699      end
700    end.compact
701
702    extends.reject!  do |modules,| modules.empty? end
703    includes.reject! do |modules,| modules.empty? end
704
705    [found, klasses, includes, extends]
706  end
707
708  ##
709  # Completes +name+ based on the caches.  For Readline
710
711  def complete name
712    klasses = classes.keys
713    completions = []
714
715    klass, selector, method = parse_name name
716
717    # may need to include Foo when given Foo::
718    klass_name = method ? name : klass
719
720    if name !~ /#|\./ then
721      completions = klasses.grep(/^#{Regexp.escape klass_name}[^:]*$/)
722      completions.concat klasses.grep(/^#{Regexp.escape name}[^:]*$/) if
723        name =~ /::$/
724
725      completions << klass if classes.key? klass # to complete a method name
726    elsif selector then
727      completions << klass if classes.key? klass
728    elsif classes.key? klass_name then
729      completions << klass_name
730    end
731
732    if completions.include? klass and name =~ /#|\.|::/ then
733      methods = list_methods_matching name
734
735      if not methods.empty? then
736        # remove Foo if given Foo:: and a method was found
737        completions.delete klass
738      elsif selector then
739        # replace Foo with Foo:: as given
740        completions.delete klass
741        completions << "#{klass}#{selector}"
742      end
743
744      completions.concat methods
745    end
746
747    completions.sort.uniq
748  end
749
750  ##
751  # Converts +document+ to text and writes it to the pager
752
753  def display document
754    page do |io|
755      text = document.accept formatter(io)
756
757      io.write text
758    end
759  end
760
761  ##
762  # Outputs formatted RI data for class +name+.  Groups undocumented classes
763
764  def display_class name
765    return if name =~ /#|\./
766
767    found, klasses, includes, extends =
768      classes_and_includes_and_extends_for name
769
770    return if found.empty?
771
772    out = class_document name, found, klasses, includes, extends
773
774    display out
775  end
776
777  ##
778  # Outputs formatted RI data for method +name+
779
780  def display_method name
781    out = RDoc::Markup::Document.new
782
783    add_method out, name
784
785    display out
786  end
787
788  ##
789  # Outputs formatted RI data for the class or method +name+.
790  #
791  # Returns true if +name+ was found, false if it was not an alternative could
792  # be guessed, raises an error if +name+ couldn't be guessed.
793
794  def display_name name
795    if name =~ /\w:(\w|$)/ then
796      display_page name
797      return true
798    end
799
800    return true if display_class name
801
802    display_method name if name =~ /::|#|\./
803
804    true
805  rescue NotFoundError
806    matches = list_methods_matching name if name =~ /::|#|\./
807    matches = classes.keys.grep(/^#{name}/) if matches.empty?
808
809    raise if matches.empty?
810
811    page do |io|
812      io.puts "#{name} not found, maybe you meant:"
813      io.puts
814      io.puts matches.sort.join("\n")
815    end
816
817    false
818  end
819
820  ##
821  # Displays each name in +name+
822
823  def display_names names
824    names.each do |name|
825      name = expand_name name
826
827      display_name name
828    end
829  end
830
831  ##
832  # Outputs formatted RI data for page +name+.
833
834  def display_page name
835    store_name, page_name = name.split ':', 2
836
837    store = @stores.find { |s| s.source == store_name }
838
839    return display_page_list store if page_name.empty?
840
841    pages = store.cache[:pages]
842
843    unless pages.include? page_name then
844      found_names = pages.select do |n|
845        n =~ /#{Regexp.escape page_name}\.[^.]+$/
846      end
847
848      if found_names.length.zero? then
849        return display_page_list store, pages
850      elsif found_names.length > 1 then
851        return display_page_list store, found_names, page_name
852      end
853
854      page_name = found_names.first
855    end
856
857    page = store.load_page page_name
858
859    display page.comment
860  end
861
862  ##
863  # Outputs a formatted RI page list for the pages in +store+.
864
865  def display_page_list store, pages = store.cache[:pages], search = nil
866    out = RDoc::Markup::Document.new
867
868    title = if search then
869              "#{search} pages"
870            else
871              'Pages'
872            end
873
874    out << RDoc::Markup::Heading.new(1, "#{title} in #{store.friendly_path}")
875    out << RDoc::Markup::BlankLine.new
876
877    list = RDoc::Markup::List.new(:BULLET)
878
879    pages.each do |page|
880      list << RDoc::Markup::Paragraph.new(page)
881    end
882
883    out << list
884
885    display out
886  end
887
888  ##
889  # Expands abbreviated klass +klass+ into a fully-qualified class.  "Zl::Da"
890  # will be expanded to Zlib::DataError.
891
892  def expand_class klass
893    klass.split('::').inject '' do |expanded, klass_part|
894      expanded << '::' unless expanded.empty?
895      short = expanded << klass_part
896
897      subset = classes.keys.select do |klass_name|
898        klass_name =~ /^#{expanded}[^:]*$/
899      end
900
901      abbrevs = Abbrev.abbrev subset
902
903      expanded = abbrevs[short]
904
905      raise NotFoundError, short unless expanded
906
907      expanded.dup
908    end
909  end
910
911  ##
912  # Expands the class portion of +name+ into a fully-qualified class.  See
913  # #expand_class.
914
915  def expand_name name
916    klass, selector, method = parse_name name
917
918    return [selector, method].join if klass.empty?
919
920    case selector
921    when ':' then
922      [find_store(klass),   selector, method]
923    else
924      [expand_class(klass), selector, method]
925    end.join
926  end
927
928  ##
929  # Filters the methods in +found+ trying to find a match for +name+.
930
931  def filter_methods found, name
932    regexp = name_regexp name
933
934    filtered = found.find_all do |store, methods|
935      methods.any? { |method| method.full_name =~ regexp }
936    end
937
938    return filtered unless filtered.empty?
939
940    found
941  end
942
943  ##
944  # Yields items matching +name+ including the store they were found in, the
945  # class being searched for, the class they were found in (an ancestor) the
946  # types of methods to look up (from #method_type), and the method name being
947  # searched for
948
949  def find_methods name
950    klass, selector, method = parse_name name
951
952    types = method_type selector
953
954    klasses = nil
955    ambiguous = klass.empty?
956
957    if ambiguous then
958      klasses = classes.keys
959    else
960      klasses = ancestors_of klass
961      klasses.unshift klass
962    end
963
964    methods = []
965
966    klasses.each do |ancestor|
967      ancestors = classes[ancestor]
968
969      next unless ancestors
970
971      klass = ancestor if ambiguous
972
973      ancestors.each do |store|
974        methods << [store, klass, ancestor, types, method]
975      end
976    end
977
978    methods = methods.sort_by do |_, k, a, _, m|
979      [k, a, m].compact
980    end
981
982    methods.each do |item|
983      yield(*item) # :yields: store, klass, ancestor, types, method
984    end
985
986    self
987  end
988
989  ##
990  # Finds the given +pager+ for jruby.  Returns an IO if +pager+ was found.
991  #
992  # Returns false if +pager+ does not exist.
993  #
994  # Returns nil if the jruby JVM doesn't support ProcessBuilder redirection
995  # (1.6 and older).
996
997  def find_pager_jruby pager
998    require 'java'
999    require 'shellwords'
1000
1001    return nil unless java.lang.ProcessBuilder.constants.include? :Redirect
1002
1003    pager = Shellwords.split pager
1004
1005    pb = java.lang.ProcessBuilder.new(*pager)
1006    pb = pb.redirect_output java.lang.ProcessBuilder::Redirect::INHERIT
1007
1008    @jruby_pager_process = pb.start
1009
1010    input = @jruby_pager_process.output_stream
1011
1012    io = input.to_io
1013    io.sync = true
1014    io
1015  rescue java.io.IOException
1016    false
1017  end
1018
1019  ##
1020  # Finds a store that matches +name+ which can be the name of a gem, "ruby",
1021  # "home" or "site".
1022  #
1023  # See also RDoc::Store#source
1024
1025  def find_store name
1026    @stores.each do |store|
1027      source = store.source
1028
1029      return source if source == name
1030
1031      return source if
1032        store.type == :gem and source =~ /^#{Regexp.escape name}-\d/
1033    end
1034
1035    raise RDoc::RI::Driver::NotFoundError, name
1036  end
1037
1038  ##
1039  # Creates a new RDoc::Markup::Formatter.  If a formatter is given with -f,
1040  # use it.  If we're outputting to a pager, use bs, otherwise ansi.
1041
1042  def formatter(io)
1043    if @formatter_klass then
1044      @formatter_klass.new
1045    elsif paging? or !io.tty? then
1046      RDoc::Markup::ToBs.new
1047    else
1048      RDoc::Markup::ToAnsi.new
1049    end
1050  end
1051
1052  ##
1053  # Runs ri interactively using Readline if it is available.
1054
1055  def interactive
1056    puts "\nEnter the method name you want to look up."
1057
1058    if defined? Readline then
1059      Readline.completion_proc = method :complete
1060      puts "You can use tab to autocomplete."
1061    end
1062
1063    puts "Enter a blank line to exit.\n\n"
1064
1065    loop do
1066      name = if defined? Readline then
1067               Readline.readline ">> "
1068             else
1069               print ">> "
1070               $stdin.gets
1071             end
1072
1073      return if name.nil? or name.empty?
1074
1075      name = expand_name name.strip
1076
1077      begin
1078        display_name name
1079      rescue NotFoundError => e
1080        puts e.message
1081      end
1082    end
1083
1084  rescue Interrupt
1085    exit
1086  end
1087
1088  ##
1089  # Is +file+ in ENV['PATH']?
1090
1091  def in_path? file
1092    return true if file =~ %r%\A/% and File.exist? file
1093
1094    ENV['PATH'].split(File::PATH_SEPARATOR).any? do |path|
1095      File.exist? File.join(path, file)
1096    end
1097  end
1098
1099  ##
1100  # Lists classes known to ri starting with +names+.  If +names+ is empty all
1101  # known classes are shown.
1102
1103  def list_known_classes names = []
1104    classes = []
1105
1106    stores.each do |store|
1107      classes << store.module_names
1108    end
1109
1110    classes = classes.flatten.uniq.sort
1111
1112    unless names.empty? then
1113      filter = Regexp.union names.map { |name| /^#{name}/ }
1114
1115      classes = classes.grep filter
1116    end
1117
1118    page do |io|
1119      if paging? or io.tty? then
1120        if names.empty? then
1121          io.puts "Classes and Modules known to ri:"
1122        else
1123          io.puts "Classes and Modules starting with #{names.join ', '}:"
1124        end
1125        io.puts
1126      end
1127
1128      io.puts classes.join("\n")
1129    end
1130  end
1131
1132  ##
1133  # Returns an Array of methods matching +name+
1134
1135  def list_methods_matching name
1136    found = []
1137
1138    find_methods name do |store, klass, ancestor, types, method|
1139      if types == :instance or types == :both then
1140        methods = store.instance_methods[ancestor]
1141
1142        if methods then
1143          matches = methods.grep(/^#{Regexp.escape method.to_s}/)
1144
1145          matches = matches.map do |match|
1146            "#{klass}##{match}"
1147          end
1148
1149          found.concat matches
1150        end
1151      end
1152
1153      if types == :class or types == :both then
1154        methods = store.class_methods[ancestor]
1155
1156        next unless methods
1157        matches = methods.grep(/^#{Regexp.escape method.to_s}/)
1158
1159        matches = matches.map do |match|
1160          "#{klass}::#{match}"
1161        end
1162
1163        found.concat matches
1164      end
1165    end
1166
1167    found.uniq
1168  end
1169
1170  ##
1171  # Loads RI data for method +name+ on +klass+ from +store+.  +type+ and
1172  # +cache+ indicate if it is a class or instance method.
1173
1174  def load_method store, cache, klass, type, name
1175    methods = store.send(cache)[klass]
1176
1177    return unless methods
1178
1179    method = methods.find do |method_name|
1180      method_name == name
1181    end
1182
1183    return unless method
1184
1185    store.load_method klass, "#{type}#{method}"
1186  end
1187
1188  ##
1189  # Returns an Array of RI data for methods matching +name+
1190
1191  def load_methods_matching name
1192    found = []
1193
1194    find_methods name do |store, klass, ancestor, types, method|
1195      methods = []
1196
1197      methods << load_method(store, :class_methods, ancestor, '::',  method) if
1198        [:class, :both].include? types
1199
1200      methods << load_method(store, :instance_methods, ancestor, '#',  method) if
1201        [:instance, :both].include? types
1202
1203      found << [store, methods.compact]
1204    end
1205
1206    found.reject do |path, methods| methods.empty? end
1207  end
1208
1209  ##
1210  # Returns a filtered list of methods matching +name+
1211
1212  def lookup_method name
1213    found = load_methods_matching name
1214
1215    raise NotFoundError, name if found.empty?
1216
1217    filter_methods found, name
1218  end
1219
1220  ##
1221  # Builds a RDoc::Markup::Document from +found+, +klasses+ and +includes+
1222
1223  def method_document name, filtered
1224    out = RDoc::Markup::Document.new
1225
1226    out << RDoc::Markup::Heading.new(1, name)
1227    out << RDoc::Markup::BlankLine.new
1228
1229    filtered.each do |store, methods|
1230      methods.each do |method|
1231        out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})")
1232
1233        unless name =~ /^#{Regexp.escape method.parent_name}/ then
1234          out << RDoc::Markup::Heading.new(3, "Implementation from #{method.parent_name}")
1235        end
1236
1237        out << RDoc::Markup::Rule.new(1)
1238
1239        if method.arglists then
1240          arglists = method.arglists.chomp.split "\n"
1241          arglists = arglists.map { |line| line + "\n" }
1242          out << RDoc::Markup::Verbatim.new(*arglists)
1243          out << RDoc::Markup::Rule.new(1)
1244        end
1245
1246        if method.respond_to?(:superclass_method) and method.superclass_method
1247          out << RDoc::Markup::BlankLine.new
1248          out << RDoc::Markup::Heading.new(4, "(Uses superclass method #{method.superclass_method})")
1249          out << RDoc::Markup::Rule.new(1)
1250        end
1251
1252        out << RDoc::Markup::BlankLine.new
1253        out << method.comment
1254        out << RDoc::Markup::BlankLine.new
1255      end
1256    end
1257
1258    out
1259  end
1260
1261  ##
1262  # Returns the type of method (:both, :instance, :class) for +selector+
1263
1264  def method_type selector
1265    case selector
1266    when '.', nil then :both
1267    when '#'      then :instance
1268    else               :class
1269    end
1270  end
1271
1272  ##
1273  # Returns a regular expression for +name+ that will match an
1274  # RDoc::AnyMethod's name.
1275
1276  def name_regexp name
1277    klass, type, name = parse_name name
1278
1279    case type
1280    when '#', '::' then
1281      /^#{klass}#{type}#{Regexp.escape name}$/
1282    else
1283      /^#{klass}(#|::)#{Regexp.escape name}$/
1284    end
1285  end
1286
1287  ##
1288  # Paginates output through a pager program.
1289
1290  def page
1291    if pager = setup_pager then
1292      begin
1293        yield pager
1294      ensure
1295        pager.close
1296        @jruby_pager_process.wait_for if @jruby_pager_process
1297      end
1298    else
1299      yield $stdout
1300    end
1301  rescue Errno::EPIPE
1302  ensure
1303    @paging = false
1304  end
1305
1306  ##
1307  # Are we using a pager?
1308
1309  def paging?
1310    @paging
1311  end
1312
1313  ##
1314  # Extracts the class, selector and method name parts from +name+ like
1315  # Foo::Bar#baz.
1316  #
1317  # NOTE: Given Foo::Bar, Bar is considered a class even though it may be a
1318  # method
1319
1320  def parse_name name
1321    parts = name.split(/(::?|#|\.)/)
1322
1323    if parts.length == 1 then
1324      if parts.first =~ /^[a-z]|^([%&*+\/<>^`|~-]|\+@|-@|<<|<=>?|===?|=>|=~|>>|\[\]=?|~@)$/ then
1325        type = '.'
1326        meth = parts.pop
1327      else
1328        type = nil
1329        meth = nil
1330      end
1331    elsif parts.length == 2 or parts.last =~ /::|#|\./ then
1332      type = parts.pop
1333      meth = nil
1334    elsif parts[1] == ':' then
1335      klass = parts.shift
1336      type  = parts.shift
1337      meth  = parts.join
1338    elsif parts[-2] != '::' or parts.last !~ /^[A-Z]/ then
1339      meth = parts.pop
1340      type = parts.pop
1341    end
1342
1343    klass ||= parts.join
1344
1345    [klass, type, meth]
1346  end
1347
1348  ##
1349  # Looks up and displays ri data according to the options given.
1350
1351  def run
1352    if @list_doc_dirs then
1353      puts @doc_dirs
1354    elsif @list then
1355      list_known_classes @names
1356    elsif @server then
1357      start_server
1358    elsif @interactive or @names.empty? then
1359      interactive
1360    else
1361      display_names @names
1362    end
1363  rescue NotFoundError => e
1364    abort e.message
1365  end
1366
1367  ##
1368  # Sets up a pager program to pass output through.  Tries the RI_PAGER and
1369  # PAGER environment variables followed by pager, less then more.
1370
1371  def setup_pager
1372    return if @use_stdout
1373
1374    jruby = Object.const_defined?(:RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
1375
1376    pagers = [ENV['RI_PAGER'], ENV['PAGER'], 'pager', 'less', 'more']
1377
1378    pagers.compact.uniq.each do |pager|
1379      next unless pager
1380
1381      pager_cmd = pager.split.first
1382
1383      next unless in_path? pager_cmd
1384
1385      if jruby then
1386        case io = find_pager_jruby(pager)
1387        when nil   then break
1388        when false then next
1389        else            io
1390        end
1391      else
1392        io = IO.popen(pager, 'w') rescue next
1393      end
1394
1395      next if $? and $?.pid == io.pid and $?.exited? # pager didn't work
1396
1397      @paging = true
1398
1399      return io
1400    end
1401
1402    @use_stdout = true
1403
1404    nil
1405  end
1406
1407  ##
1408  # Starts a WEBrick server for ri.
1409
1410  def start_server
1411    require 'webrick'
1412
1413    server = WEBrick::HTTPServer.new :Port => @server
1414
1415    server.mount '/', RDoc::Servlet
1416
1417    trap 'INT'  do server.shutdown end
1418    trap 'TERM' do server.shutdown end
1419
1420    server.start
1421  end
1422
1423end
1424
1425