├── History.txt ├── Manifest.txt ├── Rakefile ├── lib ├── sexpwrap.rb └── loadwrap.rb ├── README.txt └── test └── test_loadwrap.rb /History.txt: -------------------------------------------------------------------------------- 1 | === 1.0.0 / 2009-06-30 2 | 3 | * 1 major enhancement 4 | 5 | * Birthday! 6 | 7 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | .autotest 2 | History.txt 3 | Manifest.txt 4 | README.txt 5 | Rakefile 6 | lib/loadwrap.rb 7 | lib/sexpwrap.rb 8 | test/test_loadwrap.rb 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'rubygems' 4 | require 'hoe' 5 | 6 | Hoe.spec 'loadwrap' do 7 | developer "Lars Christensen", "larsch@belunktum.dk" 8 | end 9 | 10 | # vim: syntax=ruby 11 | -------------------------------------------------------------------------------- /lib/sexpwrap.rb: -------------------------------------------------------------------------------- 1 | require 'loadwrap' 2 | require 'ruby_parser' 3 | require 'ruby2ruby' 4 | 5 | module LoadWrap 6 | # Install a Sexp munging block to be called whenever code is 7 | # loaded via Kernel#require or Kernel#load. The block will be 8 | # passed the Sexp of the script (generated using ruby_parser). The 9 | # result of the block (value) will be the Sexp that is the actual 10 | # code to run. The resulting Sexp will be converted into Ruby 11 | # source code (using Ruby2Ruby) and eval'd and run by Ruby. 12 | # 13 | # === Example: 14 | # 15 | # require 'sexpwrap' 16 | # LoadWrapper.filter_sexp do |sexp| 17 | # perform_sexp_munging(sexp) 18 | # end 19 | def self.filter_sexp 20 | LoadWrap.filter_code do |code, filename| 21 | Ruby2Ruby.new.process(yield(RubyParser.new.parse(code, filename), filename)) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | = loadwrap 2 | 3 | * http://github.com/larsch/loadwrap 4 | 5 | == DESCRIPTION: 6 | 7 | Wraps loading of Ruby code (Through Kernel#require and Kernel#load) 8 | with your own methods that can change the code before it is parsed and 9 | run by the Ruby interpreter. 10 | 11 | == FEATURES/PROBLEMS: 12 | 13 | * $LOADED_FEATURES and $" are updated prior to evaluating the 14 | contents of a script. The MRI behaviour is to update it after the 15 | script is evaluated. 16 | 17 | == SYNOPSIS: 18 | 19 | Intercepting loading of scripts from the file system using LoadWrap.loadwrap: 20 | 21 | require 'loadwrap' 22 | LoadWrap.loadwrap do |filename| 23 | File.read(filename) 24 | end 25 | 26 | Intercepting parsing of scripts using LoadWrap.filter_code: 27 | 28 | require 'loadwrap' 29 | LoadWrap.filter_code do |code| 30 | code_munging_method(code) 31 | end 32 | 33 | Intercepting parsetrees using LoadWrap.filter_code (Requires ruby_parser and Ruby2Ruby gems): 34 | 35 | require 'sexpwrap' 36 | LoadWrap.filter_sexp do |code| 37 | sexp_munging_method(code) 38 | end 39 | 40 | == REQUIREMENTS: 41 | 42 | * ruby_parser (optional) 43 | * Ruby2Ruby (optional) 44 | * sexp_processor (optional) 45 | 46 | == INSTALL: 47 | 48 | * gem install loadwrap 49 | 50 | == LICENSE: 51 | 52 | (The MIT License) 53 | 54 | Copyright (c) 2009 Lars Christensen 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining 57 | a copy of this software and associated documentation files (the 58 | 'Software'), to deal in the Software without restriction, including 59 | without limitation the rights to use, copy, modify, merge, publish, 60 | distribute, sublicense, and/or sell copies of the Software, and to 61 | permit persons to whom the Software is furnished to do so, subject to 62 | the following conditions: 63 | 64 | The above copyright notice and this permission notice shall be 65 | included in all copies or substantial portions of the Software. 66 | 67 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 68 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 69 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 70 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 71 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 72 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 73 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 74 | -------------------------------------------------------------------------------- /test/test_loadwrap.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "loadwrap" 3 | require "fileutils" 4 | include FileUtils 5 | 6 | class TestLoadwrap < Test::Unit::TestCase 7 | 8 | def make_temporary_directory 9 | tmpbase = ENV["TEMP"] || ENV["TMP"] || "." 10 | tmppath = File.expand_path(File.join(tmpbase, ".tmp#{$$}")) 11 | mkdir_p tmppath 12 | return tmppath 13 | end 14 | 15 | def setup 16 | @original_path = Dir.getwd 17 | @tmppath = make_temporary_directory 18 | cd @tmppath 19 | puts @tmppath 20 | end 21 | 22 | def mkfile(filename, content) 23 | File.open(filename, "w") { |f| f << content } 24 | end 25 | 26 | def teardown 27 | cd @original_path 28 | rm_rf @tmppath 29 | end 30 | 31 | def test_current_file 32 | mkfile 'splat.rb', 'print __FILE__' 33 | assert_equal %x{ruby -rsplat -e 1}, LoadWrap.current_file('splat.rb') 34 | assert_equal %x{ruby -e "require './splat'" }, LoadWrap.current_file('splat.rb') 35 | path = File.basename(Dir.pwd) 36 | assert_equal %x{ruby -e "require '../#{path}/splat'" }, LoadWrap.current_file("../#{path}/splat.rb") 37 | absolute = Dir.pwd 38 | assert_equal %x{ruby -e "require '#{absolute}/splat'" }, LoadWrap.current_file("#{absolute}/splat.rb") 39 | end 40 | 41 | def test_featurep_path 42 | File.open("splat.rb", "w") { |f| f.puts "puts $LOADED_FEATURES;print $LOADED_FEATURES.grep(/splat\\.rb/)" } 43 | p %x{ ruby -e "require 'splat'; puts $LOADED_FEATURES.last" } 44 | end 45 | 46 | def test_load 47 | $loadwrap = false 48 | mkfile 'test.rb', '$loadwrap = true; $globalself = self' 49 | assert_equal true, Kernel.load('test.rb') 50 | assert_equal true, $loadwrap 51 | assert_equal eval("self.object_id", TOPLEVEL_BINDING), $globalself.object_id 52 | end 53 | 54 | def test_load_unwrapped 55 | mkfile 'test.rb', '$globalid1 = self.object_id' 56 | Kernel.loadwrap_custom_load 'test.rb' 57 | mkfile 'test.rb', '$globalid2 = self.object_id' 58 | Kernel.loadwrap_custom_load 'test.rb' 59 | 60 | assert_kind_of Numeric, $globalid1 61 | assert_kind_of Numeric, $globalid2 62 | assert_equal $globalid1, $globalid2 63 | end 64 | 65 | def test_load_wrapped 66 | mkfile 'test.rb', '$globalid1 = self.object_id; $globalstr1 = self.to_s' 67 | assert_equal true, Kernel.loadwrap_custom_load('test.rb') 68 | mkfile 'test.rb', '$globalid2 = self.object_id; $globalstr2 = self.to_s' 69 | assert_equal true, Kernel.loadwrap_custom_load('test.rb', true) 70 | 71 | assert_kind_of Numeric, $globalid1 72 | assert_kind_of Numeric, $globalid2 73 | assert_not_equal $globalid1, $globalid2 74 | 75 | assert_kind_of String, $globalstr1 76 | assert_kind_of String, $globalstr2 77 | assert_equal $globalstr1, $globalstr2 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/loadwrap.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | # LoadWrap enables munging of scripts before they are parsed and 4 | # run by Ruby. It hooks into Ruby's require and load methods and calls 5 | # back to your own filter methods with the contents of the 6 | # script. Your filter methods can then change the script in any way 7 | # and return the script to be actually loaded and run by Ruby. 8 | # 9 | # == Example: 10 | # 11 | # LoadWrap.filter_code do |code| 12 | # perform_code_munging(code) 13 | # end 14 | 15 | module LoadWrap 16 | @filters = [] 17 | @loadwrap = proc { |filename| File.read(filename) } 18 | 19 | # LoadWrap version number. 20 | VERSION = "0.0.1" 21 | 22 | class UnhandledPath < RuntimeError 23 | end 24 | 25 | class << self 26 | 27 | # Install a code munging block to be called whenever code is 28 | # loaded via Kernel#require or Kernel#load. The block will be 29 | # passed the contents of the script (Ruby source code) and 30 | # optionally the filename of the script being loaded. The result 31 | # of the block (value) will be the actual script that is parsed 32 | # and run by Ruby. 33 | # 34 | # === Example: 35 | # 36 | # LoadWrap.filter_code do |code, filename| 37 | # if filename =~ /somepattern/ 38 | # perform_code_munging(code) 39 | # end 40 | # end 41 | def filter_code(&block) 42 | @filters.push(block) 43 | end 44 | 45 | # Install a code loading block to be called whenever code is 46 | # loaded via Kernel#require or Kernel#load. The block is passed 47 | # the name of the file to be loaded and must evaluate to the Ruby 48 | # source code to be evalutated. Only 1 of these blocks can be 49 | # installed at one time. Calling this method multiple times will 50 | # replace previously installed wrappers. Filters installed using 51 | # filter_code or filter_sexp will still be called when loadwrap 52 | # hooks are installed. The default loadwrap handler is simply 53 | # calls File.read. 54 | # 55 | # === Example: 56 | # 57 | # LoadWrap.loadwrap do |filename| 58 | # File.read(filename) 59 | # end 60 | def loadwrap(&block) 61 | @loadwrap = block 62 | end 63 | 64 | # Load, munge, and run a file. Passes the file through the 65 | # loadwrap handler and the contents through each of the installed 66 | # filters (filter_code and filter_sexp). 67 | def custom_load(filename, wrap = false) #:nodoc: 68 | code = @loadwrap.call(filename) 69 | code = @filters.inject(code) { |memo, filter| 70 | raise UnhandledPath if memo.nil? 71 | if filter.arity == 2 72 | filter.call(memo, filename) 73 | else 74 | filter.call(memo) 75 | end 76 | } 77 | raise UnhandledPath if code.nil? 78 | 79 | if wrap 80 | global_clone = eval("self.clone", TOPLEVEL_BINDING) 81 | global_binding = global_clone.instance_eval("binding") 82 | else 83 | global_binding = TOPLEVEL_BINDING 84 | end 85 | eval code, global_binding, current_file(filename) 86 | return true 87 | end 88 | 89 | # Find a file in the Ruby search patch or relative to the current 90 | # path. 91 | def search_path(filename) #:nodoc: 92 | if File.file?(filename) 93 | return filename 94 | else 95 | $:.each do |pth| 96 | fpth = File.join(pth, filename) 97 | return fpth if File.file?(fpth) 98 | end 99 | end 100 | nil 101 | end 102 | 103 | # Given an absolute path and the filename originally require'd, 104 | # returns the path that would be inserted in 105 | # $LOADED_FEATURES/$". The Ruby 1.8 version returns the 106 | # filename, unchanged. 107 | def featurep_path_ruby18(path, filename) #:nodoc: 108 | filename 109 | end 110 | 111 | # Given a filename (as required, but with extension), return the 112 | # filename that will be used when eval'ing the file and which is 113 | # also available as __FILE__. The Ruby 1.8 returns the relative 114 | # patch (unless absolute) prefixed by './', except if the path 115 | # starts with '..'. 116 | def current_file_ruby18(filename) #:nodoc: 117 | if Pathname.new(filename).absolute? 118 | filename 119 | else 120 | if filename =~ /^\.\.?\// 121 | filename 122 | else 123 | File.join('.', filename) 124 | end 125 | end 126 | end 127 | 128 | # Given an path and the filename originally require'd, returns 129 | # the path that would be inserted in $LOADED_FEATURES/$". The 130 | # Ruby 1.9 version returns the expanded full path. 131 | def featurep_path_ruby19(path, filename) #:nodoc: 132 | File.expand_path(path) 133 | end 134 | 135 | # Given a filename (as required, but with extension), return the 136 | # filename that will be used when eval'ing the file and which is 137 | # also available as __FILE__. The Ruby 1.9 version returns the 138 | # absolute and expanded path. 139 | def current_file_ruby19(filename) #:nodoc: 140 | dfile = File.expand_path(filename) 141 | end 142 | 143 | if RUBY_VERSION =~ /^1\.8\./ 144 | alias featurep_path featurep_path_ruby18 145 | alias current_file current_file_ruby18 146 | else 147 | alias featurep_path featurep_path_ruby19 148 | alias current_file current_file_ruby19 149 | end 150 | 151 | # Determines if a given path (including load path) and filename 152 | # (as require'd) is already loaded. 153 | def feature_p(path, filename) #:nodoc: 154 | $LOADED_FEATURES.include?(featurep_path(path, filename)) 155 | end 156 | 157 | # Custom require method (called by Kernel#require). This search 158 | # for the specified file in the load path and adds optional '.rb' 159 | # to the filename. If the script is not found, it invokes the 160 | # original Kernel#require method (As 161 | # Kernel#loadwrap_original_require). 162 | def custom_require(filename) #:nodoc: 163 | if path = search_path(filename) 164 | return false if feature_p(path, filename) 165 | if filename =~ /\.rb$/ 166 | # p [:insert, featurep_path(path, filename)] 167 | $".push(featurep_path(path, filename)) 168 | custom_load(path) 169 | else 170 | loadwrap_original_require(filename) 171 | end 172 | else 173 | rbfilename = filename + '.rb' 174 | if path = search_path(rbfilename) 175 | return false if feature_p(path, rbfilename) 176 | # p [:insert, featurep_path(path, rbfilename)] 177 | $".push(featurep_path(path, rbfilename)) 178 | begin 179 | custom_load(path) 180 | rescue UnhandledPath 181 | $".pop 182 | loadwrap_original_require(filename) 183 | end 184 | else 185 | loadwrap_original_require(filename) 186 | end 187 | end 188 | end 189 | end 190 | end 191 | 192 | module Kernel #:nodoc: 193 | 194 | # Custom require method. This will is aliased to Kernel#require. 195 | def loadwrap_custom_require(filename) 196 | LoadWrap.custom_require(filename) 197 | end 198 | 199 | # Custom load method. This will is aliased to Kernel#load. 200 | def loadwrap_custom_load(filename, wrap = false) 201 | LoadWrap.custom_load(filename, wrap) 202 | end 203 | 204 | if respond_to?(:gem_original_require) 205 | alias loadwrap_original_require gem_original_require 206 | # Inject LoadWrap below RubyGems 207 | alias gem_original_require loadwrap_custom_require 208 | else 209 | alias loadwrap_original_require require 210 | # Install wrapper for require 211 | alias require loadwrap_custom_require 212 | end 213 | 214 | alias loadwrap_original_load load 215 | alias load loadwrap_custom_load 216 | end 217 | --------------------------------------------------------------------------------