require 'abbrev' require 'optparse' begin require 'readline' rescue LoadError end begin require 'win32console' rescue LoadError end require 'rdoc/ri' require 'rdoc/ri/paths' require 'rdoc/markup' require 'rdoc/markup/formatter' require 'rdoc/text' ## # For RubyGems backwards compatibility require 'rdoc/ri/formatter' ## # The RI driver implements the command-line ri tool. # # The driver supports: # * loading RI data from: # * Ruby's standard library # * RubyGems # * ~/.rdoc # * A user-supplied directory # * Paging output (uses RI_PAGER environment variable, PAGER environment # variable or the less, more and pager programs) # * Interactive mode with tab-completion # * Abbreviated names (ri Zl shows Zlib documentation) # * Colorized output # * Merging output from multiple RI data sources class RDoc::RI::Driver ## # Base Driver error class class Error < RDoc::RI::Error; end ## # Raised when a name isn't found in the ri data stores class NotFoundError < Error ## # Name that wasn't found alias name message def message # :nodoc: "Nothing known about #{super}" end end ## # An RDoc::RI::Store for each entry in the RI path attr_accessor :stores ## # Controls the user of the pager vs $stdout attr_accessor :use_stdout ## # Default options for ri def self.default_options options = {} options[:use_stdout] = !$stdout.tty? options[:width] = 72 options[:interactive] = false options[:use_cache] = true options[:profile] = false # By default all standard paths are used. options[:use_system] = true options[:use_site] = true options[:use_home] = true options[:use_gems] = true options[:extra_doc_dirs] = [] return options end ## # Dump +data_path+ using pp def self.dump data_path require 'pp' open data_path, 'rb' do |io| pp Marshal.load(io.read) end end ## # Parses +argv+ and returns a Hash of options def self.process_args argv options = default_options opts = OptionParser.new do |opt| opt.accept File do |file,| File.readable?(file) and not File.directory?(file) and file end opt.program_name = File.basename $0 opt.version = RDoc::VERSION opt.release = nil opt.summary_indent = ' ' * 4 opt.banner = <<-EOT Usage: #{opt.program_name} [options] [names...] Where name can be: Class | Class::method | Class#method | Class.method | method All class names may be abbreviated to their minimum unambiguous form. If a name is ambiguous, all valid options will be listed. A '.' matches either class or instance methods, while #method matches only instance and ::method matches only class methods. For example: #{opt.program_name} Fil #{opt.program_name} File #{opt.program_name} File.new #{opt.program_name} zip Note that shell quoting or escaping may be required for method names containing punctuation: #{opt.program_name} 'Array.[]' #{opt.program_name} compact\\! To see the default directories ri will search, run: #{opt.program_name} --list-doc-dirs Specifying the --system, --site, --home, --gems or --doc-dir options will limit ri to searching only the specified directories. Options may also be set in the 'RI' environment variable. EOT opt.separator nil opt.separator "Options:" opt.separator nil formatters = RDoc::Markup.constants.grep(/^To[A-Z][a-z]+$/).sort formatters = formatters.sort.map do |formatter| formatter.to_s.sub('To', '').downcase end opt.on("--format=NAME", "-f", "Uses the selected formatter. The default", "formatter is bs for paged output and ansi", "otherwise. Valid formatters are:", formatters.join(' '), formatters) do |value| options[:formatter] = RDoc::Markup.const_get "To#{value.capitalize}" end opt.separator nil opt.on("--no-pager", "-T", "Send output directly to stdout,", "rather than to a pager.") do options[:use_stdout] = true end opt.separator nil opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger, "Set the width of the output.") do |value| options[:width] = value end opt.separator nil opt.on("--interactive", "-i", "In interactive mode you can repeatedly", "look up methods with autocomplete.") do options[:interactive] = true end opt.separator nil opt.on("--list", "-l", "List classes ri knows about.") do options[:list] = true end opt.separator nil opt.on("--[no-]profile", "Run with the ruby profiler") do |value| options[:profile] = value end opt.separator nil opt.separator "Data source options:" opt.separator nil opt.on("--list-doc-dirs", "List the directories from which ri will", "source documentation on stdout and exit.") do options[:list_doc_dirs] = true end opt.separator nil opt.on("--doc-dir=DIRNAME", "-d", Array, "List of directories from which to source", "documentation in addition to the standard", "directories. May be repeated.") do |value| value.each do |dir| unless File.directory? dir then raise OptionParser::InvalidArgument, "#{dir} is not a directory" end options[:extra_doc_dirs] << File.expand_path(dir) end end opt.separator nil opt.on("--no-standard-docs", "Do not include documentation from", "the Ruby standard library, site_lib,", "installed gems, or ~/.rdoc.", "Use with --doc-dir") do options[:use_system] = false options[:use_site] = false options[:use_gems] = false options[:use_home] = false end opt.separator nil opt.on("--[no-]system", "Include documentation from Ruby's standard", "library. Defaults to true.") do |value| options[:use_system] = value end opt.separator nil opt.on("--[no-]site", "Include documentation from libraries", "installed in site_lib.", "Defaults to true.") do |value| options[:use_site] = value end opt.separator nil opt.on("--[no-]gems", "Include documentation from RubyGems.", "Defaults to true.") do |value| options[:use_gems] = value end opt.separator nil opt.on("--[no-]home", "Include documentation stored in ~/.rdoc.", "Defaults to true.") do |value| options[:use_home] = value end opt.separator nil opt.separator "Debug options:" opt.separator nil opt.on("--dump=CACHE", File, "Dumps data from an ri cache or data file") do |value| options[:dump_path] = value end end argv = ENV['RI'].to_s.split.concat argv opts.parse! argv options[:names] = argv options[:use_stdout] ||= !$stdout.tty? options[:use_stdout] ||= options[:interactive] options[:width] ||= 72 options rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e puts opts puts puts e exit 1 end ## # Runs the ri command line executable using +argv+ def self.run argv = ARGV options = process_args argv if options[:dump_path] then dump options[:dump_path] return end ri = new options ri.run end ## # Creates a new driver using +initial_options+ from ::process_args def initialize initial_options = {} @paging = false @classes = nil options = self.class.default_options.update(initial_options) @formatter_klass = options[:formatter] require 'profile' if options[:profile] @names = options[:names] @list = options[:list] @doc_dirs = [] @stores = [] RDoc::RI::Paths.each(options[:use_system], options[:use_site], options[:use_home], options[:use_gems], *options[:extra_doc_dirs]) do |path, type| @doc_dirs << path store = RDoc::RI::Store.new path, type store.load_cache @stores << store end @list_doc_dirs = options[:list_doc_dirs] @interactive = options[:interactive] @use_stdout = options[:use_stdout] end ## # Adds paths for undocumented classes +also_in+ to +out+ def add_also_in out, also_in return if also_in.empty? out << RDoc::Markup::Rule.new(1) out << RDoc::Markup::Paragraph.new("Also found in:") paths = RDoc::Markup::Verbatim.new also_in.each do |store| paths.parts.push store.friendly_path, "\n" end out << paths end ## # Adds a class header to +out+ for class +name+ which is described in # +classes+. def add_class out, name, classes heading = if classes.all? { |klass| klass.module? } then name else superclass = classes.map do |klass| klass.superclass unless klass.module? end.compact.shift || 'Object' superclass = superclass.full_name unless String === superclass "#{name} < #{superclass}" end out << RDoc::Markup::Heading.new(1, heading) out << RDoc::Markup::BlankLine.new end ## # Adds "(from ...)" to +out+ for +store+ def add_from out, store out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") end ## # Adds +includes+ to +out+ def add_includes out, includes return if includes.empty? out << RDoc::Markup::Rule.new(1) out << RDoc::Markup::Heading.new(1, "Includes:") includes.each do |modules, store| if modules.length == 1 then include = modules.first name = include.name path = store.friendly_path out << RDoc::Markup::Paragraph.new("#{name} (from #{path})") if include.comment then out << RDoc::Markup::BlankLine.new out << include.comment end else out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") wout, with = modules.partition { |incl| incl.comment.empty? } out << RDoc::Markup::BlankLine.new unless with.empty? with.each do |incl| out << RDoc::Markup::Paragraph.new(incl.name) out << RDoc::Markup::BlankLine.new out << incl.comment end unless wout.empty? then verb = RDoc::Markup::Verbatim.new wout.each do |incl| verb.push incl.name, "\n" end out << verb end end end end ## # Adds a list of +methods+ to +out+ with a heading of +name+ def add_method_list out, methods, name return if methods.empty? out << RDoc::Markup::Heading.new(1, "#{name}:") out << RDoc::Markup::BlankLine.new if @use_stdout and !@interactive out.push(*methods.map do |method| RDoc::Markup::Verbatim.new method end) else out << RDoc::Markup::IndentedParagraph.new(2, methods.join(', ')) end out << RDoc::Markup::BlankLine.new end ## # Returns ancestor classes of +klass+ def ancestors_of klass ancestors = [] unexamined = [klass] seen = [] loop do break if unexamined.empty? current = unexamined.shift seen << current stores = classes[current] break unless stores and not stores.empty? klasses = stores.map do |store| store.ancestors[current] end.flatten.uniq klasses = klasses - seen ancestors.push(*klasses) unexamined.push(*klasses) end ancestors.reverse end ## # For RubyGems backwards compatibility def class_cache # :nodoc: end ## # Builds a RDoc::Markup::Document from +found+, +klasess+ and +includes+ def class_document name, found, klasses, includes also_in = [] out = RDoc::Markup::Document.new add_class out, name, klasses add_includes out, includes found.each do |store, klass| comment = klass.comment # TODO the store's cache should always return an empty Array class_methods = store.class_methods[klass.full_name] || [] instance_methods = store.instance_methods[klass.full_name] || [] attributes = store.attributes[klass.full_name] || [] if comment.empty? and instance_methods.empty? and class_methods.empty? then also_in << store next end add_from out, store unless comment.empty? then out << RDoc::Markup::Rule.new(1) if comment.merged? then parts = comment.parts parts = parts.zip [RDoc::Markup::BlankLine.new] * parts.length parts.flatten! parts.pop out.push(*parts) else out << comment end end if class_methods or instance_methods or not klass.constants.empty? then out << RDoc::Markup::Rule.new(1) end unless klass.constants.empty? then out << RDoc::Markup::Heading.new(1, "Constants:") out << RDoc::Markup::BlankLine.new list = RDoc::Markup::List.new :NOTE constants = klass.constants.sort_by { |constant| constant.name } list.push(*constants.map do |constant| parts = constant.comment.parts if constant.comment parts << RDoc::Markup::Paragraph.new('[not documented]') if parts.empty? RDoc::Markup::ListItem.new(constant.name, *parts) end) out << list out << RDoc::Markup::BlankLine.new end add_method_list out, class_methods, 'Class methods' add_method_list out, instance_methods, 'Instance methods' add_method_list out, attributes, 'Attributes' end add_also_in out, also_in out end ## # Hash mapping a known class or module to the stores it can be loaded from def classes return @classes if @classes @classes = {} @stores.each do |store| store.cache[:modules].each do |mod| # using default block causes searched-for modules to be added @classes[mod] ||= [] @classes[mod] << store end end @classes end ## # Returns the stores wherin +name+ is found along with the classes and # includes that match it def classes_and_includes_for name klasses = [] includes = [] found = @stores.map do |store| begin klass = store.load_class name klasses << klass includes << [klass.includes, store] if klass.includes [store, klass] rescue Errno::ENOENT end end.compact includes.reject! do |modules,| modules.empty? end [found, klasses, includes] end ## # Completes +name+ based on the caches. For Readline def complete name klasses = classes.keys completions = [] klass, selector, method = parse_name name # may need to include Foo when given Foo:: klass_name = method ? name : klass if name !~ /#|\./ then completions = klasses.grep(/^#{Regexp.escape klass_name}[^:]*$/) completions.concat klasses.grep(/^#{Regexp.escape name}[^:]*$/) if name =~ /::$/ completions << klass if classes.key? klass # to complete a method name elsif selector then completions << klass if classes.key? klass elsif classes.key? klass_name then completions << klass_name end if completions.include? klass and name =~ /#|\.|::/ then methods = list_methods_matching name if not methods.empty? then # remove Foo if given Foo:: and a method was found completions.delete klass elsif selector then # replace Foo with Foo:: as given completions.delete klass completions << "#{klass}#{selector}" end completions.push(*methods) end completions.sort.uniq end ## # Converts +document+ to text and writes it to the pager def display document page do |io| text = document.accept formatter(io) io.write text end end ## # Outputs formatted RI data for class +name+. Groups undocumented classes def display_class name return if name =~ /#|\./ found, klasses, includes = classes_and_includes_for name return if found.empty? out = class_document name, found, klasses, includes display out end ## # Outputs formatted RI data for method +name+ def display_method name found = load_methods_matching name raise NotFoundError, name if found.empty? filtered = filter_methods found, name out = method_document name, filtered display out end ## # Outputs formatted RI data for the class or method +name+. # # Returns true if +name+ was found, false if it was not an alternative could # be guessed, raises an error if +name+ couldn't be guessed. def display_name name return true if display_class name display_method name if name =~ /::|#|\./ true rescue NotFoundError matches = list_methods_matching name if name =~ /::|#|\./ matches = classes.keys.grep(/^#{name}/) if matches.empty? raise if matches.empty? page do |io| io.puts "#{name} not found, maybe you meant:" io.puts io.puts matches.join("\n") end false end ## # Displays each name in +name+ def display_names names names.each do |name| name = expand_name name display_name name end end ## # Expands abbreviated klass +klass+ into a fully-qualified class. "Zl::Da" # will be expanded to Zlib::DataError. def expand_class klass klass.split('::').inject '' do |expanded, klass_part| expanded << '::' unless expanded.empty? short = expanded << klass_part subset = classes.keys.select do |klass_name| klass_name =~ /^#{expanded}[^:]*$/ end abbrevs = Abbrev.abbrev subset expanded = abbrevs[short] raise NotFoundError, short unless expanded expanded.dup end end ## # Expands the class portion of +name+ into a fully-qualified class. See # #expand_class. def expand_name name klass, selector, method = parse_name name return [selector, method].join if klass.empty? "#{expand_class klass}#{selector}#{method}" end ## # Filters the methods in +found+ trying to find a match for +name+. def filter_methods found, name regexp = name_regexp name filtered = found.find_all do |store, methods| methods.any? { |method| method.full_name =~ regexp } end return filtered unless filtered.empty? found end ## # Yields items matching +name+ including the store they were found in, the # class being searched for, the class they were found in (an ancestor) the # types of methods to look up (from #method_type), and the method name being # searched for def find_methods name klass, selector, method = parse_name name types = method_type selector klasses = nil ambiguous = klass.empty? if ambiguous then klasses = classes.keys else klasses = ancestors_of klass klasses.unshift klass end methods = [] klasses.each do |ancestor| ancestors = classes[ancestor] next unless ancestors klass = ancestor if ambiguous ancestors.each do |store| methods << [store, klass, ancestor, types, method] end end methods = methods.sort_by do |_, k, a, _, m| [k, a, m].compact end methods.each do |item| yield(*item) # :yields: store, klass, ancestor, types, method end self end ## # Creates a new RDoc::Markup::Formatter. If a formatter is given with -f, # use it. If we're outputting to a pager, use bs, otherwise ansi. def formatter(io) if @formatter_klass then @formatter_klass.new elsif paging? or !io.tty? then RDoc::Markup::ToBs.new else RDoc::Markup::ToAnsi.new end end ## # Runs ri interactively using Readline if it is available. def interactive puts "\nEnter the method name you want to look up." if defined? Readline then Readline.completion_proc = method :complete puts "You can use tab to autocomplete." end puts "Enter a blank line to exit.\n\n" loop do name = if defined? Readline then Readline.readline ">> " else print ">> " $stdin.gets end return if name.nil? or name.empty? name = expand_name name.strip begin display_name name rescue NotFoundError => e puts e.message end end rescue Interrupt exit end ## # Is +file+ in ENV['PATH']? def in_path? file return true if file =~ %r%\A/% and File.exist? file ENV['PATH'].split(File::PATH_SEPARATOR).any? do |path| File.exist? File.join(path, file) end end ## # Lists classes known to ri starting with +names+. If +names+ is empty all # known classes are shown. def list_known_classes names = [] classes = [] stores.each do |store| classes << store.modules end classes = classes.flatten.uniq.sort unless names.empty? then filter = Regexp.union names.map { |name| /^#{name}/ } classes = classes.grep filter end page do |io| if paging? or io.tty? then if names.empty? then io.puts "Classes and Modules known to ri:" else io.puts "Classes and Modules starting with #{names.join ', '}:" end io.puts end io.puts classes.join("\n") end end ## # Returns an Array of methods matching +name+ def list_methods_matching name found = [] find_methods name do |store, klass, ancestor, types, method| if types == :instance or types == :both then methods = store.instance_methods[ancestor] if methods then matches = methods.grep(/^#{Regexp.escape method.to_s}/) matches = matches.map do |match| "#{klass}##{match}" end found.push(*matches) end end if types == :class or types == :both then methods = store.class_methods[ancestor] next unless methods matches = methods.grep(/^#{Regexp.escape method.to_s}/) matches = matches.map do |match| "#{klass}::#{match}" end found.push(*matches) end end found.uniq end ## # Loads RI data for method +name+ on +klass+ from +store+. +type+ and # +cache+ indicate if it is a class or instance method. def load_method store, cache, klass, type, name methods = store.send(cache)[klass] return unless methods method = methods.find do |method_name| method_name == name end return unless method store.load_method klass, "#{type}#{method}" end ## # Returns an Array of RI data for methods matching +name+ def load_methods_matching name found = [] find_methods name do |store, klass, ancestor, types, method| methods = [] methods << load_method(store, :class_methods, ancestor, '::', method) if [:class, :both].include? types methods << load_method(store, :instance_methods, ancestor, '#', method) if [:instance, :both].include? types found << [store, methods.compact] end found.reject do |path, methods| methods.empty? end end ## # Builds a RDoc::Markup::Document from +found+, +klasess+ and +includes+ def method_document name, filtered out = RDoc::Markup::Document.new out << RDoc::Markup::Heading.new(1, name) out << RDoc::Markup::BlankLine.new filtered.each do |store, methods| methods.each do |method| out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") unless name =~ /^#{Regexp.escape method.parent_name}/ then out << RDoc::Markup::Heading.new(3, "Implementation from #{method.parent_name}") end out << RDoc::Markup::Rule.new(1) if method.arglists then arglists = method.arglists.chomp.split "\n" arglists = arglists.map { |line| line + "\n" } out << RDoc::Markup::Verbatim.new(*arglists) out << RDoc::Markup::Rule.new(1) end out << RDoc::Markup::BlankLine.new out << method.comment out << RDoc::Markup::BlankLine.new end end out end ## # Returns the type of method (:both, :instance, :class) for +selector+ def method_type selector case selector when '.', nil then :both when '#' then :instance else :class end end ## # Returns a regular expression for +name+ that will match an # RDoc::AnyMethod's name. def name_regexp name klass, type, name = parse_name name case type when '#', '::' then /^#{klass}#{type}#{Regexp.escape name}$/ else /^#{klass}(#|::)#{Regexp.escape name}$/ end end ## # Paginates output through a pager program. def page if pager = setup_pager then begin yield pager ensure pager.close end else yield $stdout end rescue Errno::EPIPE ensure @paging = false end ## # Are we using a pager? def paging? @paging end ## # Extracts the class, selector and method name parts from +name+ like # Foo::Bar#baz. # # NOTE: Given Foo::Bar, Bar is considered a class even though it may be a # method def parse_name name parts = name.split(/(::|#|\.)/) if parts.length == 1 then if parts.first =~ /^[a-z]|^([%&*+\/<>^`|~-]|\+@|-@|<<|<=>?|===?|=>|=~|>>|\[\]=?|~@)$/ then type = '.' meth = parts.pop else type = nil meth = nil end elsif parts.length == 2 or parts.last =~ /::|#|\./ then type = parts.pop meth = nil elsif parts[-2] != '::' or parts.last !~ /^[A-Z]/ then meth = parts.pop type = parts.pop end klass = parts.join [klass, type, meth] end ## # Looks up and displays ri data according to the options given. def run if @list_doc_dirs then puts @doc_dirs elsif @list then list_known_classes @names elsif @interactive or @names.empty? then interactive else display_names @names end rescue NotFoundError => e abort e.message end ## # Sets up a pager program to pass output through. Tries the RI_PAGER and # PAGER environment variables followed by pager, less then more. def setup_pager return if @use_stdout pagers = [ENV['RI_PAGER'], ENV['PAGER'], 'pager', 'less', 'more'] pagers.compact.uniq.each do |pager| next unless pager pager_cmd = pager.split.first next unless in_path? pager_cmd io = IO.popen(pager, 'w') rescue next next if $? and $?.exited? # pager didn't work @paging = true return io end @use_stdout = true nil end end