├── .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(/"/, '"').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 |
58 | <% for x in @items %>
59 | - <%= x %>
60 | <% end %>
61 |
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 |
98 | - 10
99 | - 20
100 | - 30
101 |
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 |
127 | - 10
128 | - 20
129 | - 30
130 |
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 |
194 | END
195 | expected = <<'END'
196 | _buf = ''; _buf << '
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]+, '<')
321 | return super(stripped, *args)
322 | end
323 |
324 | end
325 |
326 | template = MyTemplate.new <<-'END'
327 |
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 |
--------------------------------------------------------------------------------