├── .gitignore ├── test ├── run_all.rb ├── context_test.rb ├── rails_test.rb ├── renderer_test.rb ├── template_test.rb └── script_test.rb ├── MIT-LICENSE ├── lib ├── baby_erubis │ ├── rails.rb │ └── renderer.rb └── baby_erubis.rb ├── baby_erubis.gemspec ├── Rakefile ├── README.md ├── bin └── baby_erubis └── setup.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.pyo 4 | __pycache__/* 5 | *.rbc 6 | *.class 7 | *.o 8 | *.cache 9 | *.DS_Store 10 | tmp/* 11 | .svn/* 12 | -------------------------------------------------------------------------------- /test/run_all.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | here = File.dirname(File.expand_path(__FILE__)) 10 | Dir.glob(here + '/**/*_test.rb').each do |fpath| 11 | require fpath 12 | end 13 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 kuwata-lab.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/baby_erubis/rails.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: $ 5 | ### $Copyright: copyright(c) 2014 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | require 'baby_erubis' 10 | 11 | 12 | module BabyErubis 13 | 14 | 15 | class RailsTemplate < Template 16 | 17 | protected 18 | 19 | def add_preamble(src) 20 | src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" 21 | end 22 | 23 | def add_postamble(src) 24 | src << "@output_buffer.to_s\n" 25 | end 26 | 27 | def add_text(src, text) 28 | return if !text || text.empty? 29 | freeze = @freeze ? '.freeze' : '' 30 | text.gsub!(/['\\]/, '\\\\\&') 31 | src << "@output_buffer.safe_append='#{text}'#{freeze};" 32 | end 33 | 34 | def add_stmt(src, stmt) 35 | return if !stmt || stmt.empty? 36 | src << stmt 37 | end 38 | 39 | def add_expr(src, expr, indicator) 40 | return if !expr || expr.empty? 41 | l = '('; r = ')' 42 | l = r = ' ' if expr_has_block(expr) 43 | if indicator == '=' # escaping 44 | src << "@output_buffer.append=#{l}#{expr}#{r};" 45 | else # without escaping 46 | src << "@output_buffer.safe_append=#{l}#{expr}#{r};" 47 | end 48 | end 49 | 50 | end 51 | 52 | 53 | end 54 | -------------------------------------------------------------------------------- /baby_erubis.gemspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $License: MIT License $ 6 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 7 | ### 8 | 9 | require 'rubygems' 10 | 11 | Gem::Specification.new do |s| 12 | ## package information 13 | s.name = "baby_erubis" 14 | s.author = "makoto kuwata" 15 | s.email = "kwa(at)kuwata-lab.com" 16 | s.version = "$Release: 0.0.0 $".split()[1] 17 | s.license = "MIT License" 18 | s.platform = Gem::Platform::RUBY 19 | s.homepage = "https://github.com/kwatch/BabyErubis/tree/ruby" 20 | s.summary = "yet another eRuby implementation based on Erubis" 21 | s.description = <<'END' 22 | BabyErubis is an yet another eRuby implementation, based on Erubis. 23 | 24 | * Small and fast 25 | * Supports HTML as well as plain text 26 | * Accepts both template file and template string 27 | * Easy to customize 28 | 29 | BabyErubis support Ruby 1.9 or higher, and will work on 1.8 very well. 30 | END 31 | 32 | ## files 33 | files = [] 34 | files << 'lib/baby_erubis.rb' 35 | files << 'lib/baby_erubis/rails.rb' 36 | files << 'lib/baby_erubis/renderer.rb' 37 | files += Dir.glob('test/*.rb') 38 | files += ['bin/baby_erubis'] 39 | files += %w[README.md MIT-LICENSE setup.rb baby_erubis.gemspec Rakefile] 40 | s.files = files 41 | s.executables = ['baby_erubis'] 42 | s.bindir = 'bin' 43 | s.test_file = 'test/run_all.rb' 44 | end 45 | -------------------------------------------------------------------------------- /test/context_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | libpath = File.class_eval { join(dirname(dirname(__FILE__)), 'lib') } 10 | $: << libpath unless $:.include?(libpath) 11 | 12 | require 'minitest/autorun' 13 | 14 | require 'baby_erubis' 15 | 16 | 17 | 18 | describe 'BabyErubis::TemplateContext' do 19 | 20 | let(:ctx) { BabyErubis::TemplateContext.new } 21 | 22 | 23 | describe '#initialize()' do 24 | 25 | it "[!p69q1] takes hash object and sets them into instance variables." do 26 | ctx = BabyErubis::TemplateContext.new(:x=>10, :y=>20) 27 | assert_equal 10, ctx.instance_variable_get('@x') 28 | assert_equal 20, ctx.instance_variable_get('@y') 29 | assert_equal [:@x, :@y], ctx.instance_variables 30 | end 31 | 32 | it "[!p853f] do nothing when vars is nil." do 33 | ctx = BabyErubis::TemplateContext.new(nil) 34 | assert_equal [], ctx.instance_variables 35 | end 36 | 37 | end 38 | 39 | 40 | describe '#[]' do 41 | 42 | it "returns context value." do 43 | ctx = BabyErubis::TemplateContext.new(:x=>10) 44 | assert_equal 10, ctx[:x] 45 | end 46 | 47 | end 48 | 49 | 50 | describe '#[]=' do 51 | 52 | it "returns context value." do 53 | ctx = BabyErubis::TemplateContext.new 54 | ctx[:y] = 20 55 | assert_equal 20, ctx[:y] 56 | end 57 | 58 | end 59 | 60 | 61 | describe '#escape()' do 62 | 63 | it "converts any value into string." do 64 | assert_equal '10', ctx.escape(10) 65 | assert_equal 'true', ctx.escape(true) 66 | assert_equal '', ctx.escape(nil) 67 | assert_equal '["A", "B"]', ctx.escape(['A', 'B']) 68 | end 69 | 70 | it "does not escape html special chars." do 71 | assert_equal '<>&"', ctx.escape('<>&"') 72 | end 73 | 74 | end 75 | 76 | 77 | end 78 | 79 | 80 | 81 | describe 'BabyErubis::HtmlTemplateContext' do 82 | 83 | let(:ctx) { BabyErubis::HtmlTemplateContext.new } 84 | 85 | 86 | describe '#escape()' do 87 | 88 | it "escapes html special chars." do 89 | assert_equal '<>&"'', ctx.__send__(:escape, '<>&"\'') 90 | assert_equal '<a href="?x=1&y=2&z=3">click</a>', 91 | ctx.__send__(:escape, 'click') 92 | end 93 | 94 | end 95 | 96 | 97 | end 98 | -------------------------------------------------------------------------------- /test/rails_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | libpath = File.class_eval { join(dirname(dirname(__FILE__)), 'lib') } 10 | $: << libpath unless $:.include?(libpath) 11 | 12 | require 'minitest/autorun' 13 | 14 | require 'baby_erubis/rails' 15 | 16 | 17 | 18 | describe 'BabyErubis::RailsTemplate' do 19 | 20 | let(:tmpl) { BabyErubis::RailsTemplate.new } 21 | 22 | def _modify(ruby_code) 23 | if (''.freeze).equal?(''.freeze) 24 | return ruby_code.gsub(/([^'])';/m, "\\1'.freeze;") 25 | else 26 | return ruby_code 27 | end 28 | end 29 | 30 | eruby_template = <<'END' 31 |

New Article

32 | 33 | <%= form_for :article, url: articles_path do |f| %> 34 |

35 | <%= f.label :title %>
36 | <%= f.text_field :title %> 37 |

38 | 39 |

40 | <%= f.label :text %>
41 | <%= f.text_area :text %> 42 |

43 | 44 |

45 | <%= f.submit %> 46 |

47 | <% end %> 48 | END 49 | 50 | ruby_code = <<'END' 51 | @output_buffer = output_buffer || ActionView::OutputBuffer.new;@output_buffer.safe_append='

New Article

52 | 53 | ';@output_buffer.append= form_for :article, url: articles_path do |f| ;@output_buffer.safe_append=' 54 |

55 | ';@output_buffer.append=(f.label :title);@output_buffer.safe_append='
56 | ';@output_buffer.append=(f.text_field :title);@output_buffer.safe_append=' 57 |

58 | 59 |

60 | ';@output_buffer.append=(f.label :text);@output_buffer.safe_append='
61 | ';@output_buffer.append=(f.text_area :text);@output_buffer.safe_append=' 62 |

63 | 64 |

65 | ';@output_buffer.append=(f.submit);@output_buffer.safe_append=' 66 |

67 | '; end; 68 | @output_buffer.to_s 69 | END 70 | 71 | 72 | describe '#parse()' do 73 | 74 | it "converts eRuby template into ruby code with Rails style." do 75 | tmpl = BabyErubis::RailsTemplate.new.from_str(eruby_template) 76 | expected = _modify(ruby_code) 77 | assert_equal expected, tmpl.src 78 | end 79 | 80 | it "can understand block such as <%= form_for do |x| %>." do 81 | s = <<-'END' 82 | <%= (1..3).each do %> 83 | Hello 84 | <% end %> 85 | END 86 | tmpl = BabyErubis::RailsTemplate.new.from_str(s) 87 | assert_match /\@output_buffer.append= \(1\.\.3\)\.each do ;/, tmpl.src 88 | # 89 | s = <<-'END' 90 | <%= (1..3).each do |x, y|%> 91 | Hello 92 | <% end %> 93 | END 94 | tmpl = BabyErubis::RailsTemplate.new.from_str(s) 95 | assert_match /\@output_buffer.append= \(1\.\.3\)\.each do \|x, y\| ;/, tmpl.src 96 | end 97 | 98 | it "doesn't misunderstand <%= @todo %> as block" do 99 | tmpl = BabyErubis::RailsTemplate.new.from_str("<%= @todo %>") 100 | assert_match /\@output_buffer\.append=\(\@todo\);/, tmpl.src 101 | end 102 | 103 | end 104 | 105 | 106 | end 107 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | 5 | RELEASE = ENV['rel'] || '0.0.0' 6 | COPYRIGHT = 'copyright(c) 2014-2016 kuwata-lab.com all rights reserved' 7 | LICENSE = 'MIT License' 8 | 9 | ### 10 | 11 | task :default => :test 12 | 13 | def edit_content(content) 14 | s = content 15 | s = s.gsub /\$Release\:.*?\$/, "$Release\: #{RELEASE} $" 16 | s = s.gsub /\$Copyright\:.*?\$/, "$Copyright\: #{COPYRIGHT} $" 17 | s = s.gsub /\$License\:.*?\$/, "$License\: #{LICENSE} $" 18 | s = s.gsub /\$Release\$/, RELEASE 19 | s = s.gsub /\$Copyright\$/, COPYRIGHT 20 | s = s.gsub /\$License\$/, LICENSE 21 | s 22 | end 23 | 24 | 25 | desc "run test scripts" 26 | task :test do 27 | #sh "ruby -r minitest/autorun test/*_test.rb" 28 | sh "ruby test/run_all.rb" 29 | end 30 | 31 | 32 | desc "remove *.rbc" 33 | task :clean do 34 | rm_f [Dir.glob("lib/**/*.rbc"), Dir.glob("test/**/*.rbc")] 35 | end 36 | 37 | 38 | desc "udpate release number" 39 | task :edit do 40 | require_release_number() 41 | spec_src = File.open('baby_erubis.gemspec') {|f| f.read } 42 | spec = eval spec_src 43 | spec.files.each do |fpath| 44 | content = File.open(fpath, 'r+b:utf-8') do |f| 45 | content = f.read 46 | new_content = edit_content(content) 47 | if new_content == content 48 | puts "[ ] #{fpath}" 49 | else 50 | puts "[C] #{fpath}" 51 | f.rewind() 52 | f.truncate(0) 53 | f.write(new_content) 54 | end 55 | end 56 | end 57 | end 58 | 59 | 60 | desc "copy files into 'dist/#{RELEASE}'" 61 | task :dist => :clean do 62 | require_release_number() 63 | spec_src = File.open('baby_erubis.gemspec') {|f| f.read } 64 | spec = eval spec_src 65 | dir = "dist/#{RELEASE}" 66 | rm_rf dir 67 | mkdir_p dir 68 | sh "tar cf - #{spec.files.join(' ')} | (cd #{dir}; tar xvf -)" 69 | spec.files.each do |fpath| 70 | #filepath = File.join(dir, fpath) 71 | #content = File.open(filepath, 'rb:utf-8') {|f| f.read } 72 | #new_content = edit_content(content) 73 | #File.open(filepath, 'wb:utf-8') {|f| f.write(new_content) } 74 | content = File.open(File.join(dir, fpath), 'r+b:utf-8') do |f| 75 | content = f.read 76 | new_content = edit_content(content) 77 | f.rewind() 78 | f.truncate(0) 79 | f.write(new_content) 80 | end 81 | end 82 | end 83 | 84 | 85 | desc "create rubygem pacakge" 86 | task :package => :dist do 87 | require_release_number() 88 | chdir "dist/#{RELEASE}" do 89 | sh "gem build *.gemspec" 90 | end 91 | mv Dir.glob("dist/#{RELEASE}/*.gem"), 'dist' 92 | end 93 | 94 | 95 | desc "release gem" 96 | task :release => :package do 97 | require_release_number() 98 | sh "git tag ruby-#{RELEASE}" 99 | chdir "dist" do 100 | sh "gem push baby_erubis-#{RELEASE}.gem" 101 | end 102 | end 103 | 104 | 105 | def require_release_number 106 | if RELEASE == '0.0.0' 107 | $stderr.puts "*** Release number is not speicified" 108 | $stderr.puts "*** Usage: rake rel=X.X.X" 109 | raise StandardError 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/baby_erubis/renderer.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | 10 | module BabyErubis 11 | 12 | ## 13 | ## Module to define template rendering methods. 14 | ## 15 | ## ex: 16 | ## class MyController 17 | ## include BabyErubis::HtmlEscaper 18 | ## include BabyErubis::Renderer 19 | ## 20 | ## ERUBY_PATH = ['.', 'templates'] 21 | ## ERUBY_LAYOUT = :_layout 22 | ## ERUBY_HTML = BabyErubis::Html 23 | ## ERUBY_HTML_EXT = '.html.eruby' 24 | ## ERUBY_TEXT = BabyErubis::Text 25 | ## ERUBY_TEXT_EXT = '.eruby' 26 | ## ERUBY_CACHE = {} 27 | ## 28 | ## def index 29 | ## @items = ['A', 'B', 'C'] 30 | ## ## renders 'templates/welcome.html.eruby' 31 | ## html = eruby_render_html(:welcome) 32 | ## return html 33 | ## end 34 | ## 35 | ## end 36 | ## 37 | ## 38 | ## Notice: 39 | ## 40 | ## eruby_render_html(:welcome) # render 'welcome.html.eruby' 41 | ## eruby_render_html('welcome') # render 'welcome' 42 | ## eruby_render_html('welcome.html.eruby') # render 'welcome.html.eruby' 43 | ## 44 | module Renderer 45 | 46 | ERUBY_PATH = ['.'] 47 | ERUBY_LAYOUT = :_layout 48 | ERUBY_HTML = BabyErubis::Html 49 | ERUBY_HTML_EXT = '.html.eruby' 50 | ERUBY_TEXT = BabyErubis::Text 51 | ERUBY_TEXT_EXT = '.eruby' 52 | ERUBY_CACHE = {} 53 | 54 | def eruby_render_html(template_name, layout: true, encoding: 'utf-8') 55 | ext = self.class.const_get :ERUBY_HTML_EXT 56 | tmpl_class = self.class.const_get :ERUBY_HTML 57 | return _eruby_render_template(template_name, layout) {|tmpl_name| 58 | filename = tmpl_name.is_a?(Symbol) ? "#{tmpl_name}#{ext}" : tmpl_name 59 | _eruby_find_template(filename) {|fpath| 60 | tmpl_class.new.from_file(fpath, encoding) 61 | } 62 | } 63 | end 64 | 65 | def eruby_render_text(template_name, layout: false, encoding: 'utf-8') 66 | ext = self.class.const_get :ERUBY_TEXT_EXT 67 | tmpl_class = self.class.const_get :ERUBY_TEXT 68 | return _eruby_render_template(template_name, layout) {|tmpl_name| 69 | filename = tmpl_name.is_a?(Symbol) ? "#{tmpl_name}#{ext}" : tmpl_name 70 | _eruby_find_template(filename) {|fpath| 71 | tmpl_class.new.from_file(fpath, encoding) 72 | } 73 | } 74 | end 75 | 76 | private 77 | 78 | def _eruby_find_template(filename) 79 | cache = self.class.const_get :ERUBY_CACHE 80 | paths = self.class.const_get :ERUBY_PATH 81 | dir = paths.find {|path| cache.key?("#{path}/#{filename}") } \ 82 | || paths.find {|path| File.file?("#{path}/#{filename}") } or 83 | raise BabyErubis::TemplateError.new("#{filename}: template not found in #{paths.inspect}.") 84 | fpath = "#{dir}/#{filename}" 85 | # 86 | now = Time.now 87 | template = _eruby_load_template(cache, fpath, now) 88 | unless template 89 | mtime = File.mtime(fpath) 90 | template = yield fpath 91 | ## retry when file timestamp changed during template loading 92 | unless mtime == (mtime2 = File.mtime(fpath)) 93 | mtime = mtime2 94 | template = yield fpath 95 | mtime == File.mtime(fpath) or 96 | raise "#{fpath}: timestamp changes too frequently. something wrong." 97 | end 98 | _eruby_store_template(cache, fpath, template, mtime, now) 99 | end 100 | return template 101 | end 102 | 103 | def _eruby_load_template(cache, fpath, now) 104 | tuple = cache[fpath] 105 | template, timestamp, last_checked = tuple 106 | return nil unless template 107 | ## skip timestamp check in order to reduce syscall (= File.mtime()) 108 | interval = now - last_checked 109 | return template if interval < 0.5 110 | ## check timestamp only for 5% request in order to avoid thundering herd 111 | return template if interval < 1.0 && rand() > 0.05 112 | ## update last_checked in cache when file timestamp is not changed 113 | if timestamp == File.mtime(fpath) 114 | tuple[2] = now 115 | return template 116 | ## remove cache entry when file timestamp is changed 117 | else 118 | cache[fpath] = nil 119 | return nil 120 | end 121 | end 122 | 123 | def _eruby_store_template(cache, fpath, template, timestamp, last_checked) 124 | cache[fpath] = [template, timestamp, last_checked] 125 | end 126 | 127 | def _eruby_render_template(template_name, layout) 128 | template = yield template_name 129 | s = template.render(self) 130 | unless @_layout.nil? 131 | layout = @_layout; @_layout = nil 132 | end 133 | while layout 134 | layout = self.class.const_get :ERUBY_LAYOUT if layout == true 135 | template = yield layout 136 | @_content = s 137 | s = template.render(self) 138 | @_content = nil 139 | layout = @_layout; @_layout = nil 140 | end 141 | return s 142 | end 143 | 144 | end 145 | 146 | 147 | end 148 | -------------------------------------------------------------------------------- /lib/baby_erubis.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | 10 | ## 11 | ## Yet another eRuby implementation, based on Erubis. 12 | ## See https://github.com/kwatch/baby_erubis/tree/ruby for details. 13 | ## 14 | ## Example: 15 | ## template = BabyEruibs::Html.new.from_str <<'END', __FILE__, __LINE__+1 16 | ## <% for item in @items %> 17 | ## - item = <%= item %> 18 | ## <% end %> 19 | ## END 20 | ## print template.render(:items=>['A', 'B', 'C']) 21 | ## ## or 22 | ## template = BabyErubis::Html.new.from_file('example.html.erb', 'utf-8') 23 | ## print template.render(:items=>['A', 'B', 'C']) 24 | ## 25 | 26 | module BabyErubis 27 | 28 | RELEASE = '$Release: 0.0.0 $'.split(' ')[1] 29 | 30 | 31 | class TemplateError < StandardError 32 | end 33 | 34 | 35 | class Template 36 | 37 | FREEZE = (''.freeze).equal?(''.freeze) # Ruby 2.1 feature 38 | 39 | def initialize(opts=nil) 40 | @freeze = self.class.const_get(:FREEZE) 41 | if opts 42 | @freeze = (v=opts[:freeze ]) != nil ? v : @freeze 43 | end 44 | end 45 | 46 | def from_file(filename, encoding='utf-8') 47 | mode = "rb:#{encoding}" 48 | mode = "rb" if RUBY_VERSION < '1.9' 49 | input = File.open(filename, mode) {|f| f.read() } 50 | compile(parse(input), filename, 1) 51 | return self 52 | end 53 | 54 | def from_str(input, filename=nil, linenum=1) 55 | compile(parse(input), filename, linenum) 56 | return self 57 | end 58 | 59 | attr_reader :src 60 | 61 | #PATTERN = /(^[ \t]*)?<%(\#)?(==?)?(.*?)%>([ \t]*\r?\n)?/m 62 | PATTERN = /(^[ \t]*)?<%-?(\#)?(==?)? ?(.*?) ?-?%>([ \t]*\r?\n)?/m 63 | 64 | def pattern 65 | return self.class.const_get(:PATTERN) 66 | end 67 | 68 | def compile(src, filename=nil, linenum=1) 69 | @src = src 70 | @proc = eval("proc { #{src} }", empty_binding(), filename || '(eRuby)', linenum) 71 | return self 72 | end 73 | 74 | def parse(input) 75 | src = "" 76 | add_preamble(src) # preamble 77 | spc = "" 78 | pos = 0 79 | input.scan(pattern()) do |lspace, sharp, ch, code, rspace| 80 | match = Regexp.last_match 81 | text = input[pos, match.begin(0) - pos] 82 | pos = match.end(0) 83 | if sharp # comment 84 | code = ("\n" * code.count("\n")) 85 | if ! ch && lspace && rspace # trimmed statement 86 | add_text(src, "#{spc}#{text}"); add_stmt(src, "#{code}#{rspace}") 87 | rspace = "" 88 | else # other statement or expression 89 | add_text(src, "#{spc}#{text}#{lspace}"); add_stmt(src, code) 90 | end 91 | else 92 | if ch # expression 93 | add_text(src, "#{spc}#{text}#{lspace}"); add_expr(src, code, ch) 94 | elsif lspace && rspace # statement (trimming) 95 | add_text(src, "#{spc}#{text}"); add_stmt(src, "#{lspace} #{code};#{rspace}") 96 | rspace = "" 97 | else # statement (without trimming) 98 | add_text(src, "#{spc}#{text}#{lspace}"); add_stmt(src, " #{code};") 99 | end 100 | end 101 | spc = rspace 102 | end 103 | text = pos == 0 ? input : input[pos..-1] # or $' || input 104 | add_text(src, "#{spc}#{text}") 105 | add_postamble(src) # postamble 106 | return src 107 | end 108 | 109 | def render(context={}) 110 | ctxobj = context.nil? || context.is_a?(Hash) ? new_context(context) : context 111 | return ctxobj.instance_eval(&@proc) 112 | end 113 | 114 | def new_context(hash) 115 | return TemplateContext.new(hash) 116 | end 117 | 118 | protected 119 | 120 | def add_preamble(src) 121 | src << "_buf = '';" 122 | end 123 | 124 | def add_postamble(src) 125 | src << " _buf.to_s\n" 126 | end 127 | 128 | def add_text(src, text) 129 | return if !text || text.empty? 130 | freeze = @freeze ? '.freeze' : '' 131 | text.gsub!(/['\\]/, '\\\\\&') 132 | src << " _buf << '#{text}'#{freeze};" 133 | end 134 | 135 | def add_stmt(src, stmt) 136 | return if !stmt || stmt.empty? 137 | src << stmt 138 | end 139 | 140 | def add_expr(src, expr, indicator) 141 | return if !expr || expr.empty? 142 | if expr_has_block(expr) 143 | src << " _buf << #{expr}" 144 | elsif indicator == '=' # escaping 145 | src << " _buf << #{escaped_expr(expr)};" 146 | else # without escaping 147 | src << " _buf << (#{expr}).to_s;" 148 | end 149 | end 150 | 151 | def escaped_expr(code) 152 | return "(#{code}).to_s" 153 | end 154 | 155 | def expr_has_block(expr) 156 | return expr =~ /(\bdo|\{)\s*(\|[^|]*?\|\s*)?\z/ 157 | end 158 | 159 | private 160 | 161 | def empty_binding 162 | return binding() 163 | end 164 | 165 | end 166 | Text = Template # for shortcut 167 | 168 | 169 | class TemplateContext 170 | 171 | def initialize(vars={}) 172 | vars.each do |k, v| 173 | instance_variable_set("@#{k}", v) 174 | end if vars 175 | end 176 | 177 | def [](key) 178 | return instance_variable_get("@#{key}") 179 | end 180 | 181 | def []=(key, value) 182 | instance_variable_set("@#{key}", value) 183 | end 184 | 185 | def escape(value) 186 | return value.to_s 187 | end 188 | 189 | end 190 | 191 | 192 | class HtmlTemplate < Template 193 | 194 | def escaped_expr(code) 195 | return "escape(#{code})" # escape() is defined in HtmlTemplateContext 196 | end 197 | protected :escaped_expr 198 | 199 | def new_context(hash) 200 | return HtmlTemplateContext.new(hash) 201 | end 202 | 203 | end 204 | Html = HtmlTemplate # for shortcut 205 | 206 | 207 | module HtmlEscaper 208 | 209 | HTML_ESCAPE = {'&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"', "'"=>'''} 210 | 211 | module_function 212 | 213 | def escape(value) 214 | return value.to_s.gsub(/[<>&"']/, HTML_ESCAPE) # for Ruby 1.9 or later 215 | end 216 | 217 | if RUBY_VERSION < '1.9' 218 | def escape(value) 219 | return value.to_s.gsub(/&/, '&').gsub(//, '>').gsub(/"/, '"').gsub(/'/, ''') 220 | end 221 | end 222 | 223 | end 224 | 225 | 226 | class HtmlTemplateContext < TemplateContext 227 | include HtmlEscaper 228 | end 229 | 230 | 231 | end 232 | -------------------------------------------------------------------------------- /test/renderer_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | libpath = File.class_eval { join(dirname(dirname(__FILE__)), 'lib') } 10 | $: << libpath unless $:.include?(libpath) 11 | 12 | require 'minitest/autorun' 13 | 14 | require 'baby_erubis' 15 | require 'baby_erubis/renderer' 16 | 17 | 18 | class HelloClass 19 | include BabyErubis::HtmlEscaper 20 | include BabyErubis::Renderer 21 | 22 | ERUBY_PATH = ['_t'] 23 | ERUBY_HTML_EXT = '.html.erb' 24 | ERUBY_TEXT_EXT = '.erb' 25 | 26 | def initialize(vars={}) 27 | vars.each do |k, v| 28 | instance_variable_set("@#{k}", v) 29 | end 30 | end 31 | 32 | end 33 | 34 | 35 | describe 'BabyErubis::Renderer' do 36 | 37 | layout_template = <<'END' 38 | 39 | 40 | 41 | 42 | <%= @page_title %> 43 | 44 | 45 |
46 | <%== @_content %> 47 |
48 | 49 | 50 | END 51 | 52 | html_template = <<'END' 53 | <% 54 | @page_title = 'Example' 55 | %> 56 |

<%= @page_title %>

57 | 62 | END 63 | 64 | layout2_template = <<'END' 65 | <% 66 | @_layout = true 67 | %> 68 |
69 | <%== @_content %> 70 |
71 | END 72 | 73 | text_template = <<'END' 74 | title: <%= @title %> 75 | date: <%= @date %> 76 | END 77 | 78 | before do 79 | Dir.mkdir('_t') 80 | File.open('_t/_layout.html.erb', 'w') {|f| f.write(layout_template) } 81 | File.open('_t/_layout2.html.erb', 'w') {|f| f.write(layout2_template) } 82 | File.open('_t/welcome.html.erb', 'w') {|f| f.write(html_template) } 83 | File.open('_t/example.text.erb', 'w') {|f| f.write(text_template) } 84 | end 85 | 86 | after do 87 | Dir.glob('_t/*').each {|fpath| File.unlink(fpath) if File.file?(fpath) } 88 | Dir.rmdir('_t') 89 | end 90 | 91 | 92 | describe '#eruby_render_html()' do 93 | 94 | it "renders html template." do 95 | expected = <<'END' 96 |

Example

97 | 102 | END 103 | # 104 | obj = HelloClass.new(:items=>[10, 20, 30]) 105 | actual = obj.eruby_render_html(:'welcome', layout: false) 106 | assert_equal expected, actual 107 | # 108 | obj = HelloClass.new(:items=>[10, 20, 30]) 109 | actual = obj.eruby_render_html('welcome.html.erb', layout: false) 110 | assert_equal expected, actual 111 | end 112 | 113 | it "renders with layout template." do 114 | obj = HelloClass.new(:items=>[10, 20, 30]) 115 | actual = obj.eruby_render_html(:'welcome', layout: :'_layout') 116 | expected = <<'END' 117 | 118 | 119 | 120 | 121 | Example 122 | 123 | 124 |
125 |

Example

126 | 131 | 132 |
133 | 134 | 135 | END 136 | assert_equal expected, actual 137 | end 138 | 139 | end 140 | 141 | 142 | describe '#eruby_render_text()' do 143 | 144 | it "renders text template" do 145 | expected = <<'END' 146 | title: Homhom 147 | date: 2015-01-01 148 | END 149 | # 150 | obj = HelloClass.new(:title=>"Homhom", :date=>"2015-01-01") 151 | actual = obj.eruby_render_text(:'example.text', layout: false) 152 | assert_equal expected, actual 153 | # 154 | obj = HelloClass.new(:title=>"Homhom", :date=>"2015-01-01") 155 | actual = obj.eruby_render_text('example.text.erb', layout: false) 156 | assert_equal expected, actual 157 | end 158 | 159 | end 160 | 161 | 162 | describe '#_eruby_find_template()' do 163 | 164 | it "caches template object with timestamp." do 165 | cache = HelloClass.const_get :ERUBY_CACHE 166 | cache.clear() 167 | assert_equal 0, cache.length 168 | obj = HelloClass.new(:items=>[10, 20, 30]) 169 | t1 = Time.now 170 | obj.eruby_render_html(:'welcome') 171 | t2 = Time.now 172 | assert_equal 2, cache.length 173 | tuple = cache['_t/welcome.html.erb'] 174 | assert tuple.is_a?(Array) 175 | assert_equal 3, tuple.length 176 | assert_equal BabyErubis::HtmlTemplate, tuple[0].class 177 | assert_equal File.mtime('_t/welcome.html.erb'), tuple[1] 178 | assert t1 < tuple[2] 179 | assert t2 > tuple[2] 180 | end 181 | 182 | it "caches template object with timestamp." do 183 | cache = HelloClass.const_get :ERUBY_CACHE 184 | cache.clear() 185 | obj = HelloClass.new(:items=>[10, 20, 30]) 186 | obj.eruby_render_html(:'welcome') 187 | tuple1 = cache['_t/welcome.html.erb'] 188 | templ1 = tuple1[0] 189 | mtime1 = tuple1[1] 190 | # 191 | tstamp = Time.now - 30 192 | File.utime(tstamp, tstamp, '_t/welcome.html.erb') 193 | sleep(1.0) 194 | obj.eruby_render_html(:'welcome') 195 | tuple2 = cache['_t/welcome.html.erb'] 196 | templ2 = tuple2[0] 197 | mtime2 = tuple2[1] 198 | assert templ1 != templ2 199 | assert mtime1 != mtime2 200 | assert templ2.is_a?(BabyErubis::HtmlTemplate) 201 | assert_equal tstamp.to_s, mtime2.to_s 202 | end 203 | 204 | it "raises BabyErubis::TempalteError when template file not found." do 205 | obj = HelloClass.new(:items=>[10, 20, 30]) 206 | ex = assert_raises(BabyErubis::TemplateError) do 207 | obj.eruby_render_html(:'hello-homhom') 208 | end 209 | expected = "hello-homhom.html.erb: template not found in [\"_t\"]." 210 | assert_equal expected, ex.message 211 | end 212 | 213 | end 214 | 215 | 216 | describe '#_eruby_load_template()' do 217 | 218 | _prepare = proc { 219 | cache = HelloClass.const_get :ERUBY_CACHE 220 | cache.clear() 221 | obj = HelloClass.new(:items=>[10, 20, 30]) 222 | obj.eruby_render_html(:'welcome') 223 | fpath = '_t/welcome.html.erb' 224 | ts = Time.now - 30 225 | File.utime(ts, ts, fpath) 226 | [cache, obj, fpath] 227 | } 228 | 229 | _render = proc {|cache, obj, fpath, n, expected| 230 | count = 0 231 | n.times do 232 | obj.eruby_render_html(:'welcome') 233 | if expected != cache[fpath] 234 | count += 1 235 | cache[fpath] = expected 236 | end 237 | end 238 | count 239 | } 240 | 241 | it "skips timestamp check in order to reduce syscall (= File.mtime())" do 242 | cache, obj, fpath = _prepare.call() 243 | # 244 | sleep(0.1) 245 | count = _render.call(cache, obj, fpath, 1000, cache[fpath]) 246 | assert count == 0, "#{count} == 0: failed" 247 | end 248 | 249 | it "checks timestamp only for 5% request in order to avoid thundering herd" do 250 | cache, obj, fpath = _prepare.call() 251 | # 252 | sleep(0.6) 253 | count = _render.call(cache, obj, fpath, 1000, cache[fpath]) 254 | assert count > 0, "#{count} > 0: failed" 255 | assert count > 3, "#{count} > 3: failed" 256 | assert count < 100, "#{count} < 100: failed" 257 | end 258 | 259 | it "update last_checked in cache when file timestamp is not changed" do 260 | cache, obj, fpath = _prepare.call() 261 | _, _, old_last_checked = cache[fpath] 262 | # 263 | sleep(1.0) 264 | now = Time.now 265 | obj.eruby_render_html(:'welcome') 266 | _, _, new_last_checked = cache[fpath] 267 | assert_operator new_last_checked, :'!=', old_last_checked 268 | assert_operator (new_last_checked - old_last_checked), :'>=', 1.0 269 | assert_operator (new_last_checked - now), :'<', 0.001 270 | end 271 | 272 | it "remove cache entry when file timestamp is changed" do 273 | cache, obj, fpath = _prepare.call() 274 | # 275 | sleep(1.0) 276 | count = _render.call(cache, obj, fpath, 1000, cache[fpath]) 277 | assert count == 1000, "#{count} == 1000: failed" 278 | # 279 | assert cache[fpath] != nil 280 | ret = obj.__send__(:_eruby_load_template, cache, fpath, Time.now) 281 | assert_nil ret 282 | assert_nil cache[fpath] 283 | end 284 | 285 | end 286 | 287 | 288 | describe '#_eruby_render_template()' do 289 | 290 | it "recognizes '@_layout' variable" do 291 | expected = <<'END' 292 | 293 | 294 | 295 | 296 | Example 297 | 298 | 299 |
300 |
301 |

Example

302 |
    303 |
  • 10
  • 304 |
  • 20
  • 305 |
  • 30
  • 306 |
307 | 308 |
309 | 310 |
311 | 312 | 313 | END 314 | obj = HelloClass.new(:items=>[10,20,30]) 315 | actual = obj.eruby_render_html(:'welcome', layout: :'_layout2') 316 | assert_equal expected, actual 317 | end 318 | 319 | end 320 | 321 | 322 | end 323 | -------------------------------------------------------------------------------- /test/template_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | libpath = File.class_eval { join(dirname(dirname(__FILE__)), 'lib') } 10 | $: << libpath unless $:.include?(libpath) 11 | 12 | require 'minitest/autorun' 13 | 14 | require 'baby_erubis' 15 | 16 | 17 | describe BabyErubis::Template do 18 | 19 | let(:template) { BabyErubis::Text.new() } 20 | 21 | def _modify(ruby_code) 22 | if (''.freeze).equal?(''.freeze) 23 | return ruby_code.gsub(/([^'])';/m, "\\1'.freeze;") 24 | else 25 | return ruby_code 26 | end 27 | end 28 | 29 | 30 | describe '#parse()' do 31 | 32 | it "[!118pw] parses template string into ruby code." do 33 | input = <<'END' 34 | title: <%= @title %> 35 | items: 36 | <% for item in @items %> 37 | - <%= item %> 38 | <% end %> 39 | END 40 | expected = <<'END' 41 | _buf = ''; _buf << 'title: '; _buf << (@title).to_s; _buf << ' 42 | items: 43 | '; for item in @items; 44 | _buf << ' - '; _buf << (item).to_s; _buf << ' 45 | '; end; 46 | _buf.to_s 47 | END 48 | expected = _modify(expected) 49 | code = template.parse(input) 50 | assert_equal expected, code 51 | end 52 | 53 | it "[!7ht59] escapes single quotation and backslash characters." do 54 | input = <<'END' 55 | who's who? 56 | ''' 57 | \w 58 | \\\ 59 | END 60 | expected = <<'END' 61 | _buf = ''; _buf << 'who\'s who? 62 | \'\'\' 63 | \\w 64 | \\\\\\ 65 | '; _buf.to_s 66 | END 67 | expected = _modify(expected) 68 | assert_equal expected, template.parse(input) 69 | end 70 | 71 | it "[!u93y5] appends embedded expression in '<%= %>'." do 72 | input = <<'END' 73 | x = <%= x %> 74 | END 75 | expected = <<'END' 76 | _buf = ''; _buf << 'x = '; _buf << (x).to_s; _buf << ' 77 | '; _buf.to_s 78 | END 79 | expected = _modify(expected) 80 | assert_equal expected, template.parse(input) 81 | end 82 | 83 | it "[!auj95] appends embedded expression in '<%= %>' without escaping." do 84 | input = <<'END' 85 | x = <%== x %> 86 | END 87 | expected = <<'END' 88 | _buf = ''; _buf << 'x = '; _buf << (x).to_s; _buf << ' 89 | '; _buf.to_s 90 | END 91 | expected = _modify(expected) 92 | assert_equal expected, template.parse(input) 93 | end 94 | 95 | it "[!qveql] appends linefeeds when '<%# %>' found." do 96 | input = <<'END' 97 | <%# 98 | for x in xs 99 | %> 100 | x = <%#= 101 | x 102 | %> 103 | <%# 104 | end 105 | %> 106 | END 107 | expected = <<'END' 108 | _buf = ''; 109 | 110 | 111 | _buf << 'x = '; 112 | 113 | _buf << ' 114 | '; 115 | 116 | 117 | _buf.to_s 118 | END 119 | expected = _modify(expected) 120 | assert_equal expected, template.parse(input) 121 | end 122 | 123 | it "[!b10ns] generates ruby code correctly even when no embedded code." do 124 | input = <<'END' 125 | abc 126 | def 127 | END 128 | expected = <<'END' 129 | _buf = ''; _buf << 'abc 130 | def 131 | '; _buf.to_s 132 | END 133 | expected = _modify(expected) 134 | assert_equal expected, template.parse(input) 135 | end 136 | 137 | it "[!3bx3d] not print extra linefeeds when line starts with '<%' and ends with '%>'" do 138 | input = <<'END' 139 | <% for item in items %> 140 | <% if item %> 141 | item = <%= item %> 142 | <% end %> 143 | <% end %> 144 | END 145 | expected = <<'END' 146 | _buf = ''; for item in items; 147 | if item; 148 | _buf << ' item = '; _buf << (item).to_s; _buf << ' 149 | '; end; 150 | end; 151 | _buf.to_s 152 | END 153 | expected = _modify(expected) 154 | assert_equal expected, template.parse(input) 155 | end 156 | 157 | it "handles '<%- -%>' (but do nothing)." do 158 | input = <<'END' 159 | <%- for item in @items -%> 160 | <%-== item -%> 161 | <%- end -%> 162 | END 163 | expected = <<'END' 164 | _buf = ''; for item in @items; 165 | _buf << ' '; _buf << (item).to_s; _buf << ' 166 | '; end; 167 | _buf.to_s 168 | END 169 | expected = _modify(expected) 170 | assert_equal expected, template.parse(input) 171 | end 172 | 173 | it "uses String#freeze forcedly when ':freeze=>true' passed to constructor." do 174 | input = "value=<%== value %>" 175 | expected = "_buf = ''; _buf << 'value='.freeze; _buf << (value).to_s; _buf.to_s\n" 176 | template = BabyErubis::Text.new(:freeze=>true) 177 | assert_equal expected, template.parse(input) 178 | end 179 | 180 | it "doesn't use String#freeze when ':freeze=>false' passed to constructor." do 181 | input = "value=<%== value %>" 182 | expected = "_buf = ''; _buf << 'value='; _buf << (value).to_s; _buf.to_s\n" 183 | template = BabyErubis::Text.new(:freeze=>false) 184 | assert_equal expected, template.parse(input) 185 | end 186 | 187 | it "concats spaces around embedded expressions." do 188 | input = <<'END' 189 |
190 | <%= f.text :email %> 191 | <%= f.password :password %> 192 | <%= f.submit 'Login' %> 193 |
194 | END 195 | expected = <<'END' 196 | _buf = ''; _buf << '
197 | '; _buf << (f.text :email).to_s; _buf << ' 198 | '; _buf << (f.password :password).to_s; _buf << ' 199 | '; _buf << (f.submit 'Login').to_s; _buf << ' 200 |
201 | '; _buf.to_s 202 | END 203 | template = BabyErubis::Text.new(:freeze=>false) 204 | assert_equal expected, template.parse(input) 205 | end 206 | 207 | it "can recognize block argument in embedded expression correctly." do 208 | input = <<'END' 209 | <%= form_for(:article) do |f| %> 210 | ... 211 | <% end %> 212 | END 213 | expected = <<'END' 214 | _buf = ''; _buf << form_for(:article) do |f| _buf << ' 215 | ... 216 | '; end; 217 | _buf.to_s 218 | END 219 | template = BabyErubis::Text.new(:freeze=>false) 220 | assert_equal expected, template.parse(input) 221 | # 222 | input = <<'END' 223 | <%= form_for :article do %> 224 | ... 225 | <% end %> 226 | END 227 | expected = <<'END' 228 | _buf = ''; _buf << form_for :article do _buf << ' 229 | ... 230 | '; end; 231 | _buf.to_s 232 | END 233 | template = BabyErubis::Text.new(:freeze=>false) 234 | assert_equal expected, template.parse(input) 235 | end 236 | 237 | it "doesn't misunderstand <%= @todo %> as block" do 238 | tmpl = BabyErubis::Text.new 239 | src = tmpl.parse("<%= @todo %>") 240 | assert_match /\ _buf << \(\@todo\)\.to_s;/, src 241 | end 242 | 243 | end 244 | 245 | 246 | describe '#pattern()' do 247 | 248 | it "returns default embed pattern." do 249 | template = BabyErubis::Template.new 250 | assert_equal BabyErubis::Template::PATTERN, template.pattern 251 | end 252 | 253 | it "returns new embed pattern when overrided in subclass." do 254 | class FooTemplate < BabyErubis::Template 255 | rexp = BabyErubis::Template::PATTERN 256 | PATTERN = Regexp.compile(rexp.to_s.sub(/<%/, '\{%').sub(/%>/, '%\}')) 257 | end 258 | template = FooTemplate.new 259 | refute_equal BabyErubis::Template::PATTERN, template.pattern 260 | assert_equal FooTemplate::PATTERN, template.pattern 261 | end 262 | 263 | end 264 | 265 | 266 | describe '#render()' do 267 | 268 | it "renders template with context values." do 269 | input = <<'END' 270 | title: <%== @title %> 271 | items: 272 | <% for item in @items %> 273 | - <%== item %> 274 | <% end %> 275 | END 276 | expected = <<'END' 277 | title: Example 278 | items: 279 | - 280 | - B&B 281 | - "CCC" 282 | END 283 | context = {:title=>'Example', :items=>['', 'B&B', '"CCC"']} 284 | output = template.from_str(input).render(context) 285 | assert_equal expected, output 286 | end 287 | 288 | it "renders context values with no escaping." do 289 | input = <<'END' 290 | title: <%= @title %> 291 | items: 292 | <% for item in @items %> 293 | - <%= item %> 294 | <% end %> 295 | END 296 | expected = <<'END' 297 | title: Example 298 | items: 299 | - 300 | - B&B 301 | - "CCC" 302 | END 303 | tmpl = BabyErubis::Text.new.from_str(input) 304 | context = {:title=>'Example', :items=>['', 'B&B', '"CCC"']} 305 | output = tmpl.render(context) 306 | assert_equal expected, output 307 | end 308 | 309 | it "uses arg as context object when arg is not a hash object." do 310 | input = <<'END' 311 | title: <%= @title %> 312 | items: 313 | <% for item in @items %> 314 | - <%= item %> 315 | <% end %> 316 | END 317 | expected = <<'END' 318 | title: Example 319 | items: 320 | - 321 | - B&B 322 | - "CCC" 323 | END 324 | obj = Object.new 325 | obj.instance_variable_set('@title', 'Example') 326 | obj.instance_variable_set('@items', ['', 'B&B', '"CCC"']) 327 | output = template.from_str(input).render(obj) 328 | assert_equal expected, output 329 | end 330 | 331 | it "accepts nil as argument." do 332 | input = "self.class is <%= self.class %>" 333 | expected = "self.class is BabyErubis::TemplateContext" 334 | context = nil 335 | output = template.from_str(input).render(context) 336 | assert_equal expected, output 337 | end 338 | 339 | end 340 | 341 | 342 | describe '#compile()' do 343 | 344 | it "compiles ruby code into proc object." do 345 | assert_nil template.instance_variable_get('@_proc') 346 | template.compile("_buf = ''; _buf << (x).to_s; _buf") 347 | assert_kind_of Proc, template.instance_variable_get('@proc') 348 | end 349 | 350 | it "returns self." do 351 | assert_same template, template.compile("_buf = ''; _buf << (x).to_s; _buf") 352 | end 353 | 354 | it "takes filename and linenum." do 355 | begin 356 | template.compile("_buf = ''; _buf << (x).to_s; _buf", 'example.erb', 3) 357 | rescue SyntaxError => ex 358 | assert_match /\Aexample\.erb:3: syntax error/, ex.message 359 | end 360 | end 361 | 362 | it "uses '(eRuby)' as default filename and 1 as default linenum." do 363 | begin 364 | template.compile("_buf = ''; _buf << (x).to_s; _buf") 365 | rescue SyntaxError => ex 366 | assert_match /\A\(eRuby\):1: syntax error/, ex.message 367 | end 368 | end 369 | 370 | end 371 | 372 | 373 | describe '.from_file()' do 374 | 375 | it "reads template file and returns template object." do 376 | input = <<'END' 377 | title: <%= @title %> 378 | items: 379 | <% for item in @items %> 380 | - <%= item %> 381 | <% end %> 382 | END 383 | expected = <<'END' 384 | title: Example 385 | items: 386 | - 387 | - B&B 388 | - "CCC" 389 | END 390 | tmpfile = "test.#{rand()}.erb" 391 | File.open(tmpfile, 'wb') {|f| f.write(input) } 392 | begin 393 | template = BabyErubis::Text.new.from_file(tmpfile) 394 | context = {:title=>'Example', :items=>['', 'B&B', '"CCC"']} 395 | output = template.render(context) 396 | assert_equal expected, output 397 | ensure 398 | File.unlink(tmpfile) if File.exist?(tmpfile) 399 | end 400 | end 401 | 402 | it "reads template file with specified encoding." do 403 | input = "タイトル: <%= @title %>" 404 | expected = "タイトル: サンプル" 405 | tmpfile = "test.#{rand()}.erb" 406 | File.open(tmpfile, 'wb:utf-8') {|f| f.write(input) } 407 | begin 408 | # nothing should be raised 409 | template = BabyErubis::Text.new.from_file(tmpfile, 'utf-8') 410 | output = template.render(:title=>"サンプル") 411 | assert_equal expected, output 412 | assert_equal 'UTF-8', output.encoding.name 413 | # exception should be raised 414 | ex = assert_raises ArgumentError do 415 | template = BabyErubis::Text.new.from_file(tmpfile, 'us-ascii') 416 | end 417 | assert_equal "invalid byte sequence in US-ASCII", ex.message 418 | ensure 419 | File.unlink(tmpfile) if File.exist?(tmpfile) 420 | end 421 | end 422 | 423 | end 424 | 425 | 426 | end 427 | 428 | 429 | 430 | describe BabyErubis::HtmlTemplate do 431 | 432 | def _modify(ruby_code) 433 | if (''.freeze).equal?(''.freeze) 434 | return ruby_code.gsub(/([^'])';/m, "\\1'.freeze;") 435 | else 436 | return ruby_code 437 | end 438 | end 439 | 440 | input = <<'END' 441 | 442 |

<%= @title %>

443 |
    444 | <% for item in @items %> 445 | 446 |
  • <%= item %>
  • 447 | <% end %> 448 |
449 | 450 | END 451 | source = <<'END' 452 | _buf = ''; _buf << ' 453 |

'; _buf << escape(@title); _buf << '

454 |
    455 | '; for item in @items; 456 | _buf << ' 457 |
  • '; _buf << escape(item); _buf << '
  • 458 | '; end; 459 | _buf << '
460 | 461 | '; _buf.to_s 462 | END 463 | output = <<'END' 464 | 465 |

Example

466 |
    467 | 468 |
  • <AAA>
  • 469 | 470 |
  • B&B
  • 471 | 472 |
  • "CCC"
  • 473 |
474 | 475 | END 476 | 477 | 478 | describe '#parse()' do 479 | 480 | it "handles embedded expression with escaping." do 481 | tmpl = BabyErubis::Html.new.from_str(input) 482 | assert_equal _modify(source), tmpl.src 483 | end 484 | 485 | end 486 | 487 | 488 | describe '#render()' do 489 | 490 | it "renders context values with escaping." do 491 | tmpl = BabyErubis::Html.new.from_str(input) 492 | context = {:title=>'Example', :items=>['', 'B&B', '"CCC"']} 493 | assert_equal output, tmpl.render(context) 494 | end 495 | 496 | end 497 | 498 | 499 | end 500 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BabyErubis.rb 2 | ============= 3 | 4 | $Release: 0.0.0 $ 5 | 6 | BabyErubis is an yet another eRuby implementation, based on Erubis. 7 | 8 | * Small and fast 9 | * Easy to customize 10 | * Supports HTML as well as plain text 11 | * Accepts both template file and template string 12 | * Supports Ruby on Rails template 13 | 14 | BabyErubis supports Ruby >= 1.8 and Rubinius >= 2.0. 15 | 16 | 17 | 18 | Examples 19 | ======== 20 | 21 | 22 | Render template string: 23 | 24 | require 'baby_erubis' 25 | template = BabyErubis::Html.new.from_str <<'END', __FILE__, __LINE__+1 26 |

<%= @title %>

27 | <% for item in @items %> 28 |

<%= item %>

29 | <% end %> 30 | END 31 | context = {:title=>'Example', :items=>['A', 'B', 'C']} 32 | output = template.render(context) 33 | print output 34 | 35 | 36 | Render template file: 37 | 38 | require 'baby_erubis' 39 | template = BabyErubis::Html.new.from_file('example.html.erb', 'utf-8') 40 | context = {:title=>'Example', :items=>['A', 'B', 'C']} 41 | output = template.render(context) 42 | print output 43 | 44 | 45 | (Use `BabyErubis::Text` instead of `BabyErubis::Html` when rendering plain text.) 46 | 47 | 48 | Command-line examples (see `baby_erubis.rb --help` for details): 49 | 50 | ## convert eRuby file into Ruby code 51 | $ baby_erubis -x file.erb # text 52 | $ baby_erubis -xH file.erb # html 53 | $ baby_erubis -X file.erb # embedded code only 54 | ## render eRuby file with context data 55 | $ baby_erubis -c '{items: [A, B, C]}' file.erb # YAML 56 | $ baby_erubis -c '@items=["A","B","C"]' file.erb # Ruby 57 | $ baby_erubis -f data.yaml file.erb # or -f *.json, *.rb 58 | ## debug eRuby file 59 | $ baby_erubis -xH file.erb | ruby -wc # check syntax error 60 | $ baby_erubis -XHNU file.erb # show embedded ruby code 61 | 62 | 63 | 64 | Template Syntax 65 | =============== 66 | 67 | * `<% ... %>` : Ruby statement 68 | * `<%= ... %>` : Ruby expression with escaping 69 | * `<%== ... %>` : Ruby expression without escaping 70 | 71 | Expression in `<%= ... %>` is escaped according to template class. 72 | 73 | * `BabyErubis::Text` doesn't escape anything. 74 | It justs converts expression into a string. 75 | * `BabyErubis::Html` escapes html special characters. 76 | It converts `< > & " '` into `< > & " '` respectively. 77 | 78 | (Experimental) `<%- ... -%>` and `<%-= ... -%>` are handled same as 79 | `<% ... %>` and `<%= ... %>` respectively. 80 | 81 | (Experimental) Block argument expression supported since version 2.0. 82 | Example: 83 | 84 | ## template 85 | <%== form_for(:article) do |f| %> 86 | ... 87 | <% end %> 88 | 89 | ## compiled ruby code 90 | _buf << form_for(:article) do |f| _buf << ' 91 | ... 92 | '; end; 93 | 94 | 95 | 96 | Advanced Topics 97 | =============== 98 | 99 | 100 | Template Context 101 | ---------------- 102 | 103 | When rendering template, you can pass not only Hash object but also any object 104 | as context values. Internally, rendering method converts Hash object into 105 | `BabyErubis::TemplateContext` object automatically. 106 | 107 | Example: 108 | 109 | require 'baby_erubis' 110 | 111 | class MyApp 112 | include BabyErubis::HtmlEscaper # necessary to define escape() 113 | 114 | TEMPLATE = BabyErubis::Html.new.from_str <<-'END', __FILE__, __LINE__+1 115 | 116 | 117 |

Hello <%= @name %>!

118 | 119 | 120 | END 121 | 122 | def initialize(name) 123 | @name = name 124 | end 125 | 126 | def render() 127 | return TEMPLATE.render(self) # use self as context object 128 | end 129 | 130 | end 131 | 132 | if __FILE__ == $0 133 | print MyApp.new('World').render() 134 | end 135 | 136 | 137 | String#freeze() 138 | --------------- 139 | 140 | BabyErubis supports String#freeze() automatically when on Ruby version >= 2.1. 141 | And you can control whether to use freeze() or not. 142 | 143 | template_str = <<'END' 144 |
145 | <%= message %> 146 |
147 | END 148 | 149 | ## don't use freeze() 150 | t = BabyErubis::Text.new(:freeze=>false).from_str(template_str) 151 | print t.src 152 | # --- result --- 153 | # _buf = ''; _buf << '
154 | # '; _buf << (message).to_s; _buf << ' 155 | #
156 | # '; _buf.to_s 157 | 158 | ## use freeze() forcedly 159 | t = BabyErubis::Text.new(:freeze=>true).from_str(template_str) 160 | print t.src 161 | # --- result --- 162 | # _buf = ''; _buf << '
163 | # '.freeze; _buf << (message).to_s; _buf << ' 164 | #
165 | # '.freeze; _buf.to_s 166 | 167 | 168 | Ruby on Rails Template 169 | ---------------------- 170 | 171 | `BabyErubis::RailsTemplate` class generates Rails-style ruby code. 172 | 173 | require 'baby_erubis' 174 | require 'baby_erubis/rails' 175 | 176 | t = BabyErubis::RailsTemplate.new.from_str <<'END' 177 |
178 | <%= form_for :article do |f| %> 179 | ... 180 | <% end %> 181 |
182 | END 183 | print t.src 184 | 185 | Result: 186 | 187 | @output_buffer = output_buffer || ActionView::OutputBuffer.new;@output_buffer.safe_append='
188 | ';@output_buffer.append= form_for :article do |f| ;@output_buffer.safe_append=' 189 | ... 190 | '.freeze; end; 191 | @output_buffer.safe_append='
192 | ';@output_buffer.to_s 193 | 194 | You can check syntax of Rails template in command-line: 195 | 196 | $ baby_erubis -Rx app/views/articles/index.html.erb | ruby -wc 197 | 198 | 199 | (TODO: How to use BabyErubis in Ruby on Rails instead of Erubis) 200 | 201 | 202 | Define Rendering Methods 203 | ------------------------ 204 | 205 | It is very easy to use BabyErubis as template engine in your app or framework, 206 | because `BabyErubis/Renderer` module defines rendering methods: 207 | 208 | require 'baby_erubis' 209 | require 'baby_erubis/renderer' 210 | 211 | class MyController 212 | include BabyErubis::HtmlEscaper 213 | include BabyErubis::Renderer # !!!! 214 | 215 | ERUBY_PATH = ['.'] 216 | ERUBY_LAYOUT = :_layout 217 | ERUBY_HTML = BabyErubis::Html 218 | ERUBY_HTML_EXT = '.html.eruby' 219 | ERUBY_TEXT = BabyErubis::Text 220 | ERUBY_TEXT_EXT = '.eruby' 221 | ERUBY_CACHE = {} 222 | 223 | alias render_html eruby_render_html 224 | alias render_text eruby_render_text 225 | 226 | def index 227 | @items = ['A', 'B', 'C'] 228 | ## renders 'templates/welcome.html.eruby' 229 | html = render_html(:index) # renders 'index.html.eruby' 230 | return html 231 | end 232 | 233 | end 234 | 235 | `BabyErubis/Renderer` module defines the following methods: 236 | 237 | * `eruby_render_html(template_name, layout: true, encoding: 'utf-8')` -- 238 | renders HTML template with layout template. 239 | `layout` keyword argument is layout template name or boolean and use 240 | default layout name (= ERUBY_TEMPLATE_LAYOUT) when its value is true. 241 | * `eruby_render_text(template_name, layout: false, encoding: 'utf-8')` -- 242 | renders plain template. 243 | 244 | Template name can be Symbol or String: 245 | 246 | html = render_html(:foo) # renders 'foo.html.eruby' 247 | html = render_html("foo.html.eruby") # renders 'foo.html.eruby' 248 | 249 | text = render_text(:foo) # renders 'foo.eruby' 250 | text = render_text(:'foo.txt') # renders 'foo.txt.eruby' 251 | text = render_text("foo.txt.eruby") # renders 'foo.txt.eruby' 252 | 253 | Layout template example: 254 | 255 | <% 256 | ## you can specify parent layout template name 257 | #@_layout = :sitelayout 258 | %> 259 | 260 | 261 | 262 | 263 | <%= @page_title %> 264 | 265 | 266 |
267 | <%== @_content %> ## or <% _buf << @_content %> 268 |
269 | 270 | 271 | 272 | 273 | Customizing 274 | =========== 275 | 276 | 277 | Change Embed Pattern from '<% %>' to '{% %}' 278 | -------------------------------------------- 279 | 280 | Sample code: 281 | 282 | require 'baby_erubis' 283 | 284 | class MyTemplate < BabyErubis::Html 285 | 286 | rexp = BabyErubis::Template::PATTERN 287 | PATTERN = Regexp.compile(rexp.to_s.sub(/<%/, '\{%').sub(/%>/, '%\}')) 288 | 289 | def pattern 290 | PATTERN 291 | end 292 | 293 | end 294 | 295 | template = MyTemplate.new <<-'END' 296 | {% for item in @items %} 297 | - {%= item %} 298 | {% end %} 299 | END 300 | 301 | print template.render(:items=>['A', 'B', 'C']) 302 | 303 | Output: 304 | 305 | - A 306 | - B 307 | - C 308 | 309 | 310 | Strip Spaces in HTML Template 311 | ----------------------------- 312 | 313 | Sample code: 314 | 315 | require 'baby_erubis' 316 | 317 | class MyTemplate < BabyErubis::Html 318 | 319 | def parse(input, *args) 320 | stripped = input.gsub(/^[ \t]+ 328 | 329 |

Hello <%= @name %>!

330 | 331 | 332 | END 333 | 334 | print template.render(:name=>"Hello") 335 | 336 | Output: 337 | 338 | 339 | 340 |

Hello Hello!

341 | 342 | 343 | 344 | 345 | Layout Template 346 | --------------- 347 | 348 | Sample code: 349 | 350 | require 'baby_erubis' 351 | 352 | class MyApp 353 | include BabyErubis::HtmlEscaper # necessary to define escape() 354 | 355 | LAYOUT = BabyErubis::Html.new.from_str <<-'END', __FILE__, __LINE__+1 356 | 357 | 358 | <% _buf << @_content %> # or <%== @_content %> 359 | 360 | 361 | END 362 | 363 | TEMPLATE = BabyErubis::Html.new.from_str <<-'END', __FILE__, __LINE__+1 364 |

Hello <%= @name %>!

365 | END 366 | 367 | def initialize(name) 368 | @name = name 369 | end 370 | 371 | def render() 372 | @_content = TEMPLATE.render(self) 373 | return LAYOUT.render(self) 374 | end 375 | 376 | end 377 | 378 | if __FILE__ == $0 379 | print MyApp.new('World').render() 380 | end 381 | 382 | Output: 383 | 384 | 385 | 386 |

Hello World!

387 | 388 | 389 | 390 | 391 | Template Cache File 392 | ------------------- 393 | 394 | Sample code: 395 | 396 | require 'baby_erubis' 397 | require 'logger' 398 | 399 | $logger = Logger.new(STDERR) 400 | 401 | class MyTemplate < BabyErubis::Html 402 | 403 | def from_file(filename, encoding='utf-8') 404 | cachefile = "#{filename}.cache" 405 | timestamp = File.mtime(filename) 406 | has_cache = File.file?(cachefile) && File.mtime(cachefile) == timestamp 407 | if has_cache 408 | $logger.info("loading template from cache file: #{cachefile}") 409 | ruby_code = File.open(cachefile, "rb:#{encoding}") {|f| f.read } 410 | compile(ruby_code, filename, 1) 411 | else 412 | super(filename, encoding) 413 | $logger.info("creating template cache file: #{cachefile}") 414 | ruby_code = self.src 415 | tmpname = "#{cachefile}.#{rand().to_s[2,5]}" 416 | File.open(tmpname, "wb:#{encoding}") {|f| f.write(ruby_code) } 417 | File.utime(timestamp, timestamp, tmpname) 418 | File.rename(tmpname, cachefile) 419 | end 420 | return self 421 | end 422 | 423 | end 424 | 425 | p File.exist?('example.html.erb.cache') #=> false 426 | t = MyTemplate.new.from_file('example.html.erb') 427 | p File.exist?('example.html.erb.cache') #=> true 428 | 429 | 430 | 431 | Todo 432 | ==== 433 | 434 | * [Done] Support Rails syntax (= `<%= form_for do |f| %>`) 435 | 436 | 437 | 438 | Changes 439 | ======= 440 | 441 | 442 | Release 2.2.0 (2016-09-19) 443 | -------------------------- 444 | 445 | * [change] `BabyErubis::Renderer#eruby_render_html()` and 446 | `#eruby_render_text()` distinguish symbol and string template name: 447 | 448 | ``` 449 | ## previous 450 | html = eruby_render_html(:foo) # render 'foo.html.eruby' 451 | html = eruby_render_html("foo") # render 'foo.html.eruby' 452 | html = eruby_render_html("foo.html.eruby") # render 'foo.html.eruby.html.eruby' 453 | 454 | ## current 455 | html = eruby_render_html(:foo) # render 'foo.html.eruby' 456 | html = eruby_render_html("foo") # render 'foo' 457 | html = eruby_render_html("foo.html.eruby") # render 'foo.html.eruby' 458 | 459 | text = eruby_render_text(:foo) # render 'foo.eruby' 460 | text = eruby_render_text(:'foo.txt') # render 'foo.txt.eruby' 461 | text = eruby_render_text("foo.txt.eruby") # render 'foo.txt.eruby' 462 | ``` 463 | 464 | 465 | Release 2.1.2 (2015-10-30) 466 | -------------------------- 467 | 468 | * [bugfix] Fix typo (thanks catatsuy) 469 | 470 | 471 | Release 2.1.1 (2015-10-27) 472 | -------------------------- 473 | 474 | * [bugfix] Add a file 475 | 476 | 477 | Release 2.1.0 (2015-10-27) 478 | -------------------------- 479 | 480 | * [enhance] Add new helper module `BabyErubis::Renderer` 481 | 482 | 483 | Release 2.0.0 (2014-12-09) 484 | -------------------------- 485 | 486 | * [enhance] Ruby on Rails template support 487 | * [enhance] Block argument expression support 488 | * [enhance] New command-line option '-R' and '--format=rails' 489 | 490 | 491 | Release 1.0.0 (2014-05-17) 492 | -------------------------- 493 | 494 | * [enhance] Provides script file `bin/baby_erubis`. 495 | * [enhance] Supports Ruby 1.8 and Rubinius 2.x. 496 | * [change] Define 'BabyErubis::RELEASE'. 497 | * [bugfix] 'Template#render()' creates context object when nil passed. 498 | 499 | 500 | Release 0.1.0 (2014-05-06) 501 | -------------------------- 502 | 503 | * Public release 504 | 505 | 506 | 507 | License 508 | ======= 509 | 510 | $License: MIT License $ 511 | 512 | 513 | 514 | Copyright 515 | ========= 516 | 517 | $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 518 | -------------------------------------------------------------------------------- /bin/baby_erubis: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | ### 5 | ### $Release: 0.0.0 $ 6 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 7 | ### $License: MIT License $ 8 | ### 9 | 10 | 11 | #require 'yaml' # on-demand load 12 | #require 'json' # on-demand load 13 | require 'baby_erubis' 14 | 15 | 16 | module Cmdopt 17 | 18 | 19 | def self.new(*args) 20 | return Parser.new(*args) 21 | end 22 | 23 | def self.new_with(*optdef_strs) 24 | parser = Parser.new 25 | optdef_strs.each do |str| 26 | parser.option(str) 27 | end 28 | return parser 29 | end 30 | 31 | 32 | class Schema 33 | 34 | def initialize(optstr) 35 | short = long = nil 36 | case optstr 37 | when /\A(?:<(\w+)>)? *-(\w) *, *--(\w+)(?:\[=(\S+)\]|=(\S+))?[ \t]*(?::[ \t]*(.*))?\z/ 38 | name, short, long, optarg, arg, desc = $1, $2, $3, $4, $5, $6 39 | when /\A(?:<(\w+)>)? *-(\w)(?:\[(\S+)\]| +(\S+)?)?[ \t]*(?::[ \t]*(.*))?\z/ 40 | name, short, optarg, arg, desc = $1, $2, $3, $4, $5 41 | when /\A(?:<(\w+)>)? *--(\w+)(?:\[=(\S+)\]|=(\S+))?[ \t]*(?::[ \t]*(.*))?\z/ 42 | name, long, optarg, arg, desc = $1, $2, $3, $4, $5 43 | else 44 | raise SchemaError.new("'#{optstr}': invalid option definition.") 45 | end 46 | @name = name 47 | @short = short 48 | @long = long 49 | @arg = arg || optarg 50 | @desc = desc 51 | @arg_required = !! arg 52 | @arg_optional = !! optarg 53 | @validations = [] 54 | @action = nil 55 | # 56 | setup() 57 | end 58 | 59 | attr_reader :name, :short, :long, :arg, :desc 60 | 61 | def setup 62 | if @arg == 'N' || @optarg == 'N' 63 | _add_validation(proc {|arg| 64 | "integer expected." unless arg =~ /\A\d+\z/ 65 | }) 66 | _set_action(proc {|options, arg| 67 | options[canonical_name()] = arg == true ? arg : arg.to_i 68 | }) 69 | end 70 | end 71 | private :setup 72 | 73 | def arg_required? 74 | @arg_required 75 | end 76 | 77 | def arg_optional? 78 | @arg_optional 79 | end 80 | 81 | def validate(optarg) 82 | @validations.each do |block| 83 | errmsg = block.call(optarg) 84 | return errmsg if errmsg 85 | end 86 | return nil 87 | end 88 | 89 | def run_action(options, optarg) 90 | if @action 91 | @action.call(options, optarg) 92 | else 93 | options[canonical_name()] = optarg 94 | end 95 | nil 96 | end 97 | 98 | def canonical_name 99 | return @name || @long || @short 100 | end 101 | 102 | def _add_validation(block) 103 | @validations << block 104 | end 105 | 106 | def _set_action(block) 107 | @action = block 108 | end 109 | 110 | def to_help_message(width) 111 | if @short && @long 112 | if @optarg ; s = "-#{@short}, --#{@long}[=#{@optarg}]" 113 | elsif @arg ; s = "-#{@short}, --#{@long}=#{@arg}" 114 | else ; s = "-#{@short}, --#{@long}" 115 | end 116 | elsif @short 117 | if @optarg ; s = "-#{@short}[#{@optarg}]" 118 | elsif @arg ; s = "-#{@short} #{@arg}" 119 | else ; s = "-#{@short}" 120 | end 121 | elsif @long 122 | if @optarg ; s = " --#{@long}[=#{@optarg}]" 123 | elsif @arg ; s = " --#{@long}=#{@arg}" 124 | else ; s = " --#{@long}" 125 | end 126 | end 127 | s << ' ' * (width - s.length) if s.length < width 128 | s << ': ' << @desc.to_s 129 | s << "\n" 130 | return s 131 | end 132 | 133 | end 134 | 135 | 136 | class SchemaError < StandardError 137 | end 138 | 139 | 140 | class SchemaBuilder 141 | 142 | def initialize(schema) 143 | @schema = schema 144 | end 145 | 146 | def validation(&block) 147 | @schema._add_validation(block) 148 | return self 149 | end 150 | 151 | def action(&block) 152 | @schema._set_action(block) 153 | return self 154 | end 155 | 156 | end 157 | 158 | 159 | class Parser 160 | 161 | def initialize(cmdname=true) 162 | cmdname = File.basename($0) if cmdname == true 163 | @cmdname = cmdname 164 | @schemas = [] 165 | end 166 | 167 | def option(optstr) 168 | schema = Schema.new(optstr) 169 | @schemas << schema 170 | return SchemaBuilder.new(schema) 171 | end 172 | 173 | def parse(argv) 174 | options = {} 175 | while argv[0] && argv[0] =~ /\A-/ 176 | optstr = argv.shift 177 | if optstr == '--' 178 | break 179 | elsif optstr =~ /\A--(\w+)(?:=(.*))?/ 180 | optname = $1 181 | optarg = $2 || true 182 | schema = @schemas.find {|sch| sch.long == optname } or 183 | raise error("--#{optname}: unknown option.") 184 | if schema.arg_required? && optarg == true 185 | raise error("--#{optname}: argument required.") 186 | end 187 | errmsg = schema.validate(optarg) 188 | raise error("#{optstr}: #{errmsg}") if errmsg 189 | schema.run_action(options, optarg) 190 | else 191 | i = 1 192 | while i < optstr.length 193 | optch = optstr[i, 1] 194 | schema = @schemas.find {|sch| sch.short == optch } or 195 | raise error("-#{optch}: unknown option.") 196 | if schema.arg_required? 197 | optarg = (i+1 < optstr.length) ? optstr[(i+1)..-1] : argv.shift or 198 | raise error("-#{optch}: argument required.") 199 | errmsg = schema.validate(optarg) 200 | raise error("-#{optch} #{optarg}: #{errmsg}") if errmsg 201 | i = optstr.length 202 | elsif schema.arg_optional? 203 | optarg = (i+1 < optstr.length) ? optstr[(i+1)..-1] : true 204 | errmsg = optarg != true ? schema.validate(optarg) : nil 205 | raise error("-#{optch}#{optarg}: #{errmsg}") if errmsg 206 | i = optstr.length 207 | else 208 | optarg = true 209 | i += 1 210 | end 211 | schema.run_action(options, optarg) 212 | end 213 | end 214 | end 215 | return options 216 | end 217 | 218 | def help_message(width=30) 219 | s = "" 220 | @schemas.each do |schema| 221 | s << " " << schema.to_help_message(width-2) if schema.desc 222 | end 223 | return s 224 | end 225 | 226 | private 227 | 228 | def error(message) 229 | message = "#{@cmdname}: #{message}" if @cmdname && @cmdname.length > 0 230 | return ParseError.new(message) 231 | end 232 | 233 | end 234 | 235 | 236 | class ParseError < StandardError 237 | end 238 | 239 | 240 | end 241 | 242 | 243 | module BabyErubis 244 | 245 | 246 | module HideTextEnhander 247 | 248 | def add_text(src, text) 249 | src << text.to_s.gsub(/^.*\n/, "\n").gsub(/./, ' ') 250 | end 251 | 252 | end 253 | 254 | 255 | end 256 | 257 | 258 | class Main 259 | 260 | def self.main(argv=ARGV) 261 | begin 262 | self.new.run(argv) 263 | return 0 264 | rescue Cmdopt::ParseError => ex 265 | $stderr << ex.message << "\n" 266 | return 1 267 | end 268 | end 269 | 270 | def initialize(cmdname=File.basename($0)) 271 | @cmdname = cmdname 272 | end 273 | 274 | def run(argv) 275 | parser = build_parser() 276 | options = parser.parse(argv) 277 | if options['help'] 278 | $stdout << build_help_message(parser) 279 | return 280 | end 281 | if options['version'] 282 | $stdout << BabyErubis::RELEASE << "\n" 283 | return 284 | end 285 | # 286 | format = options['format'] || (options['H'] && 'html') || (options['R'] && 'rails') 287 | klass = handle_format(format) 288 | if options['X'] 289 | klass = Class.new(klass) 290 | klass.class_eval { include BabyErubis::HideTextEnhander } 291 | end 292 | # 293 | freeze = handle_freeze(options['freeze']) 294 | tplopt = {:freeze=>freeze} 295 | # 296 | encoding = options['encoding'] || 'utf-8' 297 | context = options['c'] 298 | datafile = options['f'] 299 | show_src = options['x'] || options['X'] 300 | (argv.empty? ? [nil] : argv).each do |filename| 301 | if filename 302 | template = klass.new(tplopt).from_file(filename, encoding) 303 | else 304 | template_str = $stdin.read() 305 | template = klass.new(tplopt).from_str(template_str, '(stdin)') 306 | end 307 | if show_src 308 | $stdout << edit_src(template.src, options['N'], options['U'], options['C']) 309 | else 310 | ctxobj = template.new_context({}) 311 | handle_datafile(datafile, encoding, ctxobj) if datafile 312 | handle_context(context, encoding, ctxobj) if context 313 | output = template.render(ctxobj) 314 | $stdout << output 315 | end 316 | end 317 | end 318 | 319 | private 320 | 321 | def build_parser 322 | parser = Cmdopt.new(@cmdname) 323 | parser.option("-h, --help : help") 324 | parser.option("-v, --version : version") 325 | parser.option("-x : show ruby code") 326 | parser.option("-X : show ruby code only (no text part)") 327 | parser.option("-N : numbering: add line numbers (for '-x/-X')") 328 | parser.option("-U : unique: compress empty lines (for '-x/-X')") 329 | parser.option("-C : compact: remove empty lines (for '-x/-X')") 330 | parser.option("-c context : context string (yaml inline style or ruby code)") 331 | parser.option("-f file : context data file (*.yaml, *.json, or *.rb)") 332 | parser.option("-H : same as --format=html") 333 | parser.option("-R : same as --format=rails") 334 | parser.option(" --format=format : 'text', 'html' or 'rails' (default: text)")\ 335 | .validation {|arg| "'text', 'html' or 'rails' expected" if arg !~ /\A(text|html|rails)\z/ } 336 | parser.option(" --encoding=name : encoding (default: utf-8)") 337 | parser.option(" --freeze={true|false} : use String#freeze() or not")\ 338 | .validation {|arg| "'true' or 'false' expected" if arg !~ /\A(true|false)\z/ } 339 | parser.option("-D") 340 | return parser 341 | end 342 | 343 | def build_help_message(parser) 344 | s = "Usage: #{@cmdname} [..options..] [erubyfile]\n" 345 | s << parser.help_message(28) 346 | s << "\n" 347 | s << <<"END" 348 | Example: 349 | ## convert eRuby file into Ruby code 350 | $ #{@cmdname} -x file.erb # text 351 | $ #{@cmdname} -xH file.erb # html 352 | $ #{@cmdname} -X file.erb # embedded code only 353 | ## render eRuby file with context data 354 | $ #{@cmdname} -c '{items: [A, B, C]}' file.erb # YAML 355 | $ #{@cmdname} -c '@items=["A","B","C"]' file.erb # Ruby 356 | $ #{@cmdname} -f data.yaml file.erb # or -f *.json, *.rb 357 | ## debug eRuby file 358 | $ #{@cmdname} -xH file.erb | ruby -wc # check syntax error 359 | $ #{@cmdname} -XHNU file.erb # show embedded ruby code 360 | END 361 | return s 362 | end 363 | 364 | def handle_format(format) 365 | case format 366 | when nil ; return BabyErubis::Text 367 | when 'text'; return BabyErubis::Text 368 | when 'html'; return BabyErubis::Html 369 | when 'rails'; require 'baby_erubis/rails'; return BabyErubis::RailsTemplate 370 | else 371 | raise "** unreachable: format=#{format.inspect}" 372 | end 373 | end 374 | 375 | def handle_freeze(freeze) 376 | case freeze 377 | when nil ; return nil 378 | when 'true' ; return true 379 | when 'false'; return false 380 | else 381 | raise "** unreachable: options['freeze']=#{options['freeze'].inspect}" 382 | end 383 | end 384 | 385 | def handle_context(context_str, encoding, context_obj) 386 | return nil if context_str.nil? || context_str.empty? 387 | if context_str =~ /\A\{/ 388 | kind = 'YAML' 389 | require 'yaml' 390 | dict = YAML.load(context_str) # raises Psych::SyntaxError 391 | dict.is_a?(Hash) or 392 | raise Cmdopt::ParseError.new("-c '#{context_str}': YAML mapping expected.") 393 | dict.each {|k, v| context_obj.instance_variable_set("@#{k}", v) } 394 | else 395 | kind = 'Ruby' 396 | _eval(context_str, context_obj) # raises SyntaxError 397 | end 398 | return context_obj 399 | #rescue Psych::SyntaxError, SyntaxError => ex 400 | rescue Exception => ex 401 | errmsg = ex.to_s 402 | case ex.class.name 403 | when 'Psych::SyntaxError'; errmsg = errmsg.sub(/\(\): /, '') 404 | when 'SyntaxError'; errmsg = errmsg.sub(/\(eval\):\d: syntax error, /, '') 405 | else 406 | if ex.class == ArgumentError && errmsg =~ /^syntax error on line \d+, (col \d+)/ 407 | errmsg = $1 # for Rubinius 408 | else 409 | raise ex 410 | end 411 | end 412 | raise Cmdopt::ParseError.new("-c '#{context_str}': #{kind} syntax error: (#{ex.class.name}) #{errmsg}") 413 | end 414 | 415 | def handle_datafile(datafile, encoding, context_obj) 416 | return nil unless datafile 417 | case datafile 418 | when /\.ya?ml\z/ 419 | kind = 'YAML' 420 | require 'yaml' 421 | dict = File.open(datafile, "rb:utf-8") {|f| YAML.load(f) } # raises Psych::SyntaxError 422 | dict.is_a?(Hash) or 423 | raise Cmdopt::ParseError.new("-f #{datafile}: YAML mapping expected.") 424 | dict.each {|k, v| context_obj.instance_variable_set("@#{k}", v) } 425 | when /\.json\z/ 426 | kind = 'JSON' 427 | require 'json' 428 | json_str = File.open(datafile, "rb:utf-8") {|f| f.read() } 429 | dict = JSON.load(json_str) # raises JSON::ParserError 430 | dict.is_a?(Hash) or 431 | raise Cmdopt::ParseError.new("-f #{datafile}: JSON object expected.") 432 | dict.each {|k, v| context_obj.instance_variable_set("@#{k}", v) } 433 | when /\.rb\z/ 434 | kind = 'Ruby' 435 | context_str = File.open(datafile, "rb:utf-8") {|f| f.read() } 436 | _eval(context_str, context_obj) # raises SyntaxError 437 | else 438 | raise Cmdopt::ParseError.new("-f #{datafile}: unknown suffix (expected '.yaml', '.json', or '.rb').") 439 | end 440 | return context_obj 441 | rescue Errno::ENOENT => ex 442 | raise Cmdopt::ParseError.new("-f #{datafile}: file not found.") 443 | #rescue Psych::SyntaxError, JSON::ParserError, SyntaxError => ex 444 | rescue Exception => ex 445 | errmsg = ex.to_s 446 | case ex.class.name 447 | when 'Psych::SyntaxError'; errmsg = errmsg.sub(/\(\): /, '') 448 | when 'JSON::ParserError'; errmsg = errmsg.sub(/^(\d+): (.*) at .*\n?/, '\1: \2') 449 | when 'SyntaxError'; errmsg = errmsg.sub(/\(eval\):\d: syntax error, /, '') 450 | else 451 | if ex.class == ArgumentError && errmsg =~ /^syntax error on line \d+, (col \d+)/ 452 | errmsg = $1 # for Rubinius 453 | else 454 | raise ex 455 | end 456 | end 457 | raise Cmdopt::ParseError.new("-f #{datafile}: #{kind} syntax error: (#{ex.class.name}) #{errmsg}") 458 | end 459 | 460 | def _eval(_context_str, _context_obj) 461 | _context_obj.instance_eval(_context_str) 462 | end 463 | 464 | def edit_src(src, numbering, unique, compact) 465 | if numbering # -N 466 | i = 0 467 | src = src.gsub(/^/) { "%4d: " % (i+=1) } 468 | src = src.gsub(/(^ *\d+:\s*\n)+/, "\n") if unique # -U 469 | src = src.gsub(/^ *\d+:\s*\n/, '') if compact # -C 470 | else 471 | src = src.gsub(/(^\s*\n)+/, "\n") if unique # -U 472 | src = src.gsub(/^\s*\n/, '') if compact # -C 473 | end 474 | return src 475 | end 476 | 477 | 478 | end 479 | 480 | 481 | #if __FILE__ == $0 482 | exit Main.main() unless defined? NOEXEC_SCRIPT 483 | #end 484 | -------------------------------------------------------------------------------- /test/script_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ### 4 | ### $Release: 0.0.0 $ 5 | ### $Copyright: copyright(c) 2014-2015 kuwata-lab.com all rights reserved $ 6 | ### $License: MIT License $ 7 | ### 8 | 9 | libpath = File.class_eval { join(dirname(dirname(__FILE__)), 'lib') } 10 | $: << libpath unless $:.include?(libpath) 11 | 12 | require 'minitest/autorun' 13 | 14 | ## enforce not to use String#freeze() even if RUBY_VERSION >= 2.1 15 | #require 'baby_erubis' 16 | #BabyErubis::Template.class_eval do 17 | # remove_const :FREEZE 18 | # FREEZE = false 19 | #end 20 | 21 | ## load script file ('bin/baby_erubis.rb') 22 | NOEXEC_SCRIPT = true 23 | load File.join(File.dirname(libpath), 'bin', 'baby_erubis') 24 | 25 | ## helper to steal stdin, stdout and stderr 26 | require 'stringio' 27 | def dummy_stdio(input=nil) 28 | stdin, stdout, stderr = $stdin, $stdout, $stderr 29 | $stdin = StringIO.new(input || '') 30 | $stdout = StringIO.new 31 | $stderr = StringIO.new 32 | yield 33 | return $stdout.string, $stderr.string 34 | ensure 35 | $stdin = stdin 36 | $stdout = stdout 37 | $stderr = stderr 38 | end 39 | 40 | ## helper to create dummy file temporarily 41 | def with_tmpfile(filename, content) 42 | File.open(filename, 'wb') {|f| f.write(content) } 43 | yield filename 44 | ensure 45 | File.unlink(filename) if File.exist?(filename) 46 | end 47 | 48 | ## helper to create eruby file temporarily 49 | def with_erubyfile(content=nil) 50 | content ||= ERUBY_TEMPLATE 51 | filename = "test_eruby.rhtml" 52 | with_tmpfile(filename, content) do 53 | yield filename 54 | end 55 | end 56 | 57 | 58 | describe Main do 59 | 60 | def _modify(ruby_code) 61 | if (''.freeze).equal?(''.freeze) 62 | return ruby_code.gsub(/([^'])';/m, "\\1'.freeze;") 63 | else 64 | return ruby_code 65 | end 66 | end 67 | 68 | def on_rubinius? 69 | return defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 70 | end 71 | 72 | help_message = <<'END'.gsub(/\$SCRIPT/, File.basename($0)) 73 | Usage: $SCRIPT [..options..] [erubyfile] 74 | -h, --help : help 75 | -v, --version : version 76 | -x : show ruby code 77 | -X : show ruby code only (no text part) 78 | -N : numbering: add line numbers (for '-x/-X') 79 | -U : unique: compress empty lines (for '-x/-X') 80 | -C : compact: remove empty lines (for '-x/-X') 81 | -c context : context string (yaml inline style or ruby code) 82 | -f file : context data file (*.yaml, *.json, or *.rb) 83 | -H : same as --format=html 84 | -R : same as --format=rails 85 | --format=format : 'text', 'html' or 'rails' (default: text) 86 | --encoding=name : encoding (default: utf-8) 87 | --freeze={true|false} : use String#freeze() or not 88 | 89 | Example: 90 | ## convert eRuby file into Ruby code 91 | $ $SCRIPT -x file.erb # text 92 | $ $SCRIPT -xH file.erb # html 93 | $ $SCRIPT -X file.erb # embedded code only 94 | ## render eRuby file with context data 95 | $ $SCRIPT -c '{items: [A, B, C]}' file.erb # YAML 96 | $ $SCRIPT -c '@items=["A","B","C"]' file.erb # Ruby 97 | $ $SCRIPT -f data.yaml file.erb # or -f *.json, *.rb 98 | ## debug eRuby file 99 | $ $SCRIPT -xH file.erb | ruby -wc # check syntax error 100 | $ $SCRIPT -XHNU file.erb # show embedded ruby code 101 | END 102 | 103 | ERUBY_TEMPLATE = <<'END' 104 | 105 | 106 |

<%= @title %>

107 |

<%== @title %>

108 |
109 |
    110 | <% for item in @items %> 111 |
  • <%= item %>
  • 112 | <% end %> 113 |
114 |
115 | 116 | 117 | END 118 | SOURCE_TEXT = <<'END' 119 | _buf = ''; _buf << ' 120 | 121 |

'; _buf << (@title).to_s; _buf << '

122 |

'; _buf << (@title).to_s; _buf << '

123 |
124 |
    125 | '; for item in @items; 126 | _buf << '
  • '; _buf << (item).to_s; _buf << '
  • 127 | '; end; 128 | _buf << '
129 |
130 | 131 | 132 | '; _buf.to_s 133 | END 134 | SOURCE_HTML = <<'END' 135 | _buf = ''; _buf << ' 136 | 137 |

'; _buf << escape(@title); _buf << '

138 |

'; _buf << (@title).to_s; _buf << '

139 |
140 |
    141 | '; for item in @items; 142 | _buf << '
  • '; _buf << escape(item); _buf << '
  • 143 | '; end; 144 | _buf << '
145 |
146 | 147 | 148 | '; _buf.to_s 149 | END 150 | SOURCE_RAILS = <<'END' 151 | @output_buffer = output_buffer || ActionView::OutputBuffer.new;@output_buffer.safe_append=' 152 | 153 |

';@output_buffer.append=(@title);@output_buffer.safe_append='

154 |

';@output_buffer.safe_append=(@title);@output_buffer.safe_append='

155 |
156 |
    157 | '; for item in @items; 158 | @output_buffer.safe_append='
  • ';@output_buffer.append=(item);@output_buffer.safe_append='
  • 159 | '; end; 160 | @output_buffer.safe_append='
161 |
162 | 163 | 164 | ';@output_buffer.to_s 165 | END 166 | SOURCE_NO_TEXT = <<'END' 167 | _buf = ''; 168 | 169 | _buf << (@title).to_s; 170 | _buf << (@title).to_s; 171 | 172 | 173 | for item in @items; 174 | _buf << (item).to_s; 175 | end; 176 | 177 | 178 | 179 | _buf.to_s 180 | END 181 | OUTPUT_HTML = <<'END' 182 | 183 | 184 |

Love&Peace

185 |

Love&Peace

186 |
187 |
    188 |
  • A
  • 189 |
  • B
  • 190 |
  • C
  • 191 |
192 |
193 | 194 | 195 | END 196 | OUTPUT_TEXT = OUTPUT_HTML.sub(/&/, '&') 197 | 198 | 199 | describe '-h, --help' do 200 | 201 | it "prints help message." do 202 | sout, serr = dummy_stdio { Main.main(["-h"]) } 203 | assert_equal help_message, sout 204 | assert_equal "", serr 205 | sout, serr = dummy_stdio { Main.main(["--help"]) } 206 | assert_equal help_message, sout 207 | assert_equal "", serr 208 | end 209 | 210 | end 211 | 212 | 213 | describe '-v, --version' do 214 | 215 | it "prints release version." do 216 | expected = "#{BabyErubis::RELEASE}\n" 217 | sout, serr = dummy_stdio { Main.main(["-v"]) } 218 | assert_equal expected, sout 219 | assert_equal "", serr 220 | sout, serr = dummy_stdio { Main.main(["--version"]) } 221 | assert_equal expected, sout 222 | assert_equal "", serr 223 | end 224 | 225 | end 226 | 227 | 228 | describe '-x' do 229 | 230 | it "shows ruby code compiled." do 231 | sout, serr = with_erubyfile do |fname| 232 | dummy_stdio { Main.main(['-x', fname]) } 233 | end 234 | assert_equal _modify(SOURCE_TEXT), sout 235 | assert_equal "", serr 236 | end 237 | 238 | it "reads stdin when no file specified." do 239 | sout, serr = dummy_stdio(ERUBY_TEMPLATE) { Main.main(['-x']) } 240 | assert_equal _modify(SOURCE_TEXT), sout 241 | assert_equal "", serr 242 | end 243 | 244 | end 245 | 246 | 247 | describe '-X' do 248 | 249 | it "shows ruby code only (no text part)." do 250 | expected = <<'END' 251 | _buf = ''; 252 | 253 | _buf << (@title).to_s; 254 | _buf << (@title).to_s; 255 | 256 | 257 | for item in @items; 258 | _buf << (item).to_s; 259 | end; 260 | 261 | 262 | 263 | 264 | _buf.to_s 265 | END 266 | sout, serr = with_erubyfile do |fname| 267 | dummy_stdio { Main.main(['-X', fname]) } 268 | end 269 | assert_equal expected, sout 270 | assert_equal "", serr 271 | end 272 | 273 | end 274 | 275 | 276 | describe '-N' do 277 | 278 | it "adds line numbers." do 279 | expected = <<'END' 280 | 1: _buf = ''; 281 | 2: 282 | 3: _buf << (@title).to_s; 283 | 4: _buf << (@title).to_s; 284 | 5: 285 | 6: 286 | 7: for item in @items; 287 | 8: _buf << (item).to_s; 288 | 9: end; 289 | 10: 290 | 11: 291 | 12: 292 | 13: 293 | 14: _buf.to_s 294 | END 295 | sout, serr = with_erubyfile do |fname| 296 | dummy_stdio { Main.main(['-XN', fname]) } 297 | end 298 | assert_equal expected, sout 299 | assert_equal "", serr 300 | end 301 | 302 | end 303 | 304 | 305 | describe '-U' do 306 | 307 | it "compresses empty lines." do 308 | expected = <<'END' 309 | 1: _buf = ''; 310 | 311 | 3: _buf << (@title).to_s; 312 | 4: _buf << (@title).to_s; 313 | 314 | 7: for item in @items; 315 | 8: _buf << (item).to_s; 316 | 9: end; 317 | 318 | 14: _buf.to_s 319 | END 320 | sout, serr = with_erubyfile do |fname| 321 | dummy_stdio { Main.main(['-XNU', fname]) } 322 | end 323 | assert_equal expected, sout 324 | assert_equal "", serr 325 | end 326 | 327 | end 328 | 329 | 330 | describe '-C' do 331 | 332 | it "removes empty lines." do 333 | expected = <<'END' 334 | 1: _buf = ''; 335 | 3: _buf << (@title).to_s; 336 | 4: _buf << (@title).to_s; 337 | 7: for item in @items; 338 | 8: _buf << (item).to_s; 339 | 9: end; 340 | 14: _buf.to_s 341 | END 342 | sout, serr = with_erubyfile do |fname| 343 | dummy_stdio { Main.main(['-XNC', fname]) } 344 | end 345 | assert_equal expected, sout 346 | assert_equal "", serr 347 | end 348 | 349 | end 350 | 351 | 352 | describe '-H' do 353 | 354 | it "escapes expressions." do 355 | sout, serr = with_erubyfile do |fname| 356 | dummy_stdio { Main.main(['-Hx', fname]) } 357 | end 358 | assert_equal _modify(SOURCE_HTML), sout 359 | assert_equal "", serr 360 | end 361 | 362 | end 363 | 364 | 365 | describe '-R' do 366 | 367 | it "uses Rails-style template." do 368 | sout, serr = with_erubyfile do |fname| 369 | dummy_stdio { Main.main(['-Rx', fname]) } 370 | end 371 | assert_equal _modify(SOURCE_RAILS), sout 372 | assert_equal "", serr 373 | end 374 | 375 | end 376 | 377 | 378 | describe '-c cotnext' do 379 | 380 | it "can specify context data in YAML format." do 381 | context_str = "{title: Love&Peace, items: [A, B, C]}" 382 | sout, serr = with_erubyfile do |fname| 383 | dummy_stdio { Main.main(['-Hc', context_str, fname]) } 384 | end 385 | assert_equal OUTPUT_HTML, sout 386 | assert_equal "", serr 387 | ## when syntax error exists 388 | context_str = "{title:Love&Peace,items:[A,B,C]" 389 | sout, serr = with_erubyfile do |fname| 390 | dummy_stdio { Main.main(['-Hc', context_str, fname]) } 391 | end 392 | assert_equal "", sout 393 | if on_rubinius? 394 | assert_equal "-c '{title:Love&Peace,items:[A,B,C]': YAML syntax error: (ArgumentError) col 31\n", serr 395 | else 396 | assert_equal "-c '{title:Love&Peace,items:[A,B,C]': YAML syntax error: (Psych::SyntaxError) found unexpected ':' while scanning a plain scalar at line 1 column 2\n", serr 397 | end 398 | end 399 | 400 | it "can specify context data as Ruby code." do 401 | context_str = "@title = 'Love&Peace'; @items = ['A','B','C']" 402 | sout, serr = with_erubyfile do |fname| 403 | dummy_stdio { Main.main(['-Hc', context_str, fname]) } 404 | end 405 | assert_equal OUTPUT_HTML, sout 406 | assert_equal "", serr 407 | ## when syntax error exists 408 | context_str = "@title = 'Love&Peace' @items = ['A','B','C']" 409 | sout, serr = with_erubyfile do |fname| 410 | dummy_stdio { Main.main(['-Hc', context_str, fname]) } 411 | end 412 | expected = "-c '@title = 'Love&Peace' @items = ['A','B','C']': Ruby syntax error: (SyntaxError) unexpected tIVAR, expecting $end 413 | @title = 'Love&Peace' @items = ['A','B','C'] 414 | ^ 415 | " 416 | expected = expected.sub(/\$end/, "end-of-input") if RUBY_VERSION =~ /^2\./ 417 | expected = "-c '@title = 'Love&Peace' @items = ['A','B','C']': Ruby syntax error: (SyntaxError) expecting $end: (eval):1:28\n" if on_rubinius? 418 | assert_equal "", sout 419 | assert_equal expected, serr 420 | end 421 | 422 | end 423 | 424 | 425 | describe '-f datafile' do 426 | 427 | it "can specify context data in YAML format." do 428 | ctx_str = "{title: Love&Peace, items: [A, B, C]}" 429 | ctx_file = "tmpdata.yaml" 430 | sout, serr = with_erubyfile do |fname| 431 | with_tmpfile(ctx_file, ctx_str) do 432 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 433 | end 434 | end 435 | assert_equal OUTPUT_HTML, sout 436 | assert_equal "", serr 437 | ## when file not found 438 | sout, serr = with_erubyfile do |fname| 439 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 440 | end 441 | assert_equal "", sout 442 | assert_equal "-f #{ctx_file}: file not found.\n", serr 443 | ## when syntax error exists 444 | ctx_str = "{title:Love&Peace,items:[A, B, C]" 445 | sout, serr = with_erubyfile do |fname| 446 | with_tmpfile(ctx_file, ctx_str) do 447 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 448 | end 449 | end 450 | assert_equal "", sout 451 | if on_rubinius? 452 | assert_equal "-f #{ctx_file}: YAML syntax error: (ArgumentError) col 33\n", serr 453 | else 454 | assert_equal "-f #{ctx_file}: YAML syntax error: (Psych::SyntaxError) found unexpected ':' while scanning a plain scalar at line 1 column 2\n", serr 455 | end 456 | end 457 | 458 | it "can specify context data in JSON format." do 459 | ctx_str = '{"title":"Love&Peace","items":["A","B","C"]}' 460 | ctx_file = "tmpdata.json" 461 | sout, serr = with_erubyfile do |fname| 462 | with_tmpfile(ctx_file, ctx_str) do 463 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 464 | end 465 | end 466 | assert_equal OUTPUT_HTML, sout 467 | assert_equal "", serr 468 | ## when file not found 469 | sout, serr = with_erubyfile do |fname| 470 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 471 | end 472 | assert_equal "", sout 473 | assert_equal "-f #{ctx_file}: file not found.\n", serr 474 | ## when syntax error exists 475 | ctx_str = '{"title":"Love&Peace",items:["A","B","C"],}' 476 | sout, serr = with_erubyfile do |fname| 477 | with_tmpfile(ctx_file, ctx_str) do 478 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 479 | end 480 | end 481 | expected = "-f #{ctx_file}: JSON syntax error: (JSON::ParserError) 743: unexpected token\n" 482 | expected = expected.sub(/743/, '814') if RUBY_VERSION >= '2.2' 483 | expected = expected.sub(/743/, '795') if RUBY_VERSION >= '2.0' 484 | assert_equal "", sout 485 | assert_equal expected, serr 486 | end 487 | 488 | it "can specify context data as Ruby code." do 489 | ctx_str = "@title = 'Love&Peace'; @items = ['A','B','C']" 490 | ctx_file = "tmpdata.rb" 491 | sout, serr = with_erubyfile do |fname| 492 | with_tmpfile(ctx_file, ctx_str) do 493 | dummy_stdio { Main.main(["-Hf#{ctx_file}", fname]) } 494 | end 495 | end 496 | assert_equal OUTPUT_HTML, sout 497 | assert_equal "", serr 498 | ## when file not found 499 | sout, serr = with_erubyfile do |fname| 500 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 501 | end 502 | assert_equal "", sout 503 | assert_equal "-f #{ctx_file}: file not found.\n", serr 504 | ## when syntax error exists 505 | ctx_str = "@title = 'Love&Peace' @items = ['A','B','C']" 506 | sout, serr = with_erubyfile do |fname| 507 | with_tmpfile(ctx_file, ctx_str) do 508 | dummy_stdio { Main.main(['-Hf', ctx_file, fname]) } 509 | end 510 | end 511 | expected = "-f #{ctx_file}: Ruby syntax error: (SyntaxError) unexpected tIVAR, expecting $end 512 | @title = 'Love&Peace' @items = ['A','B','C'] 513 | ^\n" 514 | expected = expected.sub(/\$end/, "end-of-input") if RUBY_VERSION =~ /^2\./ 515 | expected = "-f #{ctx_file}: Ruby syntax error: (SyntaxError) expecting $end: (eval):1:28\n" if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 516 | assert_equal "", sout 517 | assert_equal expected, serr 518 | end 519 | 520 | it "reports error when unknown data file suffix." do 521 | ctx_str = '{"title": "Love&Peace", "items": ["A","B","C"]}' 522 | ctx_file = "tmpdata.js" 523 | sout, serr = with_erubyfile do |fname| 524 | with_tmpfile(ctx_file, ctx_str) do 525 | dummy_stdio { Main.main(["-Hf#{ctx_file}", fname]) } 526 | end 527 | end 528 | assert_equal "", sout 529 | assert_equal "-f #{ctx_file}: unknown suffix (expected '.yaml', '.json', or '.rb').\n", serr 530 | end 531 | 532 | end 533 | 534 | 535 | describe '--format={text|html|rails}' do 536 | 537 | it "can enforce text format." do 538 | ctx_str = "{title: Love&Peace, items: [A, B, C]}" 539 | sout, serr = with_erubyfile do |fname| 540 | dummy_stdio { Main.main(['--format=text', '-c', ctx_str, fname]) } 541 | end 542 | assert_equal OUTPUT_TEXT, sout 543 | assert_equal "", serr 544 | end 545 | 546 | it "can enforce html format." do 547 | ctx_str = "{title: Love&Peace, items: [A, B, C]}" 548 | sout, serr = with_erubyfile do |fname| 549 | dummy_stdio { Main.main(['--format=html', '-c', ctx_str, fname]) } 550 | end 551 | assert_equal OUTPUT_HTML, sout 552 | assert_equal "", serr 553 | end 554 | 555 | it "can enforce rails format." do 556 | sout, serr = with_erubyfile do |fname| 557 | dummy_stdio { Main.main(['--format=rails', '-x', fname]) } 558 | end 559 | assert_equal _modify(SOURCE_RAILS), sout 560 | assert_equal "", serr 561 | end 562 | 563 | it "reports error when argument is missng." do 564 | status = nil 565 | sout, serr = with_erubyfile do |fname| 566 | dummy_stdio { status = Main.main(['-x', '--format', fname]) } 567 | end 568 | assert_equal "", sout 569 | assert_equal "#{File.basename($0)}: --format: argument required.\n", serr 570 | assert_equal 1, status 571 | end 572 | 573 | it "reports error when unknown argument." do 574 | status = nil 575 | sout, serr = with_erubyfile do |fname| 576 | dummy_stdio { status = Main.main(['-x', '--format=json', fname]) } 577 | end 578 | assert_equal "", sout 579 | assert_equal "#{File.basename($0)}: --format=json: 'text', 'html' or 'rails' expected\n", serr 580 | assert_equal 1, status 581 | end 582 | 583 | end 584 | 585 | 586 | describe '--encoding=name' do 587 | 588 | it "can specify encoding of file content." do 589 | sout, serr = with_erubyfile do |fname| 590 | dummy_stdio { status = Main.main(['-x', '--encoding=utf-8', fname]) } 591 | end 592 | assert_equal _modify(SOURCE_TEXT), sout 593 | assert_equal "" , serr 594 | end 595 | 596 | end 597 | 598 | 599 | describe '--freeze={true|false}' do 600 | 601 | it "can generate ruby code using String#freeze." do 602 | sout, serr = with_erubyfile do |fname| 603 | dummy_stdio { Main.main(['-x', '--freeze=true', fname]) } 604 | end 605 | expected = _modify(SOURCE_TEXT).gsub(/([^'])';/, "\\1'.freeze;") 606 | assert_equal expected, sout 607 | end 608 | 609 | it "can generate ruby code without String#freeze." do 610 | sout, serr = with_erubyfile do |fname| 611 | dummy_stdio { Main.main(['-x', '--freeze=false', fname]) } 612 | end 613 | expected = SOURCE_TEXT 614 | assert_equal expected, sout 615 | end 616 | 617 | it "reports error when argument is missing." do 618 | status = nil 619 | sout, serr = with_erubyfile do |fname| 620 | dummy_stdio { status = Main.main(['-x', '--freeze', fname]) } 621 | end 622 | assert_equal "", sout 623 | assert_equal "#{File.basename($0)}: --freeze: argument required.\n", serr 624 | assert_equal 1, status 625 | end 626 | 627 | it "reports error when unknown argument." do 628 | status = nil 629 | sout, serr = with_erubyfile do |fname| 630 | dummy_stdio { status = Main.main(['-x', '--freeze=yes', fname]) } 631 | end 632 | assert_equal "", sout 633 | assert_equal "#{File.basename($0)}: --freeze=yes: 'true' or 'false' expected\n", serr 634 | assert_equal 1, status 635 | end 636 | 637 | end 638 | 639 | 640 | end 641 | 642 | 643 | describe Cmdopt::Parser do 644 | 645 | let(:parser) { Main.new.__send__(:build_parser) } 646 | 647 | 648 | describe '#parse()' do 649 | 650 | it "parses short options." do 651 | argv = ["-vh", "-xc", "{x: 1}", "-fdata.txt", "file1", "file2"] 652 | options = parser.parse(argv) 653 | expected = {'version'=>true, 'help'=>true, 'x'=>true, 'c'=>'{x: 1}', 'f'=>'data.txt'} 654 | assert_equal expected, options 655 | assert_equal ["file1", "file2"], argv 656 | end 657 | 658 | it "parses long options" do 659 | argv = ["--help", "--version", "--format=html", "--freeze=true", "file1", "file2"] 660 | options = parser.parse(argv) 661 | expected = {'version'=>true, 'help'=>true, 'format'=>'html', 'freeze'=>'true'} 662 | assert_equal expected, options 663 | assert_equal ["file1", "file2"], argv 664 | end 665 | 666 | it "raises error when required argument of short option is missing." do 667 | argv = ["-f"] 668 | ex = assert_raises Cmdopt::ParseError do 669 | options = parser.parse(argv) 670 | end 671 | assert_equal "#{File.basename($0)}: -f: argument required.", ex.message 672 | end 673 | 674 | it "raises error when required argument of long option is missing." do 675 | argv = ["--format", "file1"] 676 | ex = assert_raises Cmdopt::ParseError do 677 | options = parser.parse(argv) 678 | end 679 | assert_equal "#{File.basename($0)}: --format: argument required.", ex.message 680 | end 681 | 682 | end 683 | 684 | 685 | end 686 | -------------------------------------------------------------------------------- /setup.rb: -------------------------------------------------------------------------------- 1 | # 2 | # setup.rb 3 | # 4 | # Copyright (c) 2000-2005 Minero Aoki 5 | # 6 | # This program is free software. 7 | # You can distribute/modify this program under the terms of 8 | # the GNU LGPL, Lesser General Public License version 2.1. 9 | # 10 | 11 | unless Enumerable.method_defined?(:map) # Ruby 1.4.6 12 | module Enumerable 13 | alias map collect 14 | end 15 | end 16 | 17 | unless File.respond_to?(:read) # Ruby 1.6 18 | def File.read(fname) 19 | open(fname) {|f| 20 | return f.read 21 | } 22 | end 23 | end 24 | 25 | unless Errno.const_defined?(:ENOTEMPTY) # Windows? 26 | module Errno 27 | class ENOTEMPTY 28 | # We do not raise this exception, implementation is not needed. 29 | end 30 | end 31 | end 32 | 33 | def File.binread(fname) 34 | open(fname, 'rb') {|f| 35 | return f.read 36 | } 37 | end 38 | 39 | # for corrupted Windows' stat(2) 40 | def File.dir?(path) 41 | File.directory?((path[-1,1] == '/') ? path : path + '/') 42 | end 43 | 44 | 45 | class ConfigTable 46 | 47 | include Enumerable 48 | 49 | def initialize(rbconfig) 50 | @rbconfig = rbconfig 51 | @items = [] 52 | @table = {} 53 | # options 54 | @install_prefix = nil 55 | @config_opt = nil 56 | @verbose = true 57 | @no_harm = false 58 | end 59 | 60 | attr_accessor :install_prefix 61 | attr_accessor :config_opt 62 | 63 | attr_writer :verbose 64 | 65 | def verbose? 66 | @verbose 67 | end 68 | 69 | attr_writer :no_harm 70 | 71 | def no_harm? 72 | @no_harm 73 | end 74 | 75 | def [](key) 76 | lookup(key).resolve(self) 77 | end 78 | 79 | def []=(key, val) 80 | lookup(key).set val 81 | end 82 | 83 | def names 84 | @items.map {|i| i.name } 85 | end 86 | 87 | def each(&block) 88 | @items.each(&block) 89 | end 90 | 91 | def key?(name) 92 | @table.key?(name) 93 | end 94 | 95 | def lookup(name) 96 | @table[name] or setup_rb_error "no such config item: #{name}" 97 | end 98 | 99 | def add(item) 100 | @items.push item 101 | @table[item.name] = item 102 | end 103 | 104 | def remove(name) 105 | item = lookup(name) 106 | @items.delete_if {|i| i.name == name } 107 | @table.delete_if {|name, i| i.name == name } 108 | item 109 | end 110 | 111 | def load_script(path, inst = nil) 112 | if File.file?(path) 113 | MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path 114 | end 115 | end 116 | 117 | def savefile 118 | '.config' 119 | end 120 | 121 | def load_savefile 122 | begin 123 | File.foreach(savefile()) do |line| 124 | k, v = *line.split(/=/, 2) 125 | self[k] = v.strip 126 | end 127 | rescue Errno::ENOENT 128 | setup_rb_error $!.message + "\n#{File.basename($0)} config first" 129 | end 130 | end 131 | 132 | def save 133 | @items.each {|i| i.value } 134 | File.open(savefile(), 'w') {|f| 135 | @items.each do |i| 136 | f.printf "%s=%s\n", i.name, i.value if i.value? and i.value 137 | end 138 | } 139 | end 140 | 141 | def load_standard_entries 142 | standard_entries(@rbconfig).each do |ent| 143 | add ent 144 | end 145 | end 146 | 147 | def standard_entries(rbconfig) 148 | c = rbconfig 149 | 150 | rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) 151 | 152 | major = c['MAJOR'].to_i 153 | minor = c['MINOR'].to_i 154 | teeny = c['TEENY'].to_i 155 | version = "#{major}.#{minor}" 156 | 157 | # ruby ver. >= 1.4.4? 158 | newpath_p = ((major >= 2) or 159 | ((major == 1) and 160 | ((minor >= 5) or 161 | ((minor == 4) and (teeny >= 4))))) 162 | 163 | if c['rubylibdir'] 164 | # V > 1.6.3 165 | libruby = "#{c['prefix']}/lib/ruby" 166 | librubyver = c['rubylibdir'] 167 | librubyverarch = c['archdir'] 168 | siteruby = c['sitedir'] 169 | siterubyver = c['sitelibdir'] 170 | siterubyverarch = c['sitearchdir'] 171 | elsif newpath_p 172 | # 1.4.4 <= V <= 1.6.3 173 | libruby = "#{c['prefix']}/lib/ruby" 174 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 175 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 176 | siteruby = c['sitedir'] 177 | siterubyver = "$siteruby/#{version}" 178 | siterubyverarch = "$siterubyver/#{c['arch']}" 179 | else 180 | # V < 1.4.4 181 | libruby = "#{c['prefix']}/lib/ruby" 182 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 183 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 184 | siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" 185 | siterubyver = siteruby 186 | siterubyverarch = "$siterubyver/#{c['arch']}" 187 | end 188 | parameterize = lambda {|path| 189 | path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') 190 | } 191 | 192 | if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } 193 | makeprog = arg.sub(/'/, '').split(/=/, 2)[1] 194 | else 195 | makeprog = 'make' 196 | end 197 | 198 | [ 199 | ExecItem.new('installdirs', 'std/site/home', 200 | 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ 201 | {|val, table| 202 | case val 203 | when 'std' 204 | table['rbdir'] = '$librubyver' 205 | table['sodir'] = '$librubyverarch' 206 | when 'site' 207 | table['rbdir'] = '$siterubyver' 208 | table['sodir'] = '$siterubyverarch' 209 | when 'home' 210 | setup_rb_error '$HOME was not set' unless ENV['HOME'] 211 | table['prefix'] = ENV['HOME'] 212 | table['rbdir'] = '$libdir/ruby' 213 | table['sodir'] = '$libdir/ruby' 214 | end 215 | }, 216 | PathItem.new('prefix', 'path', c['prefix'], 217 | 'path prefix of target environment'), 218 | PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 219 | 'the directory for commands'), 220 | PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 221 | 'the directory for libraries'), 222 | PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 223 | 'the directory for shared data'), 224 | PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 225 | 'the directory for man pages'), 226 | PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 227 | 'the directory for system configuration files'), 228 | PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 229 | 'the directory for local state data'), 230 | PathItem.new('libruby', 'path', libruby, 231 | 'the directory for ruby libraries'), 232 | PathItem.new('librubyver', 'path', librubyver, 233 | 'the directory for standard ruby libraries'), 234 | PathItem.new('librubyverarch', 'path', librubyverarch, 235 | 'the directory for standard ruby extensions'), 236 | PathItem.new('siteruby', 'path', siteruby, 237 | 'the directory for version-independent aux ruby libraries'), 238 | PathItem.new('siterubyver', 'path', siterubyver, 239 | 'the directory for aux ruby libraries'), 240 | PathItem.new('siterubyverarch', 'path', siterubyverarch, 241 | 'the directory for aux ruby binaries'), 242 | PathItem.new('rbdir', 'path', '$siterubyver', 243 | 'the directory for ruby scripts'), 244 | PathItem.new('sodir', 'path', '$siterubyverarch', 245 | 'the directory for ruby extentions'), 246 | PathItem.new('rubypath', 'path', rubypath, 247 | 'the path to set to #! line'), 248 | ProgramItem.new('rubyprog', 'name', rubypath, 249 | 'the ruby program using for installation'), 250 | ProgramItem.new('makeprog', 'name', makeprog, 251 | 'the make program to compile ruby extentions'), 252 | SelectItem.new('shebang', 'all/ruby/never', 'ruby', 253 | 'shebang line (#!) editing mode'), 254 | BoolItem.new('without-ext', 'yes/no', 'no', 255 | 'does not compile/install ruby extentions') 256 | ] 257 | end 258 | private :standard_entries 259 | 260 | def load_multipackage_entries 261 | multipackage_entries().each do |ent| 262 | add ent 263 | end 264 | end 265 | 266 | def multipackage_entries 267 | [ 268 | PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 269 | 'package names that you want to install'), 270 | PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 271 | 'package names that you do not want to install') 272 | ] 273 | end 274 | private :multipackage_entries 275 | 276 | ALIASES = { 277 | 'std-ruby' => 'librubyver', 278 | 'stdruby' => 'librubyver', 279 | 'rubylibdir' => 'librubyver', 280 | 'archdir' => 'librubyverarch', 281 | 'site-ruby-common' => 'siteruby', # For backward compatibility 282 | 'site-ruby' => 'siterubyver', # For backward compatibility 283 | 'bin-dir' => 'bindir', 284 | 'bin-dir' => 'bindir', 285 | 'rb-dir' => 'rbdir', 286 | 'so-dir' => 'sodir', 287 | 'data-dir' => 'datadir', 288 | 'ruby-path' => 'rubypath', 289 | 'ruby-prog' => 'rubyprog', 290 | 'ruby' => 'rubyprog', 291 | 'make-prog' => 'makeprog', 292 | 'make' => 'makeprog' 293 | } 294 | 295 | def fixup 296 | ALIASES.each do |ali, name| 297 | @table[ali] = @table[name] 298 | end 299 | @items.freeze 300 | @table.freeze 301 | @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ 302 | end 303 | 304 | def parse_opt(opt) 305 | m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" 306 | m.to_a[1,2] 307 | end 308 | 309 | def dllext 310 | @rbconfig['DLEXT'] 311 | end 312 | 313 | def value_config?(name) 314 | lookup(name).value? 315 | end 316 | 317 | class Item 318 | def initialize(name, template, default, desc) 319 | @name = name.freeze 320 | @template = template 321 | @value = default 322 | @default = default 323 | @description = desc 324 | end 325 | 326 | attr_reader :name 327 | attr_reader :description 328 | 329 | attr_accessor :default 330 | alias help_default default 331 | 332 | def help_opt 333 | "--#{@name}=#{@template}" 334 | end 335 | 336 | def value? 337 | true 338 | end 339 | 340 | def value 341 | @value 342 | end 343 | 344 | def resolve(table) 345 | @value.gsub(%r<\$([^/]+)>) { table[$1] } 346 | end 347 | 348 | def set(val) 349 | @value = check(val) 350 | end 351 | 352 | private 353 | 354 | def check(val) 355 | setup_rb_error "config: --#{name} requires argument" unless val 356 | val 357 | end 358 | end 359 | 360 | class BoolItem < Item 361 | def config_type 362 | 'bool' 363 | end 364 | 365 | def help_opt 366 | "--#{@name}" 367 | end 368 | 369 | private 370 | 371 | def check(val) 372 | return 'yes' unless val 373 | case val 374 | when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' 375 | when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' 376 | else 377 | setup_rb_error "config: --#{@name} accepts only yes/no for argument" 378 | end 379 | end 380 | end 381 | 382 | class PathItem < Item 383 | def config_type 384 | 'path' 385 | end 386 | 387 | private 388 | 389 | def check(path) 390 | setup_rb_error "config: --#{@name} requires argument" unless path 391 | path[0,1] == '$' ? path : File.expand_path(path) 392 | end 393 | end 394 | 395 | class ProgramItem < Item 396 | def config_type 397 | 'program' 398 | end 399 | end 400 | 401 | class SelectItem < Item 402 | def initialize(name, selection, default, desc) 403 | super 404 | @ok = selection.split('/') 405 | end 406 | 407 | def config_type 408 | 'select' 409 | end 410 | 411 | private 412 | 413 | def check(val) 414 | unless @ok.include?(val.strip) 415 | setup_rb_error "config: use --#{@name}=#{@template} (#{val})" 416 | end 417 | val.strip 418 | end 419 | end 420 | 421 | class ExecItem < Item 422 | def initialize(name, selection, desc, &block) 423 | super name, selection, nil, desc 424 | @ok = selection.split('/') 425 | @action = block 426 | end 427 | 428 | def config_type 429 | 'exec' 430 | end 431 | 432 | def value? 433 | false 434 | end 435 | 436 | def resolve(table) 437 | setup_rb_error "$#{name()} wrongly used as option value" 438 | end 439 | 440 | undef set 441 | 442 | def evaluate(val, table) 443 | v = val.strip.downcase 444 | unless @ok.include?(v) 445 | setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" 446 | end 447 | @action.call v, table 448 | end 449 | end 450 | 451 | class PackageSelectionItem < Item 452 | def initialize(name, template, default, help_default, desc) 453 | super name, template, default, desc 454 | @help_default = help_default 455 | end 456 | 457 | attr_reader :help_default 458 | 459 | def config_type 460 | 'package' 461 | end 462 | 463 | private 464 | 465 | def check(val) 466 | unless File.dir?("packages/#{val}") 467 | setup_rb_error "config: no such package: #{val}" 468 | end 469 | val 470 | end 471 | end 472 | 473 | class MetaConfigEnvironment 474 | def initialize(config, installer) 475 | @config = config 476 | @installer = installer 477 | end 478 | 479 | def config_names 480 | @config.names 481 | end 482 | 483 | def config?(name) 484 | @config.key?(name) 485 | end 486 | 487 | def bool_config?(name) 488 | @config.lookup(name).config_type == 'bool' 489 | end 490 | 491 | def path_config?(name) 492 | @config.lookup(name).config_type == 'path' 493 | end 494 | 495 | def value_config?(name) 496 | @config.lookup(name).config_type != 'exec' 497 | end 498 | 499 | def add_config(item) 500 | @config.add item 501 | end 502 | 503 | def add_bool_config(name, default, desc) 504 | @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) 505 | end 506 | 507 | def add_path_config(name, default, desc) 508 | @config.add PathItem.new(name, 'path', default, desc) 509 | end 510 | 511 | def set_config_default(name, default) 512 | @config.lookup(name).default = default 513 | end 514 | 515 | def remove_config(name) 516 | @config.remove(name) 517 | end 518 | 519 | # For only multipackage 520 | def packages 521 | raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer 522 | @installer.packages 523 | end 524 | 525 | # For only multipackage 526 | def declare_packages(list) 527 | raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer 528 | @installer.packages = list 529 | end 530 | end 531 | 532 | end # class ConfigTable 533 | 534 | 535 | # This module requires: #verbose?, #no_harm? 536 | module FileOperations 537 | 538 | def mkdir_p(dirname, prefix = nil) 539 | dirname = prefix + File.expand_path(dirname) if prefix 540 | $stderr.puts "mkdir -p #{dirname}" if verbose? 541 | return if no_harm? 542 | 543 | # Does not check '/', it's too abnormal. 544 | dirs = File.expand_path(dirname).split(%r<(?=/)>) 545 | if /\A[a-z]:\z/i =~ dirs[0] 546 | disk = dirs.shift 547 | dirs[0] = disk + dirs[0] 548 | end 549 | dirs.each_index do |idx| 550 | path = dirs[0..idx].join('') 551 | Dir.mkdir path unless File.dir?(path) 552 | end 553 | end 554 | 555 | def rm_f(path) 556 | $stderr.puts "rm -f #{path}" if verbose? 557 | return if no_harm? 558 | force_remove_file path 559 | end 560 | 561 | def rm_rf(path) 562 | $stderr.puts "rm -rf #{path}" if verbose? 563 | return if no_harm? 564 | remove_tree path 565 | end 566 | 567 | def remove_tree(path) 568 | if File.symlink?(path) 569 | remove_file path 570 | elsif File.dir?(path) 571 | remove_tree0 path 572 | else 573 | force_remove_file path 574 | end 575 | end 576 | 577 | def remove_tree0(path) 578 | Dir.foreach(path) do |ent| 579 | next if ent == '.' 580 | next if ent == '..' 581 | entpath = "#{path}/#{ent}" 582 | if File.symlink?(entpath) 583 | remove_file entpath 584 | elsif File.dir?(entpath) 585 | remove_tree0 entpath 586 | else 587 | force_remove_file entpath 588 | end 589 | end 590 | begin 591 | Dir.rmdir path 592 | rescue Errno::ENOTEMPTY 593 | # directory may not be empty 594 | end 595 | end 596 | 597 | def move_file(src, dest) 598 | force_remove_file dest 599 | begin 600 | File.rename src, dest 601 | rescue 602 | File.open(dest, 'wb') {|f| 603 | f.write File.binread(src) 604 | } 605 | File.chmod File.stat(src).mode, dest 606 | File.unlink src 607 | end 608 | end 609 | 610 | def force_remove_file(path) 611 | begin 612 | remove_file path 613 | rescue 614 | end 615 | end 616 | 617 | def remove_file(path) 618 | File.chmod 0777, path 619 | File.unlink path 620 | end 621 | 622 | def install(from, dest, mode, prefix = nil) 623 | $stderr.puts "install #{from} #{dest}" if verbose? 624 | return if no_harm? 625 | 626 | realdest = prefix ? prefix + File.expand_path(dest) : dest 627 | realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) 628 | str = File.binread(from) 629 | if diff?(str, realdest) 630 | verbose_off { 631 | rm_f realdest if File.exist?(realdest) 632 | } 633 | File.open(realdest, 'wb') {|f| 634 | f.write str 635 | } 636 | File.chmod mode, realdest 637 | 638 | File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| 639 | if prefix 640 | f.puts realdest.sub(prefix, '') 641 | else 642 | f.puts realdest 643 | end 644 | } 645 | end 646 | end 647 | 648 | def diff?(new_content, path) 649 | return true unless File.exist?(path) 650 | new_content != File.binread(path) 651 | end 652 | 653 | def command(*args) 654 | $stderr.puts args.join(' ') if verbose? 655 | system(*args) or raise RuntimeError, 656 | "system(#{args.map{|a| a.inspect }.join(' ')}) failed" 657 | end 658 | 659 | def ruby(*args) 660 | command config('rubyprog'), *args 661 | end 662 | 663 | def make(task = nil) 664 | command(*[config('makeprog'), task].compact) 665 | end 666 | 667 | def extdir?(dir) 668 | File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") 669 | end 670 | 671 | def files_of(dir) 672 | Dir.open(dir) {|d| 673 | return d.select {|ent| File.file?("#{dir}/#{ent}") } 674 | } 675 | end 676 | 677 | DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) 678 | 679 | def directories_of(dir) 680 | Dir.open(dir) {|d| 681 | return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT 682 | } 683 | end 684 | 685 | end 686 | 687 | 688 | # This module requires: #srcdir_root, #objdir_root, #relpath 689 | module HookScriptAPI 690 | 691 | def get_config(key) 692 | @config[key] 693 | end 694 | 695 | alias config get_config 696 | 697 | # obsolete: use metaconfig to change configuration 698 | def set_config(key, val) 699 | @config[key] = val 700 | end 701 | 702 | # 703 | # srcdir/objdir (works only in the package directory) 704 | # 705 | 706 | def curr_srcdir 707 | "#{srcdir_root()}/#{relpath()}" 708 | end 709 | 710 | def curr_objdir 711 | "#{objdir_root()}/#{relpath()}" 712 | end 713 | 714 | def srcfile(path) 715 | "#{curr_srcdir()}/#{path}" 716 | end 717 | 718 | def srcexist?(path) 719 | File.exist?(srcfile(path)) 720 | end 721 | 722 | def srcdirectory?(path) 723 | File.dir?(srcfile(path)) 724 | end 725 | 726 | def srcfile?(path) 727 | File.file?(srcfile(path)) 728 | end 729 | 730 | def srcentries(path = '.') 731 | Dir.open("#{curr_srcdir()}/#{path}") {|d| 732 | return d.to_a - %w(. ..) 733 | } 734 | end 735 | 736 | def srcfiles(path = '.') 737 | srcentries(path).select {|fname| 738 | File.file?(File.join(curr_srcdir(), path, fname)) 739 | } 740 | end 741 | 742 | def srcdirectories(path = '.') 743 | srcentries(path).select {|fname| 744 | File.dir?(File.join(curr_srcdir(), path, fname)) 745 | } 746 | end 747 | 748 | end 749 | 750 | 751 | class ToplevelInstaller 752 | 753 | Version = '3.4.1' 754 | Copyright = 'Copyright (c) 2000-2005 Minero Aoki' 755 | 756 | TASKS = [ 757 | [ 'all', 'do config, setup, then install' ], 758 | [ 'config', 'saves your configurations' ], 759 | [ 'show', 'shows current configuration' ], 760 | [ 'setup', 'compiles ruby extentions and others' ], 761 | [ 'install', 'installs files' ], 762 | [ 'test', 'run all tests in test/' ], 763 | [ 'clean', "does `make clean' for each extention" ], 764 | [ 'distclean',"does `make distclean' for each extention" ] 765 | ] 766 | 767 | def ToplevelInstaller.invoke 768 | config = ConfigTable.new(load_rbconfig()) 769 | config.load_standard_entries 770 | config.load_multipackage_entries if multipackage? 771 | config.fixup 772 | klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) 773 | klass.new(File.dirname($0), config).invoke 774 | end 775 | 776 | def ToplevelInstaller.multipackage? 777 | File.dir?(File.dirname($0) + '/packages') 778 | end 779 | 780 | def ToplevelInstaller.load_rbconfig 781 | if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } 782 | ARGV.delete(arg) 783 | load File.expand_path(arg.split(/=/, 2)[1]) 784 | $".push 'rbconfig.rb' 785 | else 786 | require 'rbconfig' 787 | end 788 | ::Config::CONFIG 789 | end 790 | 791 | def initialize(ardir_root, config) 792 | @ardir = File.expand_path(ardir_root) 793 | @config = config 794 | # cache 795 | @valid_task_re = nil 796 | end 797 | 798 | def config(key) 799 | @config[key] 800 | end 801 | 802 | def inspect 803 | "#<#{self.class} #{__id__()}>" 804 | end 805 | 806 | def invoke 807 | run_metaconfigs 808 | case task = parsearg_global() 809 | when nil, 'all' 810 | parsearg_config 811 | init_installers 812 | exec_config 813 | exec_setup 814 | exec_install 815 | else 816 | case task 817 | when 'config', 'test' 818 | ; 819 | when 'clean', 'distclean' 820 | @config.load_savefile if File.exist?(@config.savefile) 821 | else 822 | @config.load_savefile 823 | end 824 | __send__ "parsearg_#{task}" 825 | init_installers 826 | __send__ "exec_#{task}" 827 | end 828 | end 829 | 830 | def run_metaconfigs 831 | @config.load_script "#{@ardir}/metaconfig" 832 | end 833 | 834 | def init_installers 835 | @installer = Installer.new(@config, @ardir, File.expand_path('.')) 836 | end 837 | 838 | # 839 | # Hook Script API bases 840 | # 841 | 842 | def srcdir_root 843 | @ardir 844 | end 845 | 846 | def objdir_root 847 | '.' 848 | end 849 | 850 | def relpath 851 | '.' 852 | end 853 | 854 | # 855 | # Option Parsing 856 | # 857 | 858 | def parsearg_global 859 | while arg = ARGV.shift 860 | case arg 861 | when /\A\w+\z/ 862 | setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) 863 | return arg 864 | when '-q', '--quiet' 865 | @config.verbose = false 866 | when '--verbose' 867 | @config.verbose = true 868 | when '--help' 869 | print_usage $stdout 870 | exit 0 871 | when '--version' 872 | puts "#{File.basename($0)} version #{Version}" 873 | exit 0 874 | when '--copyright' 875 | puts Copyright 876 | exit 0 877 | else 878 | setup_rb_error "unknown global option '#{arg}'" 879 | end 880 | end 881 | nil 882 | end 883 | 884 | def valid_task?(t) 885 | valid_task_re() =~ t 886 | end 887 | 888 | def valid_task_re 889 | @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ 890 | end 891 | 892 | def parsearg_no_options 893 | unless ARGV.empty? 894 | task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) 895 | setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" 896 | end 897 | end 898 | 899 | alias parsearg_show parsearg_no_options 900 | alias parsearg_setup parsearg_no_options 901 | alias parsearg_test parsearg_no_options 902 | alias parsearg_clean parsearg_no_options 903 | alias parsearg_distclean parsearg_no_options 904 | 905 | def parsearg_config 906 | evalopt = [] 907 | set = [] 908 | @config.config_opt = [] 909 | while i = ARGV.shift 910 | if /\A--?\z/ =~ i 911 | @config.config_opt = ARGV.dup 912 | break 913 | end 914 | name, value = *@config.parse_opt(i) 915 | if @config.value_config?(name) 916 | @config[name] = value 917 | else 918 | evalopt.push [name, value] 919 | end 920 | set.push name 921 | end 922 | evalopt.each do |name, value| 923 | @config.lookup(name).evaluate value, @config 924 | end 925 | # Check if configuration is valid 926 | set.each do |n| 927 | @config[n] if @config.value_config?(n) 928 | end 929 | end 930 | 931 | def parsearg_install 932 | @config.no_harm = false 933 | @config.install_prefix = '' 934 | while a = ARGV.shift 935 | case a 936 | when '--no-harm' 937 | @config.no_harm = true 938 | when /\A--prefix=/ 939 | path = a.split(/=/, 2)[1] 940 | path = File.expand_path(path) unless path[0,1] == '/' 941 | @config.install_prefix = path 942 | else 943 | setup_rb_error "install: unknown option #{a}" 944 | end 945 | end 946 | end 947 | 948 | def print_usage(out) 949 | out.puts 'Typical Installation Procedure:' 950 | out.puts " $ ruby #{File.basename $0} config" 951 | out.puts " $ ruby #{File.basename $0} setup" 952 | out.puts " # ruby #{File.basename $0} install (may require root privilege)" 953 | out.puts 954 | out.puts 'Detailed Usage:' 955 | out.puts " ruby #{File.basename $0} " 956 | out.puts " ruby #{File.basename $0} [] []" 957 | 958 | fmt = " %-24s %s\n" 959 | out.puts 960 | out.puts 'Global options:' 961 | out.printf fmt, '-q,--quiet', 'suppress message outputs' 962 | out.printf fmt, ' --verbose', 'output messages verbosely' 963 | out.printf fmt, ' --help', 'print this message' 964 | out.printf fmt, ' --version', 'print version and quit' 965 | out.printf fmt, ' --copyright', 'print copyright and quit' 966 | out.puts 967 | out.puts 'Tasks:' 968 | TASKS.each do |name, desc| 969 | out.printf fmt, name, desc 970 | end 971 | 972 | fmt = " %-24s %s [%s]\n" 973 | out.puts 974 | out.puts 'Options for CONFIG or ALL:' 975 | @config.each do |item| 976 | out.printf fmt, item.help_opt, item.description, item.help_default 977 | end 978 | out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" 979 | out.puts 980 | out.puts 'Options for INSTALL:' 981 | out.printf fmt, '--no-harm', 'only display what to do if given', 'off' 982 | out.printf fmt, '--prefix=path', 'install path prefix', '' 983 | out.puts 984 | end 985 | 986 | # 987 | # Task Handlers 988 | # 989 | 990 | def exec_config 991 | @installer.exec_config 992 | @config.save # must be final 993 | end 994 | 995 | def exec_setup 996 | @installer.exec_setup 997 | end 998 | 999 | def exec_install 1000 | @installer.exec_install 1001 | end 1002 | 1003 | def exec_test 1004 | @installer.exec_test 1005 | end 1006 | 1007 | def exec_show 1008 | @config.each do |i| 1009 | printf "%-20s %s\n", i.name, i.value if i.value? 1010 | end 1011 | end 1012 | 1013 | def exec_clean 1014 | @installer.exec_clean 1015 | end 1016 | 1017 | def exec_distclean 1018 | @installer.exec_distclean 1019 | end 1020 | 1021 | end # class ToplevelInstaller 1022 | 1023 | 1024 | class ToplevelInstallerMulti < ToplevelInstaller 1025 | 1026 | include FileOperations 1027 | 1028 | def initialize(ardir_root, config) 1029 | super 1030 | @packages = directories_of("#{@ardir}/packages") 1031 | raise 'no package exists' if @packages.empty? 1032 | @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) 1033 | end 1034 | 1035 | def run_metaconfigs 1036 | @config.load_script "#{@ardir}/metaconfig", self 1037 | @packages.each do |name| 1038 | @config.load_script "#{@ardir}/packages/#{name}/metaconfig" 1039 | end 1040 | end 1041 | 1042 | attr_reader :packages 1043 | 1044 | def packages=(list) 1045 | raise 'package list is empty' if list.empty? 1046 | list.each do |name| 1047 | raise "directory packages/#{name} does not exist"\ 1048 | unless File.dir?("#{@ardir}/packages/#{name}") 1049 | end 1050 | @packages = list 1051 | end 1052 | 1053 | def init_installers 1054 | @installers = {} 1055 | @packages.each do |pack| 1056 | @installers[pack] = Installer.new(@config, 1057 | "#{@ardir}/packages/#{pack}", 1058 | "packages/#{pack}") 1059 | end 1060 | with = extract_selection(config('with')) 1061 | without = extract_selection(config('without')) 1062 | @selected = @installers.keys.select {|name| 1063 | (with.empty? or with.include?(name)) \ 1064 | and not without.include?(name) 1065 | } 1066 | end 1067 | 1068 | def extract_selection(list) 1069 | a = list.split(/,/) 1070 | a.each do |name| 1071 | setup_rb_error "no such package: #{name}" unless @installers.key?(name) 1072 | end 1073 | a 1074 | end 1075 | 1076 | def print_usage(f) 1077 | super 1078 | f.puts 'Inluded packages:' 1079 | f.puts ' ' + @packages.sort.join(' ') 1080 | f.puts 1081 | end 1082 | 1083 | # 1084 | # Task Handlers 1085 | # 1086 | 1087 | def exec_config 1088 | run_hook 'pre-config' 1089 | each_selected_installers {|inst| inst.exec_config } 1090 | run_hook 'post-config' 1091 | @config.save # must be final 1092 | end 1093 | 1094 | def exec_setup 1095 | run_hook 'pre-setup' 1096 | each_selected_installers {|inst| inst.exec_setup } 1097 | run_hook 'post-setup' 1098 | end 1099 | 1100 | def exec_install 1101 | run_hook 'pre-install' 1102 | each_selected_installers {|inst| inst.exec_install } 1103 | run_hook 'post-install' 1104 | end 1105 | 1106 | def exec_test 1107 | run_hook 'pre-test' 1108 | each_selected_installers {|inst| inst.exec_test } 1109 | run_hook 'post-test' 1110 | end 1111 | 1112 | def exec_clean 1113 | rm_f @config.savefile 1114 | run_hook 'pre-clean' 1115 | each_selected_installers {|inst| inst.exec_clean } 1116 | run_hook 'post-clean' 1117 | end 1118 | 1119 | def exec_distclean 1120 | rm_f @config.savefile 1121 | run_hook 'pre-distclean' 1122 | each_selected_installers {|inst| inst.exec_distclean } 1123 | run_hook 'post-distclean' 1124 | end 1125 | 1126 | # 1127 | # lib 1128 | # 1129 | 1130 | def each_selected_installers 1131 | Dir.mkdir 'packages' unless File.dir?('packages') 1132 | @selected.each do |pack| 1133 | $stderr.puts "Processing the package `#{pack}' ..." if verbose? 1134 | Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") 1135 | Dir.chdir "packages/#{pack}" 1136 | yield @installers[pack] 1137 | Dir.chdir '../..' 1138 | end 1139 | end 1140 | 1141 | def run_hook(id) 1142 | @root_installer.run_hook id 1143 | end 1144 | 1145 | # module FileOperations requires this 1146 | def verbose? 1147 | @config.verbose? 1148 | end 1149 | 1150 | # module FileOperations requires this 1151 | def no_harm? 1152 | @config.no_harm? 1153 | end 1154 | 1155 | end # class ToplevelInstallerMulti 1156 | 1157 | 1158 | class Installer 1159 | 1160 | FILETYPES = %w( bin lib ext data conf man ) 1161 | 1162 | include FileOperations 1163 | include HookScriptAPI 1164 | 1165 | def initialize(config, srcroot, objroot) 1166 | @config = config 1167 | @srcdir = File.expand_path(srcroot) 1168 | @objdir = File.expand_path(objroot) 1169 | @currdir = '.' 1170 | end 1171 | 1172 | def inspect 1173 | "#<#{self.class} #{File.basename(@srcdir)}>" 1174 | end 1175 | 1176 | def noop(rel) 1177 | end 1178 | 1179 | # 1180 | # Hook Script API base methods 1181 | # 1182 | 1183 | def srcdir_root 1184 | @srcdir 1185 | end 1186 | 1187 | def objdir_root 1188 | @objdir 1189 | end 1190 | 1191 | def relpath 1192 | @currdir 1193 | end 1194 | 1195 | # 1196 | # Config Access 1197 | # 1198 | 1199 | # module FileOperations requires this 1200 | def verbose? 1201 | @config.verbose? 1202 | end 1203 | 1204 | # module FileOperations requires this 1205 | def no_harm? 1206 | @config.no_harm? 1207 | end 1208 | 1209 | def verbose_off 1210 | begin 1211 | save, @config.verbose = @config.verbose?, false 1212 | yield 1213 | ensure 1214 | @config.verbose = save 1215 | end 1216 | end 1217 | 1218 | # 1219 | # TASK config 1220 | # 1221 | 1222 | def exec_config 1223 | exec_task_traverse 'config' 1224 | end 1225 | 1226 | alias config_dir_bin noop 1227 | alias config_dir_lib noop 1228 | 1229 | def config_dir_ext(rel) 1230 | extconf if extdir?(curr_srcdir()) 1231 | end 1232 | 1233 | alias config_dir_data noop 1234 | alias config_dir_conf noop 1235 | alias config_dir_man noop 1236 | 1237 | def extconf 1238 | ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt 1239 | end 1240 | 1241 | # 1242 | # TASK setup 1243 | # 1244 | 1245 | def exec_setup 1246 | exec_task_traverse 'setup' 1247 | end 1248 | 1249 | def setup_dir_bin(rel) 1250 | files_of(curr_srcdir()).each do |fname| 1251 | update_shebang_line "#{curr_srcdir()}/#{fname}" 1252 | end 1253 | end 1254 | 1255 | alias setup_dir_lib noop 1256 | 1257 | def setup_dir_ext(rel) 1258 | make if extdir?(curr_srcdir()) 1259 | end 1260 | 1261 | alias setup_dir_data noop 1262 | alias setup_dir_conf noop 1263 | alias setup_dir_man noop 1264 | 1265 | def update_shebang_line(path) 1266 | return if no_harm? 1267 | return if config('shebang') == 'never' 1268 | old = Shebang.load(path) 1269 | if old 1270 | $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 1271 | new = new_shebang(old) 1272 | return if new.to_s == old.to_s 1273 | else 1274 | return unless config('shebang') == 'all' 1275 | new = Shebang.new(config('rubypath')) 1276 | end 1277 | $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? 1278 | open_atomic_writer(path) {|output| 1279 | File.open(path, 'rb') {|f| 1280 | f.gets if old # discard 1281 | output.puts new.to_s 1282 | output.print f.read 1283 | } 1284 | } 1285 | end 1286 | 1287 | def new_shebang(old) 1288 | if /\Aruby/ =~ File.basename(old.cmd) 1289 | Shebang.new(config('rubypath'), old.args) 1290 | elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' 1291 | Shebang.new(config('rubypath'), old.args[1..-1]) 1292 | else 1293 | return old unless config('shebang') == 'all' 1294 | Shebang.new(config('rubypath')) 1295 | end 1296 | end 1297 | 1298 | def open_atomic_writer(path, &block) 1299 | tmpfile = File.basename(path) + '.tmp' 1300 | begin 1301 | File.open(tmpfile, 'wb', &block) 1302 | File.rename tmpfile, File.basename(path) 1303 | ensure 1304 | File.unlink tmpfile if File.exist?(tmpfile) 1305 | end 1306 | end 1307 | 1308 | class Shebang 1309 | def Shebang.load(path) 1310 | line = nil 1311 | File.open(path) {|f| 1312 | line = f.gets 1313 | } 1314 | return nil unless /\A#!/ =~ line 1315 | parse(line) 1316 | end 1317 | 1318 | def Shebang.parse(line) 1319 | cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') 1320 | new(cmd, args) 1321 | end 1322 | 1323 | def initialize(cmd, args = []) 1324 | @cmd = cmd 1325 | @args = args 1326 | end 1327 | 1328 | attr_reader :cmd 1329 | attr_reader :args 1330 | 1331 | def to_s 1332 | "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") 1333 | end 1334 | end 1335 | 1336 | # 1337 | # TASK install 1338 | # 1339 | 1340 | def exec_install 1341 | rm_f 'InstalledFiles' 1342 | exec_task_traverse 'install' 1343 | end 1344 | 1345 | def install_dir_bin(rel) 1346 | install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 1347 | end 1348 | 1349 | def install_dir_lib(rel) 1350 | install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 1351 | end 1352 | 1353 | def install_dir_ext(rel) 1354 | return unless extdir?(curr_srcdir()) 1355 | install_files rubyextentions('.'), 1356 | "#{config('sodir')}/#{File.dirname(rel)}", 1357 | 0555 1358 | end 1359 | 1360 | def install_dir_data(rel) 1361 | install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 1362 | end 1363 | 1364 | def install_dir_conf(rel) 1365 | # FIXME: should not remove current config files 1366 | # (rename previous file to .old/.org) 1367 | install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 1368 | end 1369 | 1370 | def install_dir_man(rel) 1371 | install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 1372 | end 1373 | 1374 | def install_files(list, dest, mode) 1375 | mkdir_p dest, @config.install_prefix 1376 | list.each do |fname| 1377 | install fname, dest, mode, @config.install_prefix 1378 | end 1379 | end 1380 | 1381 | def libfiles 1382 | glob_reject(%w(*.y *.output), targetfiles()) 1383 | end 1384 | 1385 | def rubyextentions(dir) 1386 | ents = glob_select("*.#{@config.dllext}", targetfiles()) 1387 | if ents.empty? 1388 | setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" 1389 | end 1390 | ents 1391 | end 1392 | 1393 | def targetfiles 1394 | mapdir(existfiles() - hookfiles()) 1395 | end 1396 | 1397 | def mapdir(ents) 1398 | ents.map {|ent| 1399 | if File.exist?(ent) 1400 | then ent # objdir 1401 | else "#{curr_srcdir()}/#{ent}" # srcdir 1402 | end 1403 | } 1404 | end 1405 | 1406 | # picked up many entries from cvs-1.11.1/src/ignore.c 1407 | JUNK_FILES = %w( 1408 | core RCSLOG tags TAGS .make.state 1409 | .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb 1410 | *~ *.old *.bak *.BAK *.orig *.rej _$* *$ 1411 | 1412 | *.org *.in .* 1413 | ) 1414 | 1415 | def existfiles 1416 | glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) 1417 | end 1418 | 1419 | def hookfiles 1420 | %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| 1421 | %w( config setup install clean ).map {|t| sprintf(fmt, t) } 1422 | }.flatten 1423 | end 1424 | 1425 | def glob_select(pat, ents) 1426 | re = globs2re([pat]) 1427 | ents.select {|ent| re =~ ent } 1428 | end 1429 | 1430 | def glob_reject(pats, ents) 1431 | re = globs2re(pats) 1432 | ents.reject {|ent| re =~ ent } 1433 | end 1434 | 1435 | GLOB2REGEX = { 1436 | '.' => '\.', 1437 | '$' => '\$', 1438 | '#' => '\#', 1439 | '*' => '.*' 1440 | } 1441 | 1442 | def globs2re(pats) 1443 | /\A(?:#{ 1444 | pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') 1445 | })\z/ 1446 | end 1447 | 1448 | # 1449 | # TASK test 1450 | # 1451 | 1452 | TESTDIR = 'test' 1453 | 1454 | def exec_test 1455 | unless File.directory?('test') 1456 | $stderr.puts 'no test in this package' if verbose? 1457 | return 1458 | end 1459 | $stderr.puts 'Running tests...' if verbose? 1460 | begin 1461 | require 'test/unit' 1462 | rescue LoadError 1463 | setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' 1464 | end 1465 | runner = Test::Unit::AutoRunner.new(true) 1466 | runner.to_run << TESTDIR 1467 | runner.run 1468 | end 1469 | 1470 | # 1471 | # TASK clean 1472 | # 1473 | 1474 | def exec_clean 1475 | exec_task_traverse 'clean' 1476 | rm_f @config.savefile 1477 | rm_f 'InstalledFiles' 1478 | end 1479 | 1480 | alias clean_dir_bin noop 1481 | alias clean_dir_lib noop 1482 | alias clean_dir_data noop 1483 | alias clean_dir_conf noop 1484 | alias clean_dir_man noop 1485 | 1486 | def clean_dir_ext(rel) 1487 | return unless extdir?(curr_srcdir()) 1488 | make 'clean' if File.file?('Makefile') 1489 | end 1490 | 1491 | # 1492 | # TASK distclean 1493 | # 1494 | 1495 | def exec_distclean 1496 | exec_task_traverse 'distclean' 1497 | rm_f @config.savefile 1498 | rm_f 'InstalledFiles' 1499 | end 1500 | 1501 | alias distclean_dir_bin noop 1502 | alias distclean_dir_lib noop 1503 | 1504 | def distclean_dir_ext(rel) 1505 | return unless extdir?(curr_srcdir()) 1506 | make 'distclean' if File.file?('Makefile') 1507 | end 1508 | 1509 | alias distclean_dir_data noop 1510 | alias distclean_dir_conf noop 1511 | alias distclean_dir_man noop 1512 | 1513 | # 1514 | # Traversing 1515 | # 1516 | 1517 | def exec_task_traverse(task) 1518 | run_hook "pre-#{task}" 1519 | FILETYPES.each do |type| 1520 | if type == 'ext' and config('without-ext') == 'yes' 1521 | $stderr.puts 'skipping ext/* by user option' if verbose? 1522 | next 1523 | end 1524 | traverse task, type, "#{task}_dir_#{type}" 1525 | end 1526 | run_hook "post-#{task}" 1527 | end 1528 | 1529 | def traverse(task, rel, mid) 1530 | dive_into(rel) { 1531 | run_hook "pre-#{task}" 1532 | __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') 1533 | directories_of(curr_srcdir()).each do |d| 1534 | traverse task, "#{rel}/#{d}", mid 1535 | end 1536 | run_hook "post-#{task}" 1537 | } 1538 | end 1539 | 1540 | def dive_into(rel) 1541 | return unless File.dir?("#{@srcdir}/#{rel}") 1542 | 1543 | dir = File.basename(rel) 1544 | Dir.mkdir dir unless File.dir?(dir) 1545 | prevdir = Dir.pwd 1546 | Dir.chdir dir 1547 | $stderr.puts '---> ' + rel if verbose? 1548 | @currdir = rel 1549 | yield 1550 | Dir.chdir prevdir 1551 | $stderr.puts '<--- ' + rel if verbose? 1552 | @currdir = File.dirname(rel) 1553 | end 1554 | 1555 | def run_hook(id) 1556 | path = [ "#{curr_srcdir()}/#{id}", 1557 | "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } 1558 | return unless path 1559 | begin 1560 | instance_eval File.read(path), path, 1 1561 | rescue 1562 | raise if $DEBUG 1563 | setup_rb_error "hook #{path} failed:\n" + $!.message 1564 | end 1565 | end 1566 | 1567 | end # class Installer 1568 | 1569 | 1570 | class SetupError < StandardError; end 1571 | 1572 | def setup_rb_error(msg) 1573 | raise SetupError, msg 1574 | end 1575 | 1576 | if $0 == __FILE__ 1577 | begin 1578 | ToplevelInstaller.invoke 1579 | rescue SetupError 1580 | raise if $DEBUG 1581 | $stderr.puts $!.message 1582 | $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." 1583 | exit 1 1584 | end 1585 | end 1586 | --------------------------------------------------------------------------------