#!/usr/bin/env ruby # $Id$ # # Copyright (C) 2007, 2008 Rocky Bernstein # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA. # # Author:: Rocky Bernstein (mailto:rockyb@rubyforge.net) # # = linecache # A module to read and cache lines of a Ruby program. # == Version # :include:VERSION # == SYNOPSIS # # The LineCache module allows one to get any line from any file, # caching lines of the file on first access to the file. Although the # file may be any file, the common use is when the file is a Ruby # script since parsing of the file is done to figure out where the # statement boundaries are. # # The routines here may be is useful when a small random sets of lines # are read from a single file, in particular in a debugger to show # source lines. # # # require 'linecache19' # lines = LineCache::getlines('/tmp/myruby.rb') # # The following lines have same effect as the above. # $: << '/tmp' # Dir.chdir('/tmp') {lines = LineCache::getlines('myruby.rb') # # line = LineCache::getline('/tmp/myruby.rb', 6) # # Note lines[6] == line (if /tmp/myruby.rb has 6 lines) # # LineCache::clear_file_cache # LineCache::clear_file_cache('/tmp/myruby.rb') # LineCache::update_cache # Check for modifications of all cached files. # # Some parts of the interface is derived from the Python module of the # same name. # # Defining SCRIPT_LINES__ causes Ruby to cache the lines of files # it reads. The key the setting of __FILE__ at the time when Ruby does # its read. LineCache keeps a separate copy of the lines elsewhere # and never destroys SCRIPT_LINES__ SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__ require 'digest/sha1' require 'set' begin require 'rubygems' rescue LoadError end require 'tracelines19' # require 'ruby-debug' ; Debugger.start # = module LineCache # A module to read and cache lines of a Ruby program. module LineCache LineCacheInfo = Struct.new(:stat, :line_numbers, :lines, :path, :sha1) unless defined?(LineCacheInfo) # The file cache. The key is a name as would be given by Ruby for # __FILE__. The value is a LineCacheInfo object. @@file_cache = {} # Maps a string filename (a String) to a key in @@file_cache (a # String). # # One important use of @@file2file_remap is mapping the a full path # of a file into the name stored in @@file_cache or given by Ruby's # __FILE__. Applications such as those that get input from users, # may want canonicalize a file name before looking it up. This map # gives a way to do that. # # Another related use is when a template system is used. Here we'll # probably want to remap not only the file name but also line # ranges. Will probably use this for that, but I'm not sure. @@file2file_remap = {} @@file2file_remap_lines = {} # Clear the file cache entirely. def clear_file_cache() @@file_cache = {} @@file2file_remap = {} @@file2file_remap_lines = {} end module_function :clear_file_cache # Return an array of cached file names def cached_files() @@file_cache.keys end module_function :cached_files # Discard cache entries that are out of date. If +filename+ is +nil+ # all entries in the file cache +@@file_cache+ are checked. # If we don't have stat information about a file, which can happen # if the file was read from SCRIPT_LINES__ but no corresponding file # is found, it will be kept. Return a list of invalidated filenames. # nil is returned if a filename was given but not found cached. def checkcache(filename=nil, use_script_lines=false) if !filename filenames = @@file_cache.keys() elsif @@file_cache.member?(filename) filenames = [filename] else return nil end result = [] for filename in filenames next unless @@file_cache.member?(filename) path = @@file_cache[filename].path if File.exist?(path) cache_info = @@file_cache[filename].stat stat = File.stat(path) if stat && (cache_info.size != stat.size or cache_info.mtime != stat.mtime) result << filename update_cache(filename, use_script_lines) end end end return result end module_function :checkcache # Cache filename if it's not already cached. # Return the expanded filename for it in the cache # or nil if we can't find the file. def cache(filename, reload_on_change=false) if @@file_cache.member?(filename) checkcache(filename) if reload_on_change else update_cache(filename, true) end if @@file_cache.member?(filename) @@file_cache[filename].path else nil end end module_function :cache # Return true if filename is cached def cached?(filename) @@file_cache.member?(unmap_file(filename)) end module_function :cached? def cached_script?(filename) # In 1.8.6, the SCRIPT_LINES__ filename key can be unqualified # In 1.9.1 it's the fully qualified name if RUBY_VERSION < "1.9" SCRIPT_LINES__.member?(unmap_file(filename)) else SCRIPT_LINES__.member?(File.expand_path(unmap_file(filename))) end end module_function :cached_script? def empty?(filename) filename=unmap_file(filename) @@file_cache[filename].lines.empty? end module_function :empty? # Get line +line_number+ from file named +filename+. Return nil if # there was a problem. If a file named filename is not found, the # function will look for it in the $: array. # # Examples: # # lines = LineCache::getline('/tmp/myfile.rb') # # Same as above # $: << '/tmp' # lines = LineCache.getlines('myfile.rb') # def getline(filename, line_number, reload_on_change=true) filename = unmap_file(filename) filename, line_number = unmap_file_line(filename, line_number) lines = getlines(filename, reload_on_change) if lines and (1..lines.size) === line_number return lines[line_number-1] else return nil end end module_function :getline # Read lines of +filename+ and cache the results. However +filename+ was # previously cached use the results from the cache. Return nil # if we can't get lines def getlines(filename, reload_on_change=false) filename = unmap_file(filename) checkcache(filename) if reload_on_change if @@file_cache.member?(filename) return @@file_cache[filename].lines else update_cache(filename, true) return @@file_cache[filename].lines if @@file_cache.member?(filename) end end module_function :getlines # Return full filename path for filename def path(filename) filename = unmap_file(filename) return nil unless @@file_cache.member?(filename) @@file_cache[filename].path end module_function :path def remap_file(from_file, to_file) @@file2file_remap[to_file] = from_file end module_function :remap_file def remap_file_lines(from_file, to_file, range, start) range = (range..range) if range.is_a?(Fixnum) to_file = from_file unless to_file if @@file2file_remap_lines[to_file] # FIXME: need to check for overwriting ranges: whether # they intersect or one encompasses another. @@file2file_remap_lines[to_file] << [from_file, range, start] else @@file2file_remap_lines[to_file] = [[from_file, range, start]] end end module_function :remap_file_lines # Return SHA1 of filename. def sha1(filename) filename = unmap_file(filename) return nil unless @@file_cache.member?(filename) return @@file_cache[filename].sha1.hexdigest if @@file_cache[filename].sha1 sha1 = Digest::SHA1.new @@file_cache[filename].lines.each do |line| sha1 << line end @@file_cache[filename].sha1 = sha1 sha1.hexdigest end module_function :sha1 # Return the number of lines in filename def size(filename) filename = unmap_file(filename) return nil unless @@file_cache.member?(filename) @@file_cache[filename].lines.length end module_function :size # Return File.stat in the cache for filename. def stat(filename) return nil unless @@file_cache.member?(filename) @@file_cache[filename].stat end module_function :stat # Return an Array of breakpoints in filename. # The list will contain an entry for each distinct line event call # so it is possible (and possibly useful) for a line number appear more # than once. def trace_line_numbers(filename, reload_on_change=false) fullname = cache(filename, reload_on_change) return nil unless fullname e = @@file_cache[filename] unless e.line_numbers e.line_numbers = TraceLineNumbers.lnums_for_str_array(e.lines) e.line_numbers = false unless e.line_numbers end e.line_numbers end module_function :trace_line_numbers def unmap_file(file) @@file2file_remap[file] ? @@file2file_remap[file] : file end module_function :unmap_file def unmap_file_line(file, line) if @@file2file_remap_lines[file] @@file2file_remap_lines[file].each do |from_file, range, start| if range === line from_file = from_file || file return [from_file, start+line-range.begin] end end end return [file, line] end module_function :unmap_file_line # Update a cache entry. If something's # wrong, return nil. Return true if the cache was updated and false # if not. If use_script_lines is true, use that as the source for the # lines of the file def update_cache(filename, use_script_lines=false) return nil unless filename @@file_cache.delete(filename) path = File.expand_path(filename) if use_script_lines list = [filename] list << @@file2file_remap[path] if @@file2file_remap[path] list.each do |name| if !SCRIPT_LINES__[name].nil? && SCRIPT_LINES__[name] != true begin stat = File.stat(name) rescue stat = nil end lines = SCRIPT_LINES__[name] if "ruby19".respond_to?(:force_encoding) lines.each{|l| l.force_encoding(Encoding.default_external) } end @@file_cache[filename] = LineCacheInfo.new(stat, nil, lines, path, nil) @@file2file_remap[path] = filename return true end end end if File.exist?(path) stat = File.stat(path) elsif File.basename(filename) == filename # try looking through the search path. stat = nil for dirname in $: path = File.join(dirname, filename) if File.exist?(path) stat = File.stat(path) break end end return false unless stat end begin fp = File.open(path, 'r') lines = fp.readlines() fp.close() rescue ## print '*** cannot open', path, ':', msg return nil end @@file_cache[filename] = LineCacheInfo.new(File.stat(path), nil, lines, path, nil) @@file2file_remap[path] = filename return true end module_function :update_cache end # example usage if __FILE__ == $0 def yes_no(var) return var ? "" : "not " end lines = LineCache::getlines(__FILE__) puts "#{__FILE__} has #{LineCache.size(__FILE__)} lines" line = LineCache::getline(__FILE__, 6) puts "The 6th line is\n#{line}" line = LineCache::remap_file(__FILE__, 'another_name') puts LineCache::getline('another_name', 7) puts("Files cached: #{LineCache::cached_files.inspect}") LineCache::update_cache(__FILE__) LineCache::checkcache(__FILE__) puts "#{__FILE__} has #{LineCache::size(__FILE__)} lines" puts "#{__FILE__} trace line numbers:\n" + "#{LineCache::trace_line_numbers(__FILE__).to_a.sort.inspect}" puts("#{__FILE__} is %scached." % yes_no(LineCache::cached?(__FILE__))) puts LineCache::stat(__FILE__).inspect puts "Full path: #{LineCache::path(__FILE__)}" LineCache::checkcache # Check all files in the cache LineCache::clear_file_cache puts("#{__FILE__} is now %scached." % yes_no(LineCache::cached?(__FILE__))) digest = SCRIPT_LINES__.select{|k,v| k =~ /digest.rb$/} puts digest.first[0] if digest line = LineCache::getline(__FILE__, 7) puts "The 7th line is\n#{line}" LineCache::remap_file_lines(__FILE__, 'test2', (10..20), 6) puts LineCache::getline('test2', 10) puts "Remapped 10th line of test2 is\n#{line}" end