├── .rdoc_options ├── .document ├── test ├── erb │ ├── hello.erb │ ├── test_erb_command.rb │ ├── test_erb_m17n.rb │ └── test_erb.rb └── lib │ └── helper.rb ├── _doc ├── cgi.rb └── erb_executable.md ├── lib ├── erb │ ├── version.rb │ ├── def_method.rb │ ├── util.rb │ └── compiler.rb └── erb.rb ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ ├── dependabot_automerge.yml │ ├── sync-ruby.yml │ └── push-gem.yml ├── LICENSE.txt ├── bin ├── setup └── console ├── .gitignore ├── Gemfile ├── ext └── erb │ └── escape │ ├── extconf.rb │ └── escape.c ├── Rakefile ├── BDSL ├── erb.gemspec ├── COPYING ├── NEWS.md ├── README.md └── libexec └── erb /.rdoc_options: -------------------------------------------------------------------------------- 1 | main_page: README.md 2 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | LICENSE.txt 2 | NEWS.md 3 | README.md 4 | ext/ 5 | lib/ 6 | _doc/ 7 | -------------------------------------------------------------------------------- /test/erb/hello.erb: -------------------------------------------------------------------------------- 1 | = hello 2 | <% 3.times do |n| %> 3 | * <%= n %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /_doc/cgi.rb: -------------------------------------------------------------------------------- 1 | # See {CGI document}[https://docs.ruby-lang.org/en/master/CGI.html] 2 | module CGI 3 | end 4 | -------------------------------------------------------------------------------- /lib/erb/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class ERB 3 | # The string \ERB version. 4 | VERSION = '6.0.0' 5 | end 6 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | All the files in this distribution are covered under either the Ruby's 2 | license (see the file COPYING) or BSD-2-Clause license (see the file BSDL). 3 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | *.so 11 | *.bundle 12 | *.gem 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'rake' 7 | gem 'rake-compiler' 8 | gem 'rdoc' 9 | gem 'test-unit' 10 | gem "test-unit-ruby-core" 11 | end 12 | -------------------------------------------------------------------------------- /ext/erb/escape/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | case RUBY_ENGINE 4 | when 'jruby', 'truffleruby' 5 | File.write('Makefile', dummy_makefile($srcdir).join) 6 | else 7 | have_func("rb_ext_ractor_safe", "ruby.h") 8 | create_makefile 'erb/escape' 9 | end 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "erb" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << 'test/lib' 6 | t.ruby_opts << '-rhelper' 7 | t.test_files = FileList['test/**/test_*.rb'] 8 | end 9 | 10 | case RUBY_ENGINE 11 | when 'jruby', 'truffleruby' 12 | # not using C extension 13 | else 14 | require 'rake/extensiontask' 15 | Rake::ExtensionTask.new('erb/escape') 16 | task test: :compile 17 | end 18 | 19 | task default: :test 20 | -------------------------------------------------------------------------------- /test/erb/test_erb_command.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: us-ascii -*- 2 | # frozen_string_literal: false 3 | require 'test/unit' 4 | 5 | class TestErbCommand < Test::Unit::TestCase 6 | def test_var 7 | pend if RUBY_ENGINE == 'truffleruby' 8 | assert_in_out_err(["-I#{File.expand_path('../../lib', __dir__)}", 9 | File.expand_path("../../libexec/erb", __dir__), 10 | "var=hoge"], 11 | "<%=var%>", ["hoge"]) 12 | end 13 | 14 | def test_template_file_encoding 15 | pend if RUBY_ENGINE == 'truffleruby' 16 | assert_in_out_err(["-I#{File.expand_path('../../lib', __dir__)}", 17 | File.expand_path("../../libexec/erb", __dir__)], 18 | "<%=''.encoding.to_s%>", ["UTF-8"]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | versions: '["jruby", "truffleruby-head"]' 15 | 16 | test: 17 | needs: ruby-versions 18 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 19 | strategy: 20 | matrix: 21 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 22 | os: [ubuntu-latest] 23 | fail-fast: false 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v6 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true 32 | - name: Run test 33 | run: bundle exec rake test 34 | - name: RDoc coverage 35 | run: | 36 | rdoc -C --visibility=private . 37 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | pull_request: 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | jobs: 10 | automerge: 11 | runs-on: ubuntu-latest 12 | if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'ruby/erb' 13 | steps: 14 | - name: Dependabot metadata 15 | uses: dependabot/fetch-metadata@v2 16 | id: metadata 17 | 18 | - name: Wait for status checks 19 | uses: lewagon/wait-on-check-action@v1 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 23 | check-regexp: 'build \(.*\)' 24 | wait-interval: 30 25 | 26 | - name: Auto-merge for Dependabot PRs 27 | run: gh pr merge --auto --rebase "$PR_URL" 28 | env: 29 | PR_URL: ${{ github.event.pull_request.html_url }} 30 | GITHUB_TOKEN: ${{ secrets.MATZBOT_DEPENDABOT_MERGE_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /lib/erb/def_method.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # ERB::DefMethod 4 | # 5 | # Utility module to define eRuby script as instance method. 6 | # 7 | # === Example 8 | # 9 | # example.rhtml: 10 | # <% for item in @items %> 11 | # <%= item %> 12 | # <% end %> 13 | # 14 | # example.rb: 15 | # require 'erb' 16 | # class MyClass 17 | # extend ERB::DefMethod 18 | # def_erb_method('render()', 'example.rhtml') 19 | # def initialize(items) 20 | # @items = items 21 | # end 22 | # end 23 | # print MyClass.new([10,20,30]).render() 24 | # 25 | # result: 26 | # 27 | # 10 28 | # 29 | # 20 30 | # 31 | # 30 32 | # 33 | module ERB::DefMethod 34 | # define _methodname_ as instance method of current module, using ERB 35 | # object or eRuby file 36 | def def_erb_method(methodname, erb_or_fname) 37 | if erb_or_fname.kind_of? String 38 | fname = erb_or_fname 39 | erb = ERB.new(File.read(fname)) 40 | erb.def_method(self, methodname, fname) 41 | else 42 | erb = erb_or_fname 43 | erb.def_method(self, methodname, erb.filename || '(ERB)') 44 | end 45 | end 46 | module_function :def_erb_method 47 | end 48 | -------------------------------------------------------------------------------- /BDSL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /erb.gemspec: -------------------------------------------------------------------------------- 1 | begin 2 | require_relative 'lib/erb/version' 3 | rescue LoadError 4 | # for Ruby core repository 5 | require_relative 'version' 6 | end 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = 'erb' 10 | spec.version = ERB::VERSION 11 | spec.authors = ['Masatoshi SEKI', 'Takashi Kokubun'] 12 | spec.email = ['seki@ruby-lang.org', 'k0kubun@ruby-lang.org'] 13 | 14 | spec.summary = %q{An easy to use but powerful templating system for Ruby.} 15 | spec.description = %q{An easy to use but powerful templating system for Ruby.} 16 | spec.homepage = 'https://github.com/ruby/erb' 17 | spec.licenses = ['Ruby', 'BSD-2-Clause'] 18 | 19 | spec.metadata['homepage_uri'] = spec.homepage 20 | spec.metadata['source_code_uri'] = spec.homepage 21 | spec.metadata['changelog_uri'] = "https://github.com/ruby/erb/blob/v#{spec.version}/NEWS.md" 22 | 23 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | end 26 | spec.bindir = 'libexec' 27 | spec.executables = ['erb'] 28 | spec.require_paths = ['lib'] 29 | 30 | spec.required_ruby_version = '>= 3.2.0' 31 | 32 | if RUBY_ENGINE == 'jruby' 33 | spec.platform = 'java' 34 | else 35 | spec.extensions = ['ext/erb/escape/extconf.rb'] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /.github/workflows/push-gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository_owner == 'ruby' 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | ruby: [ruby, jruby] 18 | fail-fast: false 19 | 20 | environment: 21 | name: rubygems.org 22 | url: https://rubygems.org/gems/erb 23 | 24 | permissions: 25 | contents: write 26 | id-token: write 27 | 28 | steps: 29 | - name: Harden Runner 30 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 31 | with: 32 | egress-policy: audit 33 | 34 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 35 | 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 38 | with: 39 | bundler-cache: true 40 | ruby-version: ${{ matrix.ruby }} 41 | 42 | - name: Publish to RubyGems 43 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 44 | 45 | - name: Create GitHub release 46 | run: | 47 | tag_name="$(git describe --tags --abbrev=0)" 48 | gh release create "${tag_name}" --verify-tag --generate-notes 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | if: ${{ matrix.ruby == 'ruby' }} 52 | -------------------------------------------------------------------------------- /lib/erb/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load CGI.escapeHTML and CGI.escapeURIComponent. 4 | # CRuby: 5 | # cgi.gem v0.1.0+ (Ruby 2.7-3.4) and Ruby 4.0+ stdlib have 'cgi/escape' and CGI.escapeHTML. 6 | # cgi.gem v0.3.3+ (Ruby 3.2-3.4) and Ruby 4.0+ stdlib have CGI.escapeURIComponent. 7 | # JRuby: cgi.gem has a Java extension 'cgi/escape'. 8 | # TruffleRuby: lib/truffle/cgi/escape.rb requires 'cgi/util'. 9 | require 'cgi/escape' 10 | 11 | # Load or define ERB::Escape#html_escape. 12 | # We don't build the C extension 'cgi/escape' for JRuby, TruffleRuby, and WASM. 13 | # miniruby (used by CRuby build scripts) also fails to load erb/escape.so. 14 | begin 15 | require 'erb/escape' 16 | rescue LoadError 17 | # ERB::Escape 18 | # 19 | # A subset of ERB::Util. Unlike ERB::Util#html_escape, we expect/hope 20 | # Rails will not monkey-patch ERB::Escape#html_escape. 21 | module ERB::Escape 22 | # :stopdoc: 23 | def html_escape(s) 24 | CGI.escapeHTML(s.to_s) 25 | end 26 | module_function :html_escape 27 | end 28 | end 29 | 30 | # ERB::Util 31 | # 32 | # A utility module for conversion routines, often handy in HTML generation. 33 | module ERB::Util 34 | # 35 | # A utility method for escaping HTML tag characters in _s_. 36 | # 37 | # require "erb" 38 | # include ERB::Util 39 | # 40 | # puts html_escape("is a > 0 & a < 10?") 41 | # 42 | # _Generates_ 43 | # 44 | # is a > 0 & a < 10? 45 | # 46 | include ERB::Escape # html_escape 47 | module_function :html_escape 48 | alias h html_escape 49 | module_function :h 50 | 51 | if CGI.respond_to?(:escapeURIComponent) 52 | # 53 | # A utility method for encoding the String _s_ as a URL. 54 | # 55 | # require "erb" 56 | # include ERB::Util 57 | # 58 | # puts url_encode("Programming Ruby: The Pragmatic Programmer's Guide") 59 | # 60 | # _Generates_ 61 | # 62 | # Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide 63 | # 64 | def url_encode(s) 65 | CGI.escapeURIComponent(s.to_s) 66 | end 67 | else # cgi.gem <= v0.3.2 68 | def url_encode(s) 69 | s.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/n) do |m| 70 | sprintf("%%%02X", m.unpack1("C")) 71 | end 72 | end 73 | end 74 | alias u url_encode 75 | module_function :u 76 | module_function :url_encode 77 | end 78 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 6.0.0 4 | 5 | * Remove `safe_level` and further positional arguments from `ERB.new` 6 | * Remove deprecated constant `ERB::Revision` 7 | 8 | ## 5.1.3 9 | 10 | * Release v5.1.2 with trusted publishing for JRuby 11 | 12 | ## 5.1.2 13 | 14 | * Add `changelog_uri` to spec metadata https://github.com/ruby/erb/pull/89 15 | 16 | ## 5.1.1 17 | 18 | * Fix integer overflow that is introduced at v5.1.0 19 | 20 | ## 5.1.0 21 | 22 | * html_escape: Avoid buffer allocation for strings with no escapable character https://github.com/ruby/erb/pull/87 23 | 24 | ## 5.0.3 25 | 26 | * Update help of erb(1) [#85](https://github.com/ruby/erb/pull/85) 27 | 28 | ## 5.0.2 29 | 30 | * Declare escape functions as Ractor-safe [#63](https://github.com/ruby/erb/pull/63) 31 | 32 | ## 5.0.1 33 | 34 | * Rescue `LoadError` when failing to load `erb/escape` 35 | 36 | ## 5.0.0 37 | 38 | * Bump `required_ruby_version` to Ruby 3.2+ [#60](https://github.com/ruby/erb/pull/60) 39 | * Drop `cgi` from runtime dependencies [#59](https://github.com/ruby/erb/pull/59) 40 | * Make `ERB::VERSION` public 41 | 42 | ## 4.0.4 43 | 44 | * Skip building the C extension for JRuby [#52](https://github.com/ruby/erb/pull/52) 45 | 46 | ## 4.0.3 47 | 48 | * Enable `frozen_string_literal` in all files [#49](https://github.com/ruby/erb/pull/49) 49 | 50 | ## 4.0.2 51 | 52 | * Fix line numbers after multi-line `<%#` [#42](https://github.com/ruby/erb/pull/42) 53 | 54 | ## 4.0.1 55 | 56 | * Stop building the C extension for TruffleRuby [#39](https://github.com/ruby/erb/pull/39) 57 | 58 | ## 4.0.0 59 | 60 | * Optimize `ERB::Util.html_escape` [#27](https://github.com/ruby/erb/pull/27) 61 | * No longer duplicate an argument string when nothing is escaped. 62 | * This makes `ERB::Util.html_escape` faster than `CGI.escapeHTML` in no-escape cases. 63 | * It skips calling `#to_s` when an argument is already a String. 64 | * Define `ERB::Escape.html_escape` as an alias to `ERB::Util.html_escape` [#38](https://github.com/ruby/erb/pull/38) 65 | * `ERB::Util.html_escape` is known to be monkey-patched by Rails. 66 | `ERB::Escape.html_escape` is useful when you want a non-monkey-patched version. 67 | * Drop deprecated `-S` option from `erb` command 68 | 69 | ## 3.0.0 70 | 71 | * Bump `required_ruby_version` to Ruby 2.7+ [#23](https://github.com/ruby/erb/pull/23) 72 | * `ERB::Util.url_encode` uses a native implementation [#23](https://github.com/ruby/erb/pull/23) 73 | * Fix a bug that a magic comment with a wrong format could be detected [#6](https://github.com/ruby/erb/pull/6) 74 | 75 | ## 2.2.3 76 | 77 | * Bump `required_ruby_version` from 2.3 to 2.5 as it has never been supported [#3](https://github.com/ruby/erb/pull/3) 78 | 79 | ## 2.2.2 80 | 81 | * `ERB.version` returns just a version number 82 | * `ERB::Revision` is deprecated 83 | 84 | ## 2.2.1 85 | 86 | * `ERB#initialize` warns `safe_level` and later arguments even without -w 87 | 88 | ## 2.2.0 89 | 90 | * Ruby 3.0 promoted ERB to a default gem 91 | -------------------------------------------------------------------------------- /ext/erb/escape/escape.c: -------------------------------------------------------------------------------- 1 | #include "ruby.h" 2 | #include "ruby/encoding.h" 3 | 4 | static VALUE rb_cERB, rb_mEscape, rb_cCGI; 5 | static ID id_escapeHTML; 6 | 7 | #define HTML_ESCAPE_MAX_LEN 6 8 | 9 | static const struct { 10 | uint8_t len; 11 | char str[HTML_ESCAPE_MAX_LEN+1]; 12 | } html_escape_table[UCHAR_MAX+1] = { 13 | #define HTML_ESCAPE(c, str) [c] = {rb_strlen_lit(str), str} 14 | HTML_ESCAPE('\'', "'"), 15 | HTML_ESCAPE('&', "&"), 16 | HTML_ESCAPE('"', """), 17 | HTML_ESCAPE('<', "<"), 18 | HTML_ESCAPE('>', ">"), 19 | #undef HTML_ESCAPE 20 | }; 21 | 22 | static inline void 23 | preserve_original_state(VALUE orig, VALUE dest) 24 | { 25 | rb_enc_associate(dest, rb_enc_get(orig)); 26 | } 27 | 28 | static inline long 29 | escaped_length(VALUE str) 30 | { 31 | const long len = RSTRING_LEN(str); 32 | if (len >= LONG_MAX / HTML_ESCAPE_MAX_LEN) { 33 | ruby_malloc_size_overflow(len, HTML_ESCAPE_MAX_LEN); 34 | } 35 | return len * HTML_ESCAPE_MAX_LEN; 36 | } 37 | 38 | static VALUE 39 | optimized_escape_html(VALUE str) 40 | { 41 | VALUE vbuf; 42 | char *buf = NULL; 43 | const char *cstr = RSTRING_PTR(str); 44 | const char *end = cstr + RSTRING_LEN(str); 45 | 46 | const char *segment_start = cstr; 47 | char *dest = NULL; 48 | while (cstr < end) { 49 | const unsigned char c = *cstr++; 50 | uint8_t len = html_escape_table[c].len; 51 | if (len) { 52 | size_t segment_len = cstr - segment_start - 1; 53 | if (!buf) { 54 | buf = ALLOCV_N(char, vbuf, escaped_length(str)); 55 | dest = buf; 56 | } 57 | if (segment_len) { 58 | memcpy(dest, segment_start, segment_len); 59 | dest += segment_len; 60 | } 61 | segment_start = cstr; 62 | memcpy(dest, html_escape_table[c].str, len); 63 | dest += len; 64 | } 65 | } 66 | VALUE escaped = str; 67 | if (buf) { 68 | size_t segment_len = cstr - segment_start; 69 | if (segment_len) { 70 | memcpy(dest, segment_start, segment_len); 71 | dest += segment_len; 72 | } 73 | escaped = rb_str_new(buf, dest - buf); 74 | preserve_original_state(str, escaped); 75 | ALLOCV_END(vbuf); 76 | } 77 | return escaped; 78 | } 79 | 80 | /* 81 | * ERB::Util.html_escape is similar to CGI.escapeHTML but different in the following two parts: 82 | * 83 | * * ERB::Util.html_escape converts an argument with #to_s first (only if it's not T_STRING) 84 | * * ERB::Util.html_escape does not allocate a new string when nothing needs to be escaped 85 | */ 86 | static VALUE 87 | erb_escape_html(VALUE self, VALUE str) 88 | { 89 | if (!RB_TYPE_P(str, T_STRING)) { 90 | str = rb_convert_type(str, T_STRING, "String", "to_s"); 91 | } 92 | 93 | if (rb_enc_str_asciicompat_p(str)) { 94 | return optimized_escape_html(str); 95 | } 96 | else { 97 | return rb_funcall(rb_cCGI, id_escapeHTML, 1, str); 98 | } 99 | } 100 | 101 | void 102 | Init_escape(void) 103 | { 104 | #ifdef HAVE_RB_EXT_RACTOR_SAFE 105 | rb_ext_ractor_safe(true); 106 | #endif 107 | 108 | rb_cERB = rb_define_class("ERB", rb_cObject); 109 | rb_mEscape = rb_define_module_under(rb_cERB, "Escape"); 110 | rb_define_module_function(rb_mEscape, "html_escape", erb_escape_html, 1); 111 | 112 | rb_cCGI = rb_define_class("CGI", rb_cObject); 113 | id_escapeHTML = rb_intern("escapeHTML"); 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERB (Embedded Ruby) 2 | 3 | ERB is an easy-to-use, but also very powerful, [template processor][template processor]. 4 | 5 | ERB is commonly used to produce: 6 | 7 | - Customized or personalized email messages. 8 | - Customized or personalized web pages. 9 | - Software code (in code-generating applications). 10 | 11 | Like method [sprintf][sprintf], ERB can format run-time data into a string. 12 | ERB, however, is *much more powerful* 13 | 14 | ## How ERB Works 15 | 16 | Using ERB, you can create a *template*: a plain-text string that has specially-formatted *tags*, 17 | then store it into an ERB object; 18 | when ERB produces _result_ string, it: 19 | 20 | - Inserts run-time-evaluated expressions into the result. 21 | - Executes snippets of Ruby code. 22 | - Omits comments from the results. 23 | 24 | In the result: 25 | 26 | - All non-tag text is passed through, _unchanged_. 27 | - Each tag is either _replaced_ (expression tag), 28 | or _omitted_ entirely (execution tag or comment tag). 29 | 30 | There are three types of tags: 31 | 32 | | Tag | Form | Action | Text in Result | 33 | |----------------|:------------------------------------:|:-------------------------------------:|:--------------------:| 34 | | Expression tag | '<%= _ruby_expression_ %>' | Evaluates _ruby_expression_. | Value of expression. | 35 | | Execution tag | '<% _ruby_code_ %>' | Execute _ruby_code_. | None. | 36 | | Comment tag | '<%# _comment_text_ %>' | None. | None. | 37 | 38 | These examples use `erb`, the ERB command-line interface; 39 | each "echoes" a string template and pipes it to `erb` as input: 40 | 41 | 42 | - Expression tag: 43 | 44 | $ echo "<%= $VERBOSE %>" | erb 45 | "false" 46 | $ echo "<%= 2 + 2 %>" | erb 47 | "4" 48 | 49 | - Execution tag: 50 | 51 | echo "<% if $VERBOSE %> Long message. <% else %> Short message. <% end %>" | erb 52 | " Short message. " 53 | 54 | - Comment tag: 55 | 56 | echo "<%# TODO: Fix this nonsense. %> Nonsense." | erb 57 | " Nonsense." 58 | 59 | ## How to Use ERB 60 | 61 | You can use ERB either: 62 | 63 | - In a program: see class ERB. 64 | - From the command line: see [ERB Executable][erb executable]. 65 | 66 | ## Installation 67 | 68 | ERB is installed with Ruby, and so there's no further installation needed. 69 | 70 | ## Other Template Engines 71 | 72 | There are a variety of template engines available in various Ruby projects. 73 | For example, [RDoc][rdoc], distributed with Ruby, uses its own template engine, which 74 | can be reused elsewhere. 75 | 76 | Other popular template engines may be found in the [Ruby Toolbox][ruby toolbox]. 77 | 78 | ## Code 79 | 80 | The ERB source code is in GitHub project [ruby/erb][ruby/erb]. 81 | 82 | ## Bugs 83 | 84 | Bugs may be reported at [ERB Issues][erb issues]. 85 | 86 | ## License 87 | 88 | This software is available as open source under the terms 89 | of the [2-Clause BSD License][2-clause bsd license]. 90 | 91 | [2-clause bsd license]: https://opensource.org/licenses/BSD-2-Clause 92 | [erb executable]: rdoc-ref:erb_executable.md 93 | [erb issues]: https://github.com/ruby/erb/issues 94 | [rdoc]: https://ruby.github.io/rdoc/ 95 | [ruby/erb]: https://github.com/ruby/erb 96 | [ruby toolbox]: https://www.ruby-toolbox.com/categories/template_engines 97 | [sprintf]: https://docs.ruby-lang.org/en/master/Kernel.html#method-i-sprintf 98 | [template processor]: https://en.wikipedia.org/wiki/Template_processor_ 99 | -------------------------------------------------------------------------------- /test/erb/test_erb_m17n.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # frozen_string_literal: false 3 | require 'test/unit' 4 | require 'erb' 5 | 6 | class TestERBEncoding < Test::Unit::TestCase 7 | def test_result_encoding 8 | erb = ERB.new("hello") 9 | assert_equal __ENCODING__, erb.result.encoding 10 | 11 | erb = ERB.new("こんにちは".encode("EUC-JP")) 12 | assert_equal Encoding::EUC_JP, erb.result.encoding 13 | 14 | erb = ERB.new("\xC4\xE3\xBA\xC3".force_encoding("EUC-CN")) 15 | assert_equal Encoding::EUC_CN, erb.result.encoding 16 | 17 | erb = ERB.new("γεια σας".encode("ISO-8859-7")) 18 | assert_equal Encoding::ISO_8859_7, erb.result.encoding 19 | 20 | assert_raise(ArgumentError, /ASCII compatible/) { 21 | ERB.new("こんにちは".force_encoding("ISO-2022-JP")) # dummy encoding 22 | } 23 | end 24 | 25 | def test_generate_magic_comment 26 | erb = ERB.new("hello") 27 | assert_match(/#coding:UTF-8/, erb.src) 28 | 29 | erb = ERB.new("hello".force_encoding("EUC-JP")) 30 | assert_match(/#coding:EUC-JP/, erb.src) 31 | 32 | erb = ERB.new("hello".force_encoding("ISO-8859-9")) 33 | assert_match(/#coding:ISO-8859-9/, erb.src) 34 | end 35 | 36 | def test_literal_encoding 37 | erb = ERB.new("literal encoding is <%= 'hello'.encoding %>") 38 | assert_match(/literal encoding is UTF-8/, erb.result) 39 | 40 | erb = ERB.new("literal encoding is <%= 'こんにちは'.encoding %>".encode("EUC-JP")) 41 | assert_match(/literal encoding is EUC-JP/, erb.result) 42 | 43 | erb = ERB.new("literal encoding is <%= '\xC4\xE3\xBA\xC3'.encoding %>".force_encoding("EUC-CN")) 44 | assert_match(/literal encoding is GB2312/, erb.result) 45 | end 46 | 47 | def test___ENCODING__ 48 | erb = ERB.new("__ENCODING__ is <%= __ENCODING__ %>") 49 | assert_match(/__ENCODING__ is UTF-8/, erb.result) 50 | 51 | erb = ERB.new("__ENCODING__ is <%= __ENCODING__ %>".force_encoding("EUC-JP")) 52 | assert_match(/__ENCODING__ is EUC-JP/, erb.result) 53 | 54 | erb = ERB.new("__ENCODING__ is <%= __ENCODING__ %>".force_encoding("Big5")) 55 | assert_match(/__ENCODING__ is Big5/, erb.result) 56 | end 57 | 58 | def test_recognize_magic_comment 59 | erb = ERB.new(<<-EOS.encode("EUC-KR")) 60 | <%# -*- coding: EUC-KR -*- %> 61 | 안녕하세요 62 | EOS 63 | assert_match(/#coding:EUC-KR/, erb.src) 64 | assert_equal Encoding::EUC_KR, erb.result.encoding 65 | 66 | erb = ERB.new(<<-EOS.encode("EUC-KR").force_encoding("ASCII-8BIT")) 67 | <%#-*- coding: EUC-KR -*-%> 68 | 안녕하세요 69 | EOS 70 | assert_match(/#coding:EUC-KR/, erb.src) 71 | assert_equal Encoding::EUC_KR, erb.result.encoding 72 | 73 | erb = ERB.new(<<-EOS.encode("EUC-KR").force_encoding("ASCII-8BIT")) 74 | <%# vim: tabsize=8 encoding=EUC-KR shiftwidth=2 expandtab %> 75 | 안녕하세요 76 | EOS 77 | assert_match(/#coding:EUC-KR/, erb.src) 78 | assert_equal Encoding::EUC_KR, erb.result.encoding 79 | 80 | erb = ERB.new(<<-EOS.encode("EUC-KR").force_encoding("ASCII-8BIT")) 81 | <%#coding:EUC-KR %> 82 | 안녕하세요 83 | EOS 84 | assert_match(/#coding:EUC-KR/, erb.src) 85 | assert_equal Encoding::EUC_KR, erb.result.encoding 86 | 87 | erb = ERB.new(<<-EOS.encode("EUC-KR").force_encoding("EUC-JP")) 88 | <%#coding:EUC-KR %> 89 | 안녕하세요 90 | EOS 91 | assert_match(/#coding:EUC-KR/, erb.src) 92 | assert_equal Encoding::EUC_KR, erb.result.encoding 93 | end 94 | 95 | module M; end 96 | def test_method_with_encoding 97 | obj = Object.new 98 | obj.extend(M) 99 | 100 | erb = ERB.new(<<-EOS.encode("EUC-JP").force_encoding("ASCII-8BIT")) 101 | <%#coding:EUC-JP %> 102 | literal encoding is <%= 'こんにちは'.encoding %> 103 | __ENCODING__ is <%= __ENCODING__ %> 104 | EOS 105 | erb.def_method(M, :m_from_magic_comment) 106 | 107 | result = obj.m_from_magic_comment 108 | assert_equal Encoding::EUC_JP, result.encoding 109 | assert_match(/literal encoding is EUC-JP/, result) 110 | assert_match(/__ENCODING__ is EUC-JP/, result) 111 | 112 | erb = ERB.new(<<-EOS.encode("EUC-KR")) 113 | literal encoding is <%= '안녕하세요'.encoding %> 114 | __ENCODING__ is <%= __ENCODING__ %> 115 | EOS 116 | erb.def_method(M, :m_from_eval_encoding) 117 | result = obj.m_from_eval_encoding 118 | assert_equal Encoding::EUC_KR, result.encoding 119 | assert_match(/literal encoding is EUC-KR/, result) 120 | assert_match(/__ENCODING__ is EUC-KR/, result) 121 | end 122 | end 123 | 124 | # vim:fileencoding=UTF-8 125 | -------------------------------------------------------------------------------- /libexec/erb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Tiny eRuby --- ERB2 3 | # Copyright (c) 1999-2000,2002 Masatoshi SEKI 4 | # You can redistribute it and/or modify it under the same terms as Ruby. 5 | 6 | require 'erb' 7 | 8 | class ERB 9 | module Main 10 | def ARGV.switch 11 | return nil if self.empty? 12 | arg = self.shift 13 | return nil if arg == '--' 14 | case arg 15 | when /\A-(.)(.*)/ 16 | if $1 == '-' 17 | arg, @maybe_arg = arg.split(/=/, 2) 18 | return arg 19 | end 20 | raise 'unknown switch "-"' if $2[0] == ?- and $1 != 'T' 21 | if $2.size > 0 22 | self.unshift "-#{$2}" 23 | @maybe_arg = $2 24 | else 25 | @maybe_arg = nil 26 | end 27 | "-#{$1}" 28 | when /\A(\w+)=/ 29 | arg 30 | else 31 | self.unshift arg 32 | nil 33 | end 34 | end 35 | 36 | def ARGV.req_arg 37 | (@maybe_arg || self.shift || raise('missing argument')).tap { 38 | @maybe_arg = nil 39 | } 40 | end 41 | 42 | def trim_mode_opt(trim_mode, disable_percent) 43 | return trim_mode if disable_percent 44 | case trim_mode 45 | when 0 46 | return '%' 47 | when 1 48 | return '%>' 49 | when 2 50 | return '%<>' 51 | when '-' 52 | return '%-' 53 | end 54 | end 55 | module_function :trim_mode_opt 56 | 57 | def run(factory=ERB) 58 | trim_mode = 0 59 | disable_percent = false 60 | variables = {} 61 | begin 62 | while switch = ARGV.switch 63 | case switch 64 | when '-x' # ruby source 65 | output = true 66 | when '-n' # line number 67 | number = true 68 | when '-v' # verbose 69 | $VERBOSE = true 70 | when '--version' # version 71 | STDERR.puts factory.version 72 | exit 73 | when '-d', '--debug' # debug 74 | $DEBUG = true 75 | when '-r' # require 76 | require ARGV.req_arg 77 | when '-T' # trim mode 78 | arg = ARGV.req_arg 79 | if arg == '-' 80 | trim_mode = arg 81 | next 82 | end 83 | raise "invalid trim mode #{arg.dump}" unless arg =~ /\A[0-2]\z/ 84 | trim_mode = arg.to_i 85 | when '-E', '--encoding' 86 | arg = ARGV.req_arg 87 | set_encoding(*arg.split(/:/, 2)) 88 | when '-U' 89 | set_encoding(Encoding::UTF_8, Encoding::UTF_8) 90 | when '-P' 91 | disable_percent = true 92 | when '--help' 93 | raise '' 94 | when /\A-/ 95 | raise "Unknown switch: #{switch.dump}" 96 | else 97 | var, val = *switch.split('=', 2) 98 | (variables ||= {})[var] = val 99 | end 100 | end 101 | rescue # usage 102 | message = $!.to_s 103 | STDERR.puts message unless message.empty? 104 | STDERR.puts 'Usage:' 105 | STDERR.puts " #{File.basename($0)} [options] [filepaths]" 106 | STDERR.puts <'; '2' means '<>'; '-' means '%-'. 119 | -U Set default encoding to UTF-8. 120 | -v Set $VERBOSE to enable debugging, 121 | --version Print ERB version string and exit. 122 | -x Print generated Ruby source code. 123 | -- Treat all following words as filepaths (not options). 124 | name=value Set the variable named name to the given string value. 125 | 126 | Filepaths: 127 | The erb program reads the text from all files at the filepaths as a single ERB template: 128 | plain text, possibly with embedded ERB tags; 129 | filepaths may be repeated. 130 | 131 | The pseudo-filepath '-' (hyphen character) specifies the standard input. 132 | 133 | If no filepaths are given, the sole input is the standard input. 134 | 135 | See details and examples at https://docs.ruby-lang.org/en/master/erb_executable_md.html 136 | EOU 137 | exit 1 138 | end 139 | 140 | $<.set_encoding(Encoding::UTF_8, nil) 141 | src = $<.read 142 | filename = $FILENAME 143 | exit 2 unless src 144 | trim = trim_mode_opt(trim_mode, disable_percent) 145 | erb = factory.new(src, trim_mode: trim) 146 | erb.filename = filename 147 | if output 148 | if number 149 | erb.src.each_line.with_index do |line, l| 150 | puts "%3d %s"%[l+1, line] 151 | end 152 | else 153 | puts erb.src 154 | end 155 | else 156 | bind = TOPLEVEL_BINDING 157 | if variables 158 | enc = erb.encoding 159 | for var, val in variables do 160 | val = val.encode(enc) if val 161 | bind.local_variable_set(var, val) 162 | end 163 | end 164 | erb.run(bind) 165 | end 166 | end 167 | module_function :run 168 | 169 | def set_encoding(extern, intern = nil) 170 | verbose, $VERBOSE = $VERBOSE, nil 171 | Encoding.default_external = extern unless extern.nil? || extern == "" 172 | Encoding.default_internal = intern unless intern.nil? || intern == "" 173 | [$stdin, $stdout, $stderr].each do |io| 174 | io.set_encoding(extern, intern) 175 | end 176 | ensure 177 | $VERBOSE = verbose 178 | end 179 | module_function :set_encoding 180 | class << self; private :set_encoding; end 181 | end 182 | end 183 | 184 | ERB::Main.run 185 | -------------------------------------------------------------------------------- /_doc/erb_executable.md: -------------------------------------------------------------------------------- 1 | # \ERB Executable 2 | 3 | The `erb` executable gives command-line access to ERB template processing. 4 | 5 | The executable is installed with \ERB, which is part of the Ruby installation. 6 | 7 | For a quick summary, type: 8 | 9 | ```bash 10 | $ erb --help 11 | ``` 12 | 13 | The format of the command is 14 | `erb [_options_] [_filepaths_]`, 15 | where: 16 | 17 | - _options_ are zero or more [options][options]. 18 | - _filepaths_ are zero or more paths to files, each containing an plain text 19 | that can include \ERB tags. 20 | 21 | ## Filepaths 22 | 23 | With one or more _filepaths_ given, `erb` reads all the given files as a single template; 24 | that is, `erb` processes multiple files into a single result: 25 | 26 | ```bash 27 | $ cat t.erb 28 | <%= RUBY_VERSION %> 29 | <%= Time.now %> 30 | $ cat u.erb 31 | % Encoding.list.take(4).each do |encoding| 32 | * <%= encoding %> 33 | % end 34 | $ erb t.erb u.erb 35 | 3.4.5 36 | 2025-09-24 00:23:00 +0100 37 | * ASCII-8BIT 38 | * UTF-8 39 | * US-ASCII 40 | * UTF-16BE 41 | ``` 42 | 43 | There is a special "filepath", `'-'`, that specifies the standard input: 44 | 45 | ```bash 46 | $ echo "<%= RUBY_VERSION %>" | erb u.erb - 47 | * ASCII-8BIT 48 | * UTF-8 49 | * US-ASCII 50 | * UTF-16BE 51 | 3.4.5 52 | ``` 53 | 54 | Any filepath, including `'-'`, may be repeated. 55 | 56 | With no _filepaths_ given, `erb` reads and processes the standard input: 57 | 58 | ```bash 59 | $ echo "<%= RUBY_VERSION %>" | erb # Prints the ERB version string. 60 | ``` 61 | 62 | ## Options 63 | 64 | ### `-d`, `--debug`: Set $DEBUG 65 | 66 | Use option `-d` or `--debug` to turn on debugging output: 67 | 68 | ```bash 69 | $ echo "<%= $DEBUG %>" | erb 70 | "false" 71 | $echo "<%= $DEBUG %>" | erb --debug 72 | "true" 73 | ``` 74 | 75 | ### `-E`, `--encoding`: Set Encodings 76 | 77 | Use option `-E` or `--encoding` to set the default external encoding to `_ex_` 78 | and, if `_in_` is given, to set the default internal encoding to `_in_`. 79 | 80 | Each encoding, `ex` and `in`, must be the name of an Encoding: 81 | 82 | ``` 83 | erb -E ASCII-8BIT:ASCII-8BIT t.erb 84 | ``` 85 | 86 | ### `-h`, `--help`: Print Help 87 | 88 | Use option `-h` or `--help` to print `erb` help text: 89 | 90 | ```bash 91 | $ erb --help 92 | ``` 93 | 94 | ### `-n`: Print Source with Line Numbers 95 | 96 | Use option `-n` with option `-x` to print the output of ERB#src, 97 | with numbered lines: 98 | 99 | ```bash 100 | $ cat t.erb 101 | <%= RUBY_VERSION %> 102 | <%= Time.now %> 103 | $ erb -n -x t.erb 104 | 1 #coding:UTF-8 105 | 2 _erbout = +''; _erbout.<<(( RUBY_VERSION ).to_s); _erbout.<< "\n".freeze 106 | 3 ; _erbout.<<(( Time.now ).to_s); _erbout.<< "\n".freeze 107 | 4 ; _erbout 108 | ``` 109 | 110 | Using option `-n` without option `-x` has no effect: 111 | 112 | ```bash 113 | $ erb -n t.erb 114 | 3.4.5 115 | 2025-09-23 02:44:57 +0100 116 | ``` 117 | 118 | ### `-P`: Disable Execution Tag Shorthand 119 | 120 | By default, `erb` enables [execution tag shorthand][execution tag shorthand]: 121 | 122 | ``` 123 | $ cat u.erb 124 | % Encoding.list.take(4).each do |encoding| 125 | * <%= encoding %> 126 | % end 127 | $ erb u.erb 128 | * ASCII-8BIT 129 | * UTF-8 130 | * US-ASCII 131 | * UTF-16BE 132 | ``` 133 | 134 | You can use option `-P` to disable the shorthand: 135 | 136 | ``` 137 | $ erb -P u.erb # Raises NameError: "undefined local variable or method 'encoding'" 138 | ``` 139 | 140 | ### `-r`: Load Library 141 | 142 | You can use option `-r` to load a library; 143 | the option may be given multiple times, to load multiple libraries: 144 | 145 | ``` 146 | $ erb -r csv -r bigdecimal t.erb 147 | ``` 148 | 149 | ### `-T`: Set Trim Mode 150 | 151 | You can use option `-T` to set the trim mode. 152 | 153 | The values for the option are: 154 | 155 | - `'0'`, meaning `'%'`; enable execution tag shorthand; 156 | see [execution tag shorthand][execution tag shorthand]. 157 | - `'1'`, meaning `'%>'`: enable execution tag shorthand and omit newline for each line ending with `'%>'`; 158 | see [suppressing unwanted newlines][suppressing unwanted newlines]. 159 | - `'2'`, meaning `'<>'`: to suppress the trailing newline for each line 160 | that both begins with `'<%'` and ends with `'%>'`; 161 | see [suppressing unwanted newlines][suppressing unwanted newlines]. 162 | - `'-'`, meaning `'%-'`: enable execution tag shorthand and omit each blank line ending with `'-%>'`. 163 | see [execution tag shorthand][execution tag shorthand] 164 | and [suppressing unwanted blank lines][suppressing unwanted blank lines]. 165 | 166 | Example: 167 | 168 | ```bash 169 | $ erb -T 0 t.erb 170 | ``` 171 | 172 | ### `-U`: Set Default Encodings to UTF-8 173 | 174 | You can use option `-U` to set both external and internal encodings to UTF-8: 175 | 176 | ```bash 177 | $ erb -U t.erb 178 | ``` 179 | 180 | ### `-v`: Set $VERBOSE 181 | 182 | Use option `-v` to turn on verbose output: 183 | 184 | ```bash 185 | $ $ "<%= $VERBOSE %>" | erb 186 | "false" 187 | $ echo "<%= $VERBOSE %>" | erb -v 188 | "true" 189 | ``` 190 | 191 | ### `-v`: Print \ERB Version 192 | 193 | Use option `--version` to print the \ERB version string: 194 | 195 | ```bash 196 | $ erb --version 197 | ``` 198 | 199 | ### `-x`: Print Source 200 | 201 | Use option `-x` to print the output of ERB#src, 202 | which is the Ruby code that is to be run when ERB#result is called: 203 | 204 | ```bash 205 | $ cat t.erb 206 | <%= RUBY_VERSION %> 207 | <%= Time.now %> 208 | $ erb -x t.erb 209 | #coding:UTF-8 210 | _erbout = +''; _erbout.<<(( RUBY_VERSION ).to_s); _erbout.<< "\n".freeze 211 | ; _erbout.<<(( Time.now ).to_s); _erbout.<< "\n".freeze 212 | ; _erbout 213 | ``` 214 | 215 | ### `--`: End of Options 216 | 217 | You can use option `'--'` to declare the end of options in the `erb` command; 218 | `erb` treats each word following as a filepath (even if it looks like an option): 219 | 220 | ``` 221 | erb -- --help # Raises Errno::ENOENT: "No such file or directory @ rb_sysopen - --help" 222 | ``` 223 | 224 | ### `name=value`: Set the Value of a Variable 225 | 226 | You can use option `name=value` to set the value of the variable named `name` 227 | to the given `value`. 228 | 229 | The option may be given multiple times to set multiple variables: 230 | 231 | ```bash 232 | $ echo "<%= foo %> <%= bar %>" | erb foo=1 bar=2 233 | "1 2" 234 | ``` 235 | 236 | [erb.new]: https://docs.ruby-lang.org/en/master/ERB.html#method-c-new. 237 | [execution tag shorthand]: rdoc-ref:ERB@Shorthand+Format+for+Execution+Tags 238 | [options]: rdoc-ref:erb_executable.md@Options 239 | [suppressing unwanted blank lines]: rdoc-ref:ERB@Suppressing+Unwanted+Blank+Lines 240 | [suppressing unwanted newlines]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines -------------------------------------------------------------------------------- /lib/erb/compiler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # ERB::Compiler 4 | # 5 | # Compiles ERB templates into Ruby code; the compiled code produces the 6 | # template result when evaluated. ERB::Compiler provides hooks to define how 7 | # generated output is handled. 8 | # 9 | # Internally ERB does something like this to generate the code returned by 10 | # ERB#src: 11 | # 12 | # compiler = ERB::Compiler.new('<>') 13 | # compiler.pre_cmd = ["_erbout=+''"] 14 | # compiler.put_cmd = "_erbout.<<" 15 | # compiler.insert_cmd = "_erbout.<<" 16 | # compiler.post_cmd = ["_erbout"] 17 | # 18 | # code, enc = compiler.compile("Got <%= obj %>!\n") 19 | # puts code 20 | # 21 | # Generates: 22 | # 23 | # #coding:UTF-8 24 | # _erbout=+''; _erbout.<< "Got ".freeze; _erbout.<<(( obj ).to_s); _erbout.<< "!\n".freeze; _erbout 25 | # 26 | # By default the output is sent to the print method. For example: 27 | # 28 | # compiler = ERB::Compiler.new('<>') 29 | # code, enc = compiler.compile("Got <%= obj %>!\n") 30 | # puts code 31 | # 32 | # Generates: 33 | # 34 | # #coding:UTF-8 35 | # print "Got ".freeze; print(( obj ).to_s); print "!\n".freeze 36 | # 37 | # == Evaluation 38 | # 39 | # The compiled code can be used in any context where the names in the code 40 | # correctly resolve. Using the last example, each of these print 'Got It!' 41 | # 42 | # Evaluate using a variable: 43 | # 44 | # obj = 'It' 45 | # eval code 46 | # 47 | # Evaluate using an input: 48 | # 49 | # mod = Module.new 50 | # mod.module_eval %{ 51 | # def get(obj) 52 | # #{code} 53 | # end 54 | # } 55 | # extend mod 56 | # get('It') 57 | # 58 | # Evaluate using an accessor: 59 | # 60 | # klass = Class.new Object 61 | # klass.class_eval %{ 62 | # attr_accessor :obj 63 | # def initialize(obj) 64 | # @obj = obj 65 | # end 66 | # def get_it 67 | # #{code} 68 | # end 69 | # } 70 | # klass.new('It').get_it 71 | # 72 | # Good! See also ERB#def_method, ERB#def_module, and ERB#def_class. 73 | class ERB::Compiler # :nodoc: 74 | class PercentLine # :nodoc: 75 | def initialize(str) 76 | @value = str 77 | end 78 | attr_reader :value 79 | alias :to_s :value 80 | end 81 | 82 | class Scanner # :nodoc: 83 | @scanner_map = defined?(Ractor) ? Ractor.make_shareable({}) : {} 84 | class << self 85 | if defined?(Ractor) 86 | def register_scanner(klass, trim_mode, percent) 87 | @scanner_map = Ractor.make_shareable({ **@scanner_map, [trim_mode, percent] => klass }) 88 | end 89 | else 90 | def register_scanner(klass, trim_mode, percent) 91 | @scanner_map[[trim_mode, percent]] = klass 92 | end 93 | end 94 | alias :regist_scanner :register_scanner 95 | end 96 | 97 | def self.default_scanner=(klass) 98 | @default_scanner = klass 99 | end 100 | 101 | def self.make_scanner(src, trim_mode, percent) 102 | klass = @scanner_map.fetch([trim_mode, percent], @default_scanner) 103 | klass.new(src, trim_mode, percent) 104 | end 105 | 106 | DEFAULT_STAGS = %w(<%% <%= <%# <%).freeze 107 | DEFAULT_ETAGS = %w(%%> %>).freeze 108 | def initialize(src, trim_mode, percent) 109 | @src = src 110 | @stag = nil 111 | @stags = DEFAULT_STAGS 112 | @etags = DEFAULT_ETAGS 113 | end 114 | attr_accessor :stag 115 | attr_reader :stags, :etags 116 | 117 | def scan; end 118 | end 119 | 120 | class TrimScanner < Scanner # :nodoc: 121 | def initialize(src, trim_mode, percent) 122 | super 123 | @trim_mode = trim_mode 124 | @percent = percent 125 | if @trim_mode == '>' 126 | @scan_reg = /(.*?)(%>\r?\n|#{(stags + etags).join('|')}|\n|\z)/m 127 | @scan_line = self.method(:trim_line1) 128 | elsif @trim_mode == '<>' 129 | @scan_reg = /(.*?)(%>\r?\n|#{(stags + etags).join('|')}|\n|\z)/m 130 | @scan_line = self.method(:trim_line2) 131 | elsif @trim_mode == '-' 132 | @scan_reg = /(.*?)(^[ \t]*<%\-|<%\-|-%>\r?\n|-%>|#{(stags + etags).join('|')}|\z)/m 133 | @scan_line = self.method(:explicit_trim_line) 134 | else 135 | @scan_reg = /(.*?)(#{(stags + etags).join('|')}|\n|\z)/m 136 | @scan_line = self.method(:scan_line) 137 | end 138 | end 139 | 140 | def scan(&block) 141 | @stag = nil 142 | if @percent 143 | @src.each_line do |line| 144 | percent_line(line, &block) 145 | end 146 | else 147 | @scan_line.call(@src, &block) 148 | end 149 | nil 150 | end 151 | 152 | def percent_line(line, &block) 153 | if @stag || line[0] != ?% 154 | return @scan_line.call(line, &block) 155 | end 156 | 157 | line[0] = '' 158 | if line[0] == ?% 159 | @scan_line.call(line, &block) 160 | else 161 | yield(PercentLine.new(line.chomp)) 162 | end 163 | end 164 | 165 | def scan_line(line) 166 | line.scan(@scan_reg) do |tokens| 167 | tokens.each do |token| 168 | next if token.empty? 169 | yield(token) 170 | end 171 | end 172 | end 173 | 174 | def trim_line1(line) 175 | line.scan(@scan_reg) do |tokens| 176 | tokens.each do |token| 177 | next if token.empty? 178 | if token == "%>\n" || token == "%>\r\n" 179 | yield('%>') 180 | yield(:cr) 181 | else 182 | yield(token) 183 | end 184 | end 185 | end 186 | end 187 | 188 | def trim_line2(line) 189 | head = nil 190 | line.scan(@scan_reg) do |tokens| 191 | tokens.each do |token| 192 | next if token.empty? 193 | head = token unless head 194 | if token == "%>\n" || token == "%>\r\n" 195 | yield('%>') 196 | if is_erb_stag?(head) 197 | yield(:cr) 198 | else 199 | yield("\n") 200 | end 201 | head = nil 202 | else 203 | yield(token) 204 | head = nil if token == "\n" 205 | end 206 | end 207 | end 208 | end 209 | 210 | def explicit_trim_line(line) 211 | line.scan(@scan_reg) do |tokens| 212 | tokens.each do |token| 213 | next if token.empty? 214 | if @stag.nil? && /[ \t]*<%-/ =~ token 215 | yield('<%') 216 | elsif @stag && (token == "-%>\n" || token == "-%>\r\n") 217 | yield('%>') 218 | yield(:cr) 219 | elsif @stag && token == '-%>' 220 | yield('%>') 221 | else 222 | yield(token) 223 | end 224 | end 225 | end 226 | end 227 | 228 | ERB_STAG = %w(<%= <%# <%) 229 | def is_erb_stag?(s) 230 | ERB_STAG.member?(s) 231 | end 232 | end 233 | 234 | Scanner.default_scanner = TrimScanner 235 | 236 | begin 237 | require 'strscan' 238 | rescue LoadError 239 | else 240 | class SimpleScanner < Scanner # :nodoc: 241 | def scan 242 | stag_reg = (stags == DEFAULT_STAGS) ? /(.*?)(<%[%=#]?|\z)/m : /(.*?)(#{stags.join('|')}|\z)/m 243 | etag_reg = (etags == DEFAULT_ETAGS) ? /(.*?)(%%?>|\z)/m : /(.*?)(#{etags.join('|')}|\z)/m 244 | scanner = StringScanner.new(@src) 245 | while ! scanner.eos? 246 | scanner.scan(@stag ? etag_reg : stag_reg) 247 | yield(scanner[1]) 248 | yield(scanner[2]) 249 | end 250 | end 251 | end 252 | Scanner.register_scanner(SimpleScanner, nil, false) 253 | 254 | class ExplicitScanner < Scanner # :nodoc: 255 | def scan 256 | stag_reg = /(.*?)(^[ \t]*<%-|<%-|#{stags.join('|')}|\z)/m 257 | etag_reg = /(.*?)(-%>|#{etags.join('|')}|\z)/m 258 | scanner = StringScanner.new(@src) 259 | while ! scanner.eos? 260 | scanner.scan(@stag ? etag_reg : stag_reg) 261 | yield(scanner[1]) 262 | 263 | elem = scanner[2] 264 | if /[ \t]*<%-/ =~ elem 265 | yield('<%') 266 | elsif elem == '-%>' 267 | yield('%>') 268 | yield(:cr) if scanner.scan(/(\r?\n|\z)/) 269 | else 270 | yield(elem) 271 | end 272 | end 273 | end 274 | end 275 | Scanner.register_scanner(ExplicitScanner, '-', false) 276 | end 277 | 278 | class Buffer # :nodoc: 279 | def initialize(compiler, enc=nil, frozen=nil) 280 | @compiler = compiler 281 | @line = [] 282 | @script = +'' 283 | @script << "#coding:#{enc}\n" if enc 284 | @script << "#frozen-string-literal:#{frozen}\n" unless frozen.nil? 285 | @compiler.pre_cmd.each do |x| 286 | push(x) 287 | end 288 | end 289 | attr_reader :script 290 | 291 | def push(cmd) 292 | @line << cmd 293 | end 294 | 295 | def cr 296 | @script << (@line.join('; ')) 297 | @line = [] 298 | @script << "\n" 299 | end 300 | 301 | def close 302 | return unless @line 303 | @compiler.post_cmd.each do |x| 304 | push(x) 305 | end 306 | @script << (@line.join('; ')) 307 | @line = nil 308 | end 309 | end 310 | 311 | def add_put_cmd(out, content) 312 | out.push("#{@put_cmd} #{content.dump}.freeze#{"\n" * content.count("\n")}") 313 | end 314 | 315 | def add_insert_cmd(out, content) 316 | out.push("#{@insert_cmd}((#{content}).to_s)") 317 | end 318 | 319 | # Compiles an ERB template into Ruby code. Returns an array of the code 320 | # and encoding like ["code", Encoding]. 321 | def compile(s) 322 | enc = s.encoding 323 | raise ArgumentError, "#{enc} is not ASCII compatible" if enc.dummy? 324 | s = s.b # see String#b 325 | magic_comment = detect_magic_comment(s, enc) 326 | out = Buffer.new(self, *magic_comment) 327 | 328 | self.content = +'' 329 | scanner = make_scanner(s) 330 | scanner.scan do |token| 331 | next if token.nil? 332 | next if token == '' 333 | if scanner.stag.nil? 334 | compile_stag(token, out, scanner) 335 | else 336 | compile_etag(token, out, scanner) 337 | end 338 | end 339 | add_put_cmd(out, content) if content.size > 0 340 | out.close 341 | return out.script, *magic_comment 342 | end 343 | 344 | def compile_stag(stag, out, scanner) 345 | case stag 346 | when PercentLine 347 | add_put_cmd(out, content) if content.size > 0 348 | self.content = +'' 349 | out.push(stag.to_s) 350 | out.cr 351 | when :cr 352 | out.cr 353 | when '<%', '<%=', '<%#' 354 | scanner.stag = stag 355 | add_put_cmd(out, content) if content.size > 0 356 | self.content = +'' 357 | when "\n" 358 | content << "\n" 359 | add_put_cmd(out, content) 360 | self.content = +'' 361 | when '<%%' 362 | content << '<%' 363 | else 364 | content << stag 365 | end 366 | end 367 | 368 | def compile_etag(etag, out, scanner) 369 | case etag 370 | when '%>' 371 | compile_content(scanner.stag, out) 372 | scanner.stag = nil 373 | self.content = +'' 374 | when '%%>' 375 | content << '%>' 376 | else 377 | content << etag 378 | end 379 | end 380 | 381 | def compile_content(stag, out) 382 | case stag 383 | when '<%' 384 | if content[-1] == ?\n 385 | content.chop! 386 | out.push(content) 387 | out.cr 388 | else 389 | out.push(content) 390 | end 391 | when '<%=' 392 | add_insert_cmd(out, content) 393 | when '<%#' 394 | out.push("\n" * content.count("\n")) # only adjust lineno 395 | end 396 | end 397 | 398 | def prepare_trim_mode(mode) # :nodoc: 399 | case mode 400 | when 1 401 | return [false, '>'] 402 | when 2 403 | return [false, '<>'] 404 | when 0, nil 405 | return [false, nil] 406 | when String 407 | unless mode.match?(/\A(%|-|>|<>){1,2}\z/) 408 | warn_invalid_trim_mode(mode, uplevel: 5) 409 | end 410 | 411 | perc = mode.include?('%') 412 | if mode.include?('-') 413 | return [perc, '-'] 414 | elsif mode.include?('<>') 415 | return [perc, '<>'] 416 | elsif mode.include?('>') 417 | return [perc, '>'] 418 | else 419 | [perc, nil] 420 | end 421 | else 422 | warn_invalid_trim_mode(mode, uplevel: 5) 423 | return [false, nil] 424 | end 425 | end 426 | 427 | def make_scanner(src) # :nodoc: 428 | Scanner.make_scanner(src, @trim_mode, @percent) 429 | end 430 | 431 | # Construct a new compiler using the trim_mode. See ERB::new for available 432 | # trim modes. 433 | def initialize(trim_mode) 434 | @percent, @trim_mode = prepare_trim_mode(trim_mode) 435 | @put_cmd = 'print' 436 | @insert_cmd = @put_cmd 437 | @pre_cmd = [] 438 | @post_cmd = [] 439 | end 440 | attr_reader :percent, :trim_mode 441 | 442 | # The command to handle text that ends with a newline 443 | attr_accessor :put_cmd 444 | 445 | # The command to handle text that is inserted prior to a newline 446 | attr_accessor :insert_cmd 447 | 448 | # An array of commands prepended to compiled code 449 | attr_accessor :pre_cmd 450 | 451 | # An array of commands appended to compiled code 452 | attr_accessor :post_cmd 453 | 454 | private 455 | 456 | # A buffered text in #compile 457 | attr_accessor :content 458 | 459 | def detect_magic_comment(s, enc = nil) 460 | re = @percent ? /\G(?:<%#(.*)%>|%#(.*)\n)/ : /\G<%#(.*)%>/ 461 | frozen = nil 462 | s.scan(re) do 463 | comment = $+ 464 | comment = $1 if comment[/-\*-\s*([^\s].*?)\s*-\*-$/] 465 | case comment 466 | when %r"coding\s*[=:]\s*([[:alnum:]\-_]+)" 467 | enc = Encoding.find($1.sub(/-(?:mac|dos|unix)/i, '')) 468 | when %r"frozen[-_]string[-_]literal\s*:\s*([[:alnum:]]+)" 469 | frozen = $1 470 | end 471 | end 472 | return enc, frozen 473 | end 474 | 475 | # :stopdoc: 476 | WARNING_UPLEVEL = Class.new { 477 | attr_reader :c 478 | def initialize from 479 | @c = caller.length - from.length 480 | end 481 | }.new(caller(0)).c 482 | private_constant :WARNING_UPLEVEL 483 | 484 | def warn_invalid_trim_mode(mode, uplevel:) 485 | warn "Invalid ERB trim mode: #{mode.inspect} (trim_mode: nil, 0, 1, 2, or String composed of '%' and/or '-', '>', '<>')", uplevel: uplevel + WARNING_UPLEVEL 486 | end 487 | end 488 | -------------------------------------------------------------------------------- /test/erb/test_erb.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: us-ascii -*- 2 | # frozen_string_literal: false 3 | require 'test/unit' 4 | require 'erb' 5 | require 'stringio' 6 | 7 | class TestERB < Test::Unit::TestCase 8 | class MyError < RuntimeError ; end 9 | 10 | def test_without_filename 11 | erb = ERB.new("<% raise ::TestERB::MyError %>") 12 | e = assert_raise(MyError) { 13 | erb.result 14 | } 15 | assert_match(/\A\(erb\):1\b/, e.backtrace[0]) 16 | end 17 | 18 | def test_with_filename 19 | erb = ERB.new("<% raise ::TestERB::MyError %>") 20 | erb.filename = "test filename" 21 | e = assert_raise(MyError) { 22 | erb.result 23 | } 24 | assert_match(/\Atest filename:1\b/, e.backtrace[0]) 25 | end 26 | 27 | def test_with_filename_lineno 28 | erb = ERB.new("<% raise ::TestERB::MyError %>") 29 | erb.filename = "test filename" 30 | erb.lineno = 100 31 | e = assert_raise(MyError) { 32 | erb.result 33 | } 34 | assert_match(/\Atest filename:101\b/, e.backtrace[0]) 35 | end 36 | 37 | def test_with_location 38 | erb = ERB.new("<% raise ::TestERB::MyError %>") 39 | erb.location = ["test filename", 200] 40 | e = assert_raise(MyError) { 41 | erb.result 42 | } 43 | assert_match(/\Atest filename:201\b/, e.backtrace[0]) 44 | end 45 | 46 | def test_html_escape 47 | assert_equal(" !"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", 48 | ERB::Util.html_escape(" !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")) 49 | 50 | assert_equal("", ERB::Util.html_escape("")) 51 | assert_equal("abc", ERB::Util.html_escape("abc")) 52 | assert_equal("<<", ERB::Util.html_escape("<\<")) 53 | assert_equal("'&"><", ERB::Util.html_escape("'&\"><")) 54 | 55 | assert_equal("", ERB::Util.html_escape(nil)) 56 | assert_equal("123", ERB::Util.html_escape(123)) 57 | 58 | assert_equal(65536+5, ERB::Util.html_escape("x"*65536 + "&").size) 59 | assert_equal(65536+5, ERB::Util.html_escape("&" + "x"*65536).size) 60 | end 61 | 62 | def test_html_escape_to_s 63 | object = Object.new 64 | def object.to_s 65 | "object" 66 | end 67 | assert_equal("object", ERB::Util.html_escape(object)) 68 | end 69 | 70 | def test_html_escape_extension 71 | assert_nil(ERB::Util.method(:html_escape).source_location) 72 | end if RUBY_ENGINE == 'ruby' 73 | 74 | def test_concurrent_default_binding 75 | # This test randomly fails with JRuby -- NameError: undefined local variable or method `template2' 76 | pend if RUBY_ENGINE == 'jruby' 77 | 78 | template1 = 'one <%= ERB.new(template2).result %>' 79 | 80 | eval 'template2 = "two"', TOPLEVEL_BINDING 81 | 82 | bug7046 = '[ruby-core:47638]' 83 | assert_equal("one two", ERB.new(template1).result, bug7046) 84 | end 85 | end 86 | 87 | class TestERBCore < Test::Unit::TestCase 88 | def setup 89 | @erb = ERB 90 | end 91 | 92 | def test_version 93 | assert_equal(String, @erb.version.class) 94 | end 95 | 96 | def test_core 97 | erb = @erb.new("hello") 98 | assert_equal("hello", erb.result) 99 | 100 | erb = @erb.new("hello", trim_mode: 0) 101 | assert_equal("hello", erb.result) 102 | 103 | erb = @erb.new("hello", trim_mode: 1) 104 | assert_equal("hello", erb.result) 105 | 106 | erb = @erb.new("hello", trim_mode: 2) 107 | assert_equal("hello", erb.result) 108 | 109 | src = < 113 | % n=0 114 | * <%= n %> 115 | <% end %> 116 | EOS 117 | 118 | ans = <') 150 | assert_equal(ans.chomp, erb.result) 151 | 152 | ans = <') 166 | assert_equal(ans, erb.result) 167 | 168 | ans = <') 188 | assert_equal(ans.chomp, erb.result) 189 | 190 | ans = <') 198 | assert_equal(ans, erb.result) 199 | end 200 | 201 | def test_trim_line1_with_carriage_return 202 | erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '>') 203 | assert_equal("line\r\n" * 3, erb.result) 204 | 205 | erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '%>') 206 | assert_equal("line\r\n" * 3, erb.result) 207 | end 208 | 209 | def test_trim_line2_with_carriage_return 210 | erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '<>') 211 | assert_equal("line\r\n" * 3, erb.result) 212 | 213 | erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '%<>') 214 | assert_equal("line\r\n" * 3, erb.result) 215 | end 216 | 217 | def test_explicit_trim_line_with_carriage_return 218 | erb = @erb.new("<%- 3.times do -%>\r\nline\r\n<%- end -%>\r\n", trim_mode: '-') 219 | assert_equal("line\r\n" * 3, erb.result) 220 | 221 | erb = @erb.new("<%- 3.times do -%>\r\nline\r\n<%- end -%>\r\n", trim_mode: '%-') 222 | assert_equal("line\r\n" * 3, erb.result) 223 | end 224 | 225 | def test_invalid_trim_mode 226 | pend if RUBY_ENGINE == 'truffleruby' 227 | 228 | assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do 229 | @erb.new("", trim_mode: 'abc-def') 230 | end 231 | 232 | assert_warning(/Invalid ERB trim mode/) do 233 | @erb.new("", trim_mode: 'abc-def') 234 | end 235 | 236 | assert_warning(/Invalid ERB trim mode/) do 237 | @erb.new("", trim_mode: '%<') 238 | end 239 | 240 | assert_warning(/Invalid ERB trim mode/) do 241 | @erb.new("", trim_mode: '%<>-') 242 | end 243 | 244 | assert_warning(/Invalid ERB trim mode/) do 245 | @erb.new("", trim_mode: 3) 246 | end 247 | end 248 | 249 | def test_run 250 | out = StringIO.new 251 | orig, $stdout = $stdout, out 252 | 253 | num = 3 254 | @erb.new('<%= num * 3 %>').run(binding) 255 | 256 | $stdout = orig 257 | out.rewind 258 | assert_equal('9', out.read) 259 | return unless num # to remove warning 260 | end 261 | 262 | class Foo; end 263 | 264 | def test_def_class 265 | erb = @erb.new('hello') 266 | cls = erb.def_class 267 | assert_equal(Object, cls.superclass) 268 | assert_respond_to(cls.new, 'result') 269 | cls = erb.def_class(Foo) 270 | assert_equal(Foo, cls.superclass) 271 | assert_respond_to(cls.new, 'result') 272 | cls = erb.def_class(Object, 'erb') 273 | assert_equal(Object, cls.superclass) 274 | assert_respond_to(cls.new, 'erb') 275 | end 276 | 277 | def test_percent 278 | src = < 281 | EOS 282 | assert_equal("1\n", ERB.new(src, trim_mode: '%').result(binding)) 283 | 284 | src = < 287 | EOS 288 | ans = "\n" 289 | assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding)) 290 | 291 | src = "<%\n%>" 292 | # ans = "\n" 293 | ans = "" 294 | assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding)) 295 | 296 | src = <<%= n%> 300 | EOS 301 | assert_equal("1\n", ERB.new(src, trim_mode: '%').result(binding)) 302 | 303 | src = < 307 | %% %%><%%<%= i%><% 308 | end%> 309 | %%% 310 | EOS 311 | ans = <<%0 314 | % %%><%1 315 | %% 316 | EOS 317 | assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding)) 318 | end 319 | 320 | def test_def_erb_method 321 | klass = Class.new 322 | klass.module_eval do 323 | extend ERB::DefMethod 324 | fname = File.join(File.dirname(File.expand_path(__FILE__)), 'hello.erb') 325 | def_erb_method('hello', fname) 326 | end 327 | assert_respond_to(klass.new, 'hello') 328 | 329 | assert_not_respond_to(klass.new, 'hello_world') 330 | erb = @erb.new('hello, world') 331 | klass.module_eval do 332 | def_erb_method('hello_world', erb) 333 | end 334 | assert_respond_to(klass.new, 'hello_world') 335 | end 336 | 337 | def test_def_method_without_filename 338 | klass = Class.new 339 | erb = ERB.new("<% raise ::TestERB::MyError %>") 340 | erb.filename = "test filename" 341 | assert_not_respond_to(klass.new, 'my_error') 342 | erb.def_method(klass, 'my_error') 343 | e = assert_raise(::TestERB::MyError) { 344 | klass.new.my_error 345 | } 346 | assert_match(/\A\(ERB\):1\b/, e.backtrace[0]) 347 | end 348 | 349 | def test_def_method_with_fname 350 | klass = Class.new 351 | erb = ERB.new("<% raise ::TestERB::MyError %>") 352 | erb.filename = "test filename" 353 | assert_not_respond_to(klass.new, 'my_error') 354 | erb.def_method(klass, 'my_error', 'test fname') 355 | e = assert_raise(::TestERB::MyError) { 356 | klass.new.my_error 357 | } 358 | assert_match(/\Atest fname:1\b/, e.backtrace[0]) 359 | end 360 | 361 | def test_def_module 362 | klass = Class.new 363 | klass.include ERB.new('<%= val %>').def_module('render(val)') 364 | assert_equal('1', klass.new.render(1)) 365 | end 366 | 367 | def test_escape 368 | src = < 370 | 2.%%> : <%="%%>"%> 371 | 3. 372 | % x = "foo" 373 | <%=x%> 374 | 4. 375 | %% print "foo" 376 | 5. 377 | %% <%="foo"%> 378 | 6.<%=" 379 | % print 'foo' 380 | "%> 381 | 7.<%=" 382 | %% print 'foo' 383 | "%> 384 | EOS 385 | ans = < : %> 388 | 3. 389 | foo 390 | 4. 391 | % print "foo" 392 | 5. 393 | % foo 394 | 6. 395 | % print 'foo' 396 | 397 | 7. 398 | %% print 'foo' 399 | 400 | EOS 401 | assert_equal(ans, ERB.new(src, trim_mode: '%').result) 402 | end 403 | 404 | def test_keep_lineno 405 | src = < 409 | % raise("lineno") 410 | EOS 411 | 412 | erb = ERB.new(src, trim_mode: '%') 413 | e = assert_raise(RuntimeError) { 414 | erb.result 415 | } 416 | assert_match(/\A\(erb\):4\b/, e.backtrace[0].to_s) 417 | 418 | src = < 420 | Hello,\s 421 | <% x = "World%%> 422 | "%> 423 | <%= x%> 424 | EOS 425 | 426 | ans = <Hello,\s 428 | World%> 429 | EOS 430 | assert_equal(ans, ERB.new(src, trim_mode: '>').result) 431 | 432 | ans = < 434 | Hello,\s 435 | 436 | World%> 437 | EOS 438 | assert_equal(ans, ERB.new(src, trim_mode: '<>').result) 439 | 440 | ans = < 442 | Hello,\s 443 | 444 | World%> 445 | 446 | EOS 447 | assert_equal(ans, ERB.new(src).result) 448 | 449 | src = < 453 | <%= x%> 454 | <% raise("lineno") %> 455 | EOS 456 | 457 | erb = ERB.new(src) 458 | e = assert_raise(RuntimeError) { 459 | erb.result 460 | } 461 | assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s) 462 | 463 | erb = ERB.new(src, trim_mode: '>') 464 | e = assert_raise(RuntimeError) { 465 | erb.result 466 | } 467 | assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s) 468 | 469 | erb = ERB.new(src, trim_mode: '<>') 470 | e = assert_raise(RuntimeError) { 471 | erb.result 472 | } 473 | assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s) 474 | 475 | src = < 479 | <%= x %><%- x = nil -%>\s 480 | <% raise("lineno") %> 481 | EOS 482 | 483 | erb = ERB.new(src, trim_mode: '-') 484 | e = assert_raise(RuntimeError) { 485 | erb.result 486 | } 487 | assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s) 488 | 489 | erb = ERB.new(src, trim_mode: '%-') 490 | e = assert_raise(RuntimeError) { 491 | erb.result 492 | } 493 | assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s) 494 | end 495 | 496 | def test_explicit 497 | src = < 499 | NotSkip <%- y = x -%> NotSkip 500 | <% x.each do |w| -%> 501 | <%- up = w.upcase -%> 502 | * <%= up %> 503 | <% end -%> 504 | <%- z = nil -%> NotSkip <%- z = x %> 505 | <%- z.each do |w| -%> 506 | <%- down = w.downcase -%> 507 | * <%= down %> 508 | <%- up = w.upcase -%> 509 | * <%= up %> 510 | <%- end -%> 511 | KeepNewLine <%- z = nil -%>\s 512 | EOS 513 | 514 | ans = <%", trim_mode: "%").result) 543 | end 544 | 545 | def test_token_extension 546 | extended_erb = Class.new(ERB) 547 | extended_erb.module_eval do 548 | def make_compiler(trim_mode) 549 | compiler = Class.new(ERB::Compiler) 550 | compiler.module_eval do 551 | def compile_stag(stag, out, scanner) 552 | case stag 553 | when '<%==' 554 | scanner.stag = stag 555 | add_put_cmd(out, content) if content.size > 0 556 | self.content = '' 557 | else 558 | super 559 | end 560 | end 561 | 562 | def compile_content(stag, out) 563 | case stag 564 | when '<%==' 565 | out.push("#{@insert_cmd}(::ERB::Util.html_escape(#{content}))") 566 | else 567 | super 568 | end 569 | end 570 | 571 | def make_scanner(src) 572 | scanner = Class.new(ERB::Compiler::SimpleScanner) 573 | scanner.module_eval do 574 | def stags 575 | ['<%=='] + super 576 | end 577 | end 578 | scanner.new(src, @trim_mode, @percent) 579 | end 580 | end 581 | compiler.new(trim_mode) 582 | end 583 | end 584 | 585 | src = <<~EOS 586 | <% tag = '<>' \%> 587 | <\%= tag \%> 588 | <\%== tag \%> 589 | EOS 590 | ans = <<~EOS 591 | 592 | <> 593 | <> 594 | EOS 595 | assert_equal(ans, extended_erb.new(src).result) 596 | end 597 | 598 | def test_frozen_string_literal 599 | bug12031 = '[ruby-core:73561] [Bug #12031]' 600 | e = @erb.new("<%#encoding: us-ascii%>a") 601 | e.src.sub!(/\A#(?:-\*-)?(.*)(?:-\*-)?/) { 602 | '# -*- \1; frozen-string-literal: true -*-' 603 | } 604 | assert_equal("a", e.result, bug12031) 605 | 606 | %w(false true).each do |flag| 607 | erb = @erb.new("<%#frozen-string-literal: #{flag}%><%=''.frozen?%>") 608 | assert_equal(flag, erb.result) 609 | end 610 | end 611 | 612 | def test_result_with_hash 613 | erb = @erb.new("<%= foo %>") 614 | assert_equal("1", erb.result_with_hash(foo: "1")) 615 | end 616 | 617 | def test_result_with_hash_does_not_use_caller_local_variables 618 | erb = @erb.new("<%= foo %>") 619 | foo = 1 620 | assert_raise(NameError) { erb.result_with_hash({}) } 621 | assert_equal("1", erb.result_with_hash(foo: foo)) 622 | end 623 | 624 | def test_result_with_hash_does_not_modify_caller_binding 625 | erb = @erb.new("<%= foo %>") 626 | erb.result_with_hash(foo: "1") 627 | assert_equal(false, binding.local_variable_defined?(:foo)) 628 | end 629 | 630 | def test_result_with_hash_does_not_modify_toplevel_binding 631 | erb = @erb.new("<%= foo %>") 632 | erb.result_with_hash(foo: "1") 633 | assert_equal(false, TOPLEVEL_BINDING.local_variable_defined?(:foo)) 634 | TOPLEVEL_BINDING.eval 'template2 = "two"' 635 | erb = @erb.new("<%= template2 %>") 636 | erb.result_with_hash(template2: "TWO") 637 | assert_equal "two", TOPLEVEL_BINDING.local_variable_get("template2") 638 | end 639 | 640 | # This depends on the behavior that #local_variable_set raises TypeError by invalid key. 641 | def test_result_with_hash_with_invalid_keys_raises_type_error 642 | erb = @erb.new("<%= 1 %>") 643 | assert_raise(TypeError) { erb.result_with_hash({ 1 => "1" }) } 644 | end 645 | 646 | # Bug#14243 647 | def test_half_working_comment_backward_compatibility 648 | assert_nothing_raised do 649 | @erb.new("<% # comment %>\n").result 650 | end 651 | end 652 | 653 | def test_prohibited_marshal_dump 654 | erb = ERB.new("") 655 | assert_raise(TypeError) {Marshal.dump(erb)} 656 | end 657 | 658 | def test_prohibited_marshal_load 659 | erb = ERB.allocate 660 | erb.instance_variable_set(:@src, "") 661 | erb.instance_variable_set(:@lineno, 1) 662 | erb.instance_variable_set(:@_init, true) 663 | erb = Marshal.load(Marshal.dump(erb)) 664 | assert_raise(ArgumentError) {erb.result} 665 | end 666 | 667 | def test_multi_line_comment_lineno 668 | erb = ERB.new(<<~EOS) 669 | <%= __LINE__ %> 670 | <%# 671 | %><%= __LINE__ %> 672 | EOS 673 | assert_equal <<~EOS, erb.result 674 | 1 675 | 3 676 | EOS 677 | end 678 | end 679 | 680 | class TestERBCoreWOStrScan < TestERBCore 681 | def setup 682 | @save_map = ERB::Compiler::Scanner.instance_variable_get('@scanner_map') 683 | map = {[nil, false]=>ERB::Compiler::SimpleScanner} 684 | ERB::Compiler::Scanner.instance_variable_set('@scanner_map', map) 685 | super 686 | end 687 | 688 | def teardown 689 | ERB::Compiler::Scanner.instance_variable_set('@scanner_map', @save_map) 690 | end 691 | end 692 | -------------------------------------------------------------------------------- /lib/erb.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: us-ascii -*- 2 | # frozen_string_literal: true 3 | # = ERB -- Ruby Templating 4 | # 5 | # Author:: Masatoshi SEKI 6 | # Documentation:: James Edward Gray II, Gavin Sinclair, and Simon Chiang 7 | # 8 | # See ERB for primary documentation and ERB::Util for a couple of utility 9 | # routines. 10 | # 11 | # Copyright (c) 1999-2000,2002,2003 Masatoshi SEKI 12 | # 13 | # You can redistribute it and/or modify it under the same terms as Ruby. 14 | 15 | # A NOTE ABOUT TERMS: 16 | # 17 | # Formerly: The documentation in this file used the term _template_ to refer to an ERB object. 18 | # 19 | # Now: The documentation in this file uses the term _template_ 20 | # to refer to the string input to ERB.new. 21 | # 22 | # The reason for the change: When documenting the ERB executable erb, 23 | # we need a term that refers to its string input; 24 | # _source_ is not a good idea, because ERB#src means something entirely different; 25 | # the two different sorts of sources would bring confusion. 26 | # 27 | # Therefore we use the term _template_ to refer to: 28 | # 29 | # - The string input to ERB.new 30 | # - The string input to executable erb. 31 | # 32 | 33 | require 'erb/version' 34 | require 'erb/compiler' 35 | require 'erb/def_method' 36 | require 'erb/util' 37 | 38 | # :markup: markdown 39 | # 40 | # Class **ERB** (the name stands for **Embedded Ruby**) 41 | # is an easy-to-use, but also very powerful, [template processor][template processor]. 42 | # 43 | # ## Usage 44 | # 45 | # Before you can use \ERB, you must first require it 46 | # (examples on this page assume that this has been done): 47 | # 48 | # ``` 49 | # require 'erb' 50 | # ``` 51 | # 52 | # ## In Brief 53 | # 54 | # Here's how \ERB works: 55 | # 56 | # - You can create a *template*: a plain-text string that includes specially formatted *tags*.. 57 | # - You can create an \ERB object to store the template. 58 | # - You can call instance method ERB#result to get the *result*. 59 | # 60 | # \ERB supports tags of three kinds: 61 | # 62 | # - [Expression tags][expression tags]: 63 | # each begins with `'<%='`, ends with `'%>'`; contains a Ruby expression; 64 | # in the result, the value of the expression replaces the entire tag: 65 | # 66 | # template = 'The magic word is <%= magic_word %>.' 67 | # erb = ERB.new(template) 68 | # magic_word = 'xyzzy' 69 | # erb.result(binding) # => "The magic word is xyzzy." 70 | # 71 | # The above call to #result passes argument `binding`, 72 | # which contains the binding of variable `magic_word` to its string value `'xyzzy'`. 73 | # 74 | # The below call to #result need not pass a binding, 75 | # because its expression `Date::DAYNAMES` is globally defined. 76 | # 77 | # ERB.new('Today is <%= Date::DAYNAMES[Date.today.wday] %>.').result # => "Today is Monday." 78 | # 79 | # - [Execution tags][execution tags]: 80 | # each begins with `'<%'`, ends with `'%>'`; contains Ruby code to be executed: 81 | # 82 | # template = '<% File.write("t.txt", "Some stuff.") %>' 83 | # ERB.new(template).result 84 | # File.read('t.txt') # => "Some stuff." 85 | # 86 | # - [Comment tags][comment tags]: 87 | # each begins with `'<%#'`, ends with `'%>'`; contains comment text; 88 | # in the result, the entire tag is omitted. 89 | # 90 | # template = 'Some stuff;<%# Note to self: figure out what the stuff is. %> more stuff.' 91 | # ERB.new(template).result # => "Some stuff; more stuff." 92 | # 93 | # ## Some Simple Examples 94 | # 95 | # Here's a simple example of \ERB in action: 96 | # 97 | # ``` 98 | # template = 'The time is <%= Time.now %>.' 99 | # erb = ERB.new(template) 100 | # erb.result 101 | # # => "The time is 2025-09-09 10:49:26 -0500." 102 | # ``` 103 | # 104 | # Details: 105 | # 106 | # 1. A plain-text string is assigned to variable `template`. 107 | # Its embedded [expression tag][expression tags] `'<%= Time.now %>'` includes a Ruby expression, `Time.now`. 108 | # 2. The string is put into a new \ERB object, and stored in variable `erb`. 109 | # 4. Method call `erb.result` generates a string that contains the run-time value of `Time.now`, 110 | # as computed at the time of the call. 111 | # 112 | # The 113 | # \ERB object may be re-used: 114 | # 115 | # ``` 116 | # erb.result 117 | # # => "The time is 2025-09-09 10:49:33 -0500." 118 | # ``` 119 | # 120 | # Another example: 121 | # 122 | # ``` 123 | # template = 'The magic word is <%= magic_word %>.' 124 | # erb = ERB.new(template) 125 | # magic_word = 'abracadabra' 126 | # erb.result(binding) 127 | # # => "The magic word is abracadabra." 128 | # ``` 129 | # 130 | # Details: 131 | # 132 | # 1. As before, a plain-text string is assigned to variable `template`. 133 | # Its embedded [expression tag][expression tags] `'<%= magic_word %>'` has a variable *name*, `magic_word`. 134 | # 2. The string is put into a new \ERB object, and stored in variable `erb`; 135 | # note that `magic_word` need not be defined before the \ERB object is created. 136 | # 3. `magic_word = 'abracadabra'` assigns a value to variable `magic_word`. 137 | # 4. Method call `erb.result(binding)` generates a string 138 | # that contains the *value* of `magic_word`. 139 | # 140 | # As before, the \ERB object may be re-used: 141 | # 142 | # ``` 143 | # magic_word = 'xyzzy' 144 | # erb.result(binding) 145 | # # => "The magic word is xyzzy." 146 | # ``` 147 | # 148 | # ## Bindings 149 | # 150 | # A call to method #result, which produces the formatted result string, 151 | # requires a [Binding object][binding object] as its argument. 152 | # 153 | # The binding object provides the bindings for expressions in [expression tags][expression tags]. 154 | # 155 | # There are three ways to provide the required binding: 156 | # 157 | # - [Default binding][default binding]. 158 | # - [Local binding][local binding]. 159 | # - [Augmented binding][augmented binding] 160 | # 161 | # ### Default Binding 162 | # 163 | # When you pass no `binding` argument to method #result, 164 | # the method uses its default binding: the one returned by method #new_toplevel. 165 | # This binding has the bindings defined by Ruby itself, 166 | # which are those for Ruby's constants and variables. 167 | # 168 | # That binding is sufficient for an expression tag that refers only to Ruby's constants and variables; 169 | # these expression tags refer only to Ruby's global constant `RUBY_COPYRIGHT` and global variable `$0`: 170 | # 171 | # ``` 172 | # template = <