添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • Released at: Dec 25, 2016 ( NEWS file)
  • Status (as of Dec 25, 2023): EOL, latest is 2.4.10
  • This document first published: Oct 14, 2019
  • Last change to this document: Dec 25, 2023
  • Highlights

  • Toplevel return
  • Unification of Fixnum and Bignum into Integer
  • Full Unicode support for String case operations
  • Warning module for fine-grained warnings output control
  • Comparable#clamp
  • Language

    Multiple assignment allowed in conditional expression

  • Reason: It is not what typically considered good style, but multiple assignment in conditions is now consistent with outside of condition behavior.
  • Discussion: Feature #10617
  • Documentation:
  • Code:
    points = []
    if (x, y = points.first) # in Ruby <2.4, will raise SyntaxError: multiple assignment in conditional
      p [x, y]
      
  • Note: Be aware that condition considered false if the whole right hand of assignment is false/nil, not the first assigned variable:
    points = [[]]
    if (x, y = points.first) # x is nil, y is nil, but condition is [], which is truthy
      p [x, y] # will print this
    

    Toplevel return

    return statement at the top level of any .rb file stops further execution of this file.

  • Reason: Useful for code that depends on platform, presence of third-party libraries and so on; return unless some_condition at the beginning of the file will allow writing the rest of the code in assumption that necessary condition satisfied.
  • Discussion: Feature #4840
  • Documentation:
  • Code:
    # Some nokogiri_patch.rb
    # In Ruby < 2.4:
    if defined? Nokogiri
      # the rest of the code should all be nested in the unless
    # Ruby 2.4
    return unless defined? Nokogiri
    # The rest of the code can be written at the top level, now.
    

    Refinements improvements

    NB: Some of those changes are language-level, some are just method changes, but all a related to refinements.

    Refinements are supported in Symbol#to_proc and send

  • Discussions: Feature #9451, Feature #11476
  • Code:
    module Tests
      refine Numeric do
        def normalize100
          clamp(0, 100)
    using Tests
    p [1, 700, 132].map(&:normalize100)
    # Ruby 2.3: undefined method `normalize100' for 1
    # Ruby 2.4: => [1, 100, 100]
    p 123.send(:normalize100)
    # Ruby 2.3: undefined method `normalize100' for 1
    # Ruby 2.4: => 100
      
  • Code:
    module Tests
      refine Enumerable do # in 2.3: wrong argument type Module (expected Class)
        def tally
          each_with_object(Hash.new(0)) { |el, counter| counter[el] += 1}
    using Tests
    p [1, 3, 1, 2, 1, 3].tally
    # => {1=>3, 3=>2, 2=>1}
    using NoRefinements
    p Module.used_modules # => [First, Second] -- note NoRefinements absence
        

    Note that order of modules in array returned is not guaranteed.

    Warning module

    New single-method module was introduced, meant to be overridden in order to control warnings issued by Ruby.

  • Reason:
  • Discussion: Feature #12299
  • Documentation: (introduced in 2.4, but documented in 2.5) Warning
  • Code:
    def Warning.warn(msg)
      puts ".warn called with: #{msg.inspect}"
    X = 1
    X = 2
    # Prints:
    #   .warn called with: "<location>: warning: already initialized constant X\n"
    #   .warn called with: "<location>: warning: previous definition of X was here\n"
      
  • Follow-ups:
  • Surprisingly as at may be, Kernel#warn haven’t been changed to call Warning.warn in 2.4, but it was fixed in 2.5:
    def Warning.warn(msg)
      puts ".warn called with: #{msg.inspect}"
    warn 'foo', 'bar'
    # Ruby 2.4 prints:
    #  foo
    #  bar
    # Ruby 2.5 prints:
    #  .warn called with: "foo\nbar\n"
          
  • In Ruby 2.7, new methods were added to Warning module allowing control over per-category warning suppression;
  • In Ruby 3.0, category support was improved.
  • 3.3: added new warning category: :performance.
  • Reason: Previously, there was no way to receive an unfrozen copy of the frozen object, including its singleton class: .dup returns unfrozen object, but doesn’t copies the singleton class, while .clone copies both (singleton class & frozen state).
  • Discussion: Feature #12300
  • Documentation: Object#clone
  • Code:
    h = {breed: 'Dog', name: 'Rex'}
    class << h
      def bark
        puts "Bark! Bark!"
    h.freeze
    h.bark    # "Bark! Bark!"
    d = h.dup
    d.frozen? # => false
    d.bark    # NoMethodError: undefined method `bark'
    h2 = h.clone(freeze: false)
    h2[:age] = 8
    h2.bark   # "Bark! Bark!"
      
  • Note: Surprisingly enough, unfrozen_object.clone(freeze: true) doesn’t make object frozen. See discussion.
  • Follow-up: since 3.0:
  • clone(freeze: true) works as expected;
  • freeze: argument is passed to initialize_clone so the object could properly freeze/unfreeze its internal data.
  • 123.clamp(0, 20)  # => 20
    -123.clamp(0, 20) # => 0
    18.clamp(0, 20)   # => 18
      
  • Follow-up: In Ruby 2.7, clamp also allows passing range argument, which, especially when combined with endless (2.6) and beginless (2.7) ranges, allows to use more powerful and idiomatic code:
    123.clamp(0..20)  # => 20
    123.clamp(..20)   # => 20
    -123.clamp(0..)   # => 0
    

    Fixnum and Bignum are unified into Integer

    Historically, Ruby had two subclasses of Integer: Fixnum for numbers that fit into machine word, and Bignum for larger numbers. Since Ruby 2.4, there is only one Integer; Fixnum and Bignum are defined as (deprecated) constants synonymous to it.

  • Reason: Fixnum/Bignum separation always been an implementation detail, which led to confusion and sudden bugs, now this detail is hidden by interpreter.
  • Discussion: Feature #12005
  • Documentation: Integer
  • Code:
    # Before 2.4.0
    10.class        # => Fixnum
    (10**100).class # => Bignum
    # 2.4+
    10.class        # => Integer
    (10**100).class # => Integer
    Fixnum
    # warning: constant ::Fixnum is deprecated
    # => Integer
      
  • Reason: The methods were present in Float and BigDecimal, but not in other numeric classes, which made it harder to write code uniformly processing numbers which may be integer/float/infinite.
  • Discussion: Feature #12039
  • Documentation: Numeric#infinite?, Numeric#finite?
  • Code:
    1.infinite?  # => nil
    1.finite?    # => true
      
  • Note: Notice that infinite? returns nil/-1/1 (always nil for integers), not true/false as most of other predicate methods. While unusual, it is convenient for checking both for infinity and its sign (+Infinity/-Infinity), and can be treated effectively as true/false in boolean context.
  • Integer#digits

    Returns an array of digits of the number.

  • Reason: Useful for calculating checksums.
  • Discussion: Feature #12447
  • Documentation: Integer.html#digits
  • Code:
    12345.digits      # => [5, 4, 3, 2, 1] -- digits are returned in lowest-position-first order
    0b11010.digits(2) # => [0, 1, 0, 1, 1] -- optional base can be passed
    

    ndigits optional argument for rounding methods

    Rounding methods of numerics (ceil/floor etc.) now accept an optional argument to specify how many digits to truncate to. If argument is positive, it means decimal digits, and if it is negative, means tens (the same behavior #round had since Ruby 1.9).

  • Discussion: Feature #12245
  • Documentation: Numeric#ceil, Numeric#floor, Numeric#truncate (in fact, Integer and Float classes are affected, because Rational had the same option long ago).
  • Code:
    123.4567.ceil(2)  # => 123.46
    123.4567.ceil(0)  # => 124
    123.4567.ceil(-1) # => 130
    

    half: option for #round method

    For numbers that are exact half, there are several options provided how to round them: up, down, or to the nearest even number. The default behavior kept unchanged (always up).

  • Discussion: Bug #12958 (discussion of rounding behavior), Feature #12953 (additional rounding options)
  • Documentation: (feature introduced in 2.4, but comprehensive docs were written in 2.5) Integer#round, Float#round, Rational#round
  • Code:
    2.5.round # => 3
    2.5.round(half: :down) # => 2
    2.5.round(half: :even) # => 2
    3.5.round(half: :even) # => 4
    25.round(-1, half: :down) # => 20
    (13/2r).round(half: :down) # => 6
    

    Unicode case conversions

    All case-conversion methods for String and Symbol support full Unicode since 2.4.

  • Discussion: Feature #10085
  • Documentation: String#downcase, String#upcase, String#capitalize, String#swapcase, Symbol#downcase, Symbol#upcase, Symbol#capitalize, Symbol#swapcase,
  • Code:
    'Мамо, ДИВИСЬ, Unicode'.downcase          # => "мамо, дивись, unicode"
    'Мамо, ДИВИСЬ, Unicode'.downcase(:ascii)  # => "Мамо, ДИВИСЬ, unicode"  -- ASCII-only processing (old behavior)
    'Straße'.downcase                         # => "straße"
    'Straße'.downcase(:fold)                  # => "strasse"  -- Unicode case folding
    'TURKIC'.downcase                         # => "turkic"
    'TURKIC'.downcase(:turkic)                # => "turkıc" -- Turkic-specific "dotless i" conversion
    

    String.new(capacity: size)

    When string is created for usage as a mutable buffer for some large textual data, now expected size could be specified, thus optimizing memory allocations.

  • Reason: Ruby’s mutable strings, when used for sequential building of some large text, cause constant reallocations of bigger and bigger memory buffer. By specifying expected capacity beforehand, one can avoid this reallocations.
  • Discussion: Feature #12024
  • Documentation: String.new
  • Code:
    s = String.new(capacity: 10_000_000)
    # => "" -- it is still just an empty string, but internal buffer is already allocated large
      
  • Note: Be careful about subtle difference in encoding, when constructing an empty string:
    # Without source provided, default encoding is ASCII
    String.new(capacity: 10_000_000).encoding # => #<Encoding:ASCII-8BIT>
    # When explicitly constructed from empty string, has this string's encoding (defautl to UTF-8 for
    # string literals)
    String.new('', capacity: 10_000_000).encoding # => #<Encoding:UTF-8>
    

    #casecmp?

    In addition to long-existing case-insensitive comparison method String#casecmp(other) (returning -1, 0, 1 like <=>), new boolean String#casecmp? and Symbol#casecmp? were added.

  • Discussion: Feature #
  • Documentation: String#casecmp?, Symbol#casecmp?
  • Code:
    'test'.casecmp?('Test') # => true
    'test'.casecmp?('Tset') # => false
    'test'.casecmp?(:Test)  # TypeError: no implicit conversion of Symbol into String
      
  • Follow-up: In Ruby 2.5, behavior on incompatible types was changed to return nil, like == does:
    'test' == :Test # => nil
    'test'.casecmp?(:Test) # => nil
      
  • Notes: It was proposed (in the discussion above), but never implemented to allow passing options to casecmp?, making it more precise by specifying locales. Currently, some local characters can produce unexpected results:
    'ı'.casecmp?('I') # => false, though it is Turkish small dotless "I"
    # ...but...
    'ı'.upcase == 'I' # => true
      
  • Documentation: String#concat, String#prepend
  • Code:
    "Hello, ".concat('Judy', ', ', 'John', ' and ', 'Paul')
    # => "Hello, Judy, John and Paul"
    'file.mp3'.prepend('dir1/', 'dir2/')
    # => "dir1/dir2/file.mp3"
      
  • Follow-up: In Ruby 3.1, a second argument was added to unpack1 (and unpack) allowing to unpack values from the middle of the string.
  • #match? method

    New boolean methods for checking if some pattern matches some string/symbol.

  • Reason: In the (frequent) situation when only “matches or not” is important, boolean match? is more readable; also, it is more effective because doesn’t set global variables (in case of Regexp#match?) and doesn’t construct MatchData.
  • Discussion: Feature #8110, Feature #12898
  • Documentation: Regexp#match?, String#match?, Symbol#match?
  • Code:
    # before 2.4
    if username =~ /^Admin/
    # Ruby 2.4:
    if username.match?(/^Admin/)
    # Also supports second parameter: position to search matches from:
    if username.match?(/:admin/, 3)
    

    MatchData: better support for named captures

    MatchData#named_captures returns the hash of {capture_name => captured string}; MatchData#values_at supports named captures.

  • Discussion: Feature #11999, Feature #9179
  • Documentation: MatchData#named_captures, MatchData#values_at
  • Code:
    m = 'Serhii Zhadan'.match(/^((?<first>.+?) (?<last>.+?))$/)
    # => #<MatchData "Serhii Zhadan" first:"Serhii" last:"Zhadan">
    m.named_captures
    # => {"first"=>"Serhii", "last"=>"Zhadan"}
    m.values_at(:first, :last) # symbols are supported, too
    # => ["Serhii", "Zhadan"]
    m.values_at(0, :first, 2) # as well as a mix of named and numbered
    # => ["Serhii Zhadan", "Serhii", "Zhadan"]
    # Example of usage:
    ('a'..'k').chunk.with_index { |e, i| (i % 3).zero? }.to_a
    # => [
    #  [true, ["a"]],
    #  [false, ["b", "c"]],
    #  [true, ["d"]],
    #  [false, ["e", "f"]], ...
    (1..5).sum { |x| x ** 2} # => 55
    # Unlike reduce(:+), initial value is implicitly 0, so...
    [].reduce(:+)  # => nil
    [].sum         # => 0
    ('a'..'f').reduce(:+) # => "abcdef"
    ('a'..'f').sum        # TypeError: String can't be coerced into Integer
    ('a'..'f').sum('')    # => "abcdef"
      
  • Note: Separate implementation of Array#sum is provided for efficiency. Important thing to note is it doesn’t rely on Array#each method:
    class MyAry < Array
      def each(&block)
        super { |val| yield val ** 2 }
    MyAry.new([1, 2, 3, 4, 5]).sum # => 15, not affected by reimplmented #each
    

    #uniq

    #uniq method, previously present only in Array, now available for Enumerable and Enumerator::Lazy

  • Discussion: Feature #11090
  • Documentation: Enumerable#iniq, Enumerator::Lazy#uniq
  • Code:
    {a: 1, b: 2, c: 1, d: 2, e: 1}.uniq { |k, v| v }
    #  => [[:a, 1], [:b, 2]]
    File.open('very_large_log.log').each_line.lazy.uniq { |ln| ln.scan(/Date: (\S+):/) }.take(10)
    # => first 10 of first-line-of-day
      
  • Discussion: Feature #12172
  • Documentation: Array#max, Array#min
  • Note: Beware that custom reimplementation of Enumerable#max and #min are now ignored for arrays; and that Array’s implementation doesn’t use #each method.
  • Array#concat takes multiple arguments

  • Discussion: Feature #12333
  • Documentation: Array#concat
  • Code:
    a = [1, 2]
    a.concat([3, 4], [5, 6]) # => [1, 2, 3, 4, 5, 6]
    a # => [1, 2, 3, 4, 5, 6]
    

    Array#pack(buffer:)

    When provided with optional buffer: keyword argument, Array#pack uses it as a receiver of data.

  • Reason: a) pre-allocate the memory for big packed data and b) use the same buffer as a target for several chunks of data
  • Discussion: Feature #12754
  • Documentation: Array#pack
  • Code:
    # Old way
    [82, 117, 98, 121].pack('C*')        # => "Ruby"
    [32, 105, 115, 32, 99, 111, 111, 108, 33].pack('C*') # => " is cool!"
    # New way
    buffer = String.new(capacity: 30)
    [82, 117, 98, 121].pack('C*', buffer: buffer)
    # => "Ruby"
    buffer # => "Ruby"
    [32, 105, 115, 32, 99, 111, 111, 108, 33].pack('@4C*', buffer: buffer)
    # => "Ruby is cool!"
    buffer # => "Ruby is cool!"
    # Note that if the buffer already has content, the unpacked data is appended to it:
    [32, 73, 115, 32, 105, 116, 63].pack('C*', buffer: buffer)
    # => "Ruby is cool! Is it?"
    # It can be rewritten with explicit offset 0 directive:
    [32, 73, 115, 32, 105, 116, 63].pack('@0C*', buffer: buffer)
    # => " Is it?"
      
  • Documentation: Hash#compact, Hash#compact!
  • Code:
    data = {name: 'John', age: 34, occupation: nil}
    data.compact # => {:name=>"John", :age=>34}
    data # => {:name=>"John", :age=>34, :occupation=>nil} -- was not affected
    data.compact! # => {:name=>"John", :age=>34}
    data # => {:name=>"John", :age=>34}
    data.compact! # => nil -- if there were nothing to remove
      
  • Note: Notice the last example: when destructive version haven’t changed a hash, it returns nil instead of hash itself. It is consistent with behavior of other destructive methods, and allows writing code like this:
    if data.compact!
      log.info 'Cleaned up the data'
    

    Hash#transform_values and #transform_values!

    Accepts block to transform each value of the hash.

  • Reason: New method is much easier to write and read, and more effective than
      hash.map { |key, val| [key, do_something(val)] }.to_h
      
  • Discussion: Feature #12512
  • Documentation: Hash#transform_values, Hash#transform_values!
  • Code:
    h = {x: '10', y: '12', z: '54'}
    h.transform_values(&:to_i) # => {:x=>10, :y=>12, :z=>54}
    h.transform_values!(&:to_i) # => {:x=>10, :y=>12, :z=>54}
    h # => {:x=>10, :y=>12, :z=>54}
    h.transform_values!(&:to_i) # => {:x=>10, :y=>12, :z=>54} -- unlike #compact!, always returns self
    # Without block, returns Enumerator
    h.transform_values
    # => #<Enumerator: {:x=>"10", :y=>"12", :z=>"54"}:transform_values>
    # Can be useful this way:
    h = {manager: 'Jane', reporter: 'John', qa: 'Jane', developer: 'Abraham'}
    h.transform_values.with_index(1) { |v, i| "#{i}: #{v}" }
    # => {:manager=>"1: Jane", :reporter=>"2: John", :qa=>"3: Jane", :developer=>"4: Abraham"}
      
  • Follow-up: Ruby 2.5 also added #transform_keys and #transform_keys!
  • Filesystem and IO

    chomp: option for string splitting

    In all contexts where input is split into lines, or received line-by-line, new optional keyword argument chomp: true was added to remove (chomp) line-endings.

  • Reason: Before this change, all line-by-line operations should’ve included chomp as a separate operations when line ending is not needed, which turned out to be most o the cases.
  • Discussion: Feature #12553
  • Documentation (feature introduced in 2.4, but comprehensive docs were written in 2.5-2.6):
  • String#each_line, String#lines,
  • IO#gets, IO#readline, IO#readlines, IO.foreach, IO.readlines,
  • Standard library: methods of the class affected, but no documentation for the change: StringIO#gets, StringIO#each_line, StringIO#readlines
  • Code:
    # The effect is the same with String, IO and StringIO, so we are demonstrating just one example:
    require 'stringio'
    io = StringIO.new("foo\nbar\nbaz\n")
    io.gets              # => "foo\n"
    io.gets(chomp: true) # => "bar"
      
  • Notes: What is chomped (and what is lines split on) is controlled by $/ global variable (dubbed $RS or $INPUT_RECORD_SEPARATOR by English module). While quite esoteric by today’s standards, it could be really useful for one-off scripts that work with specific data:
    records = <<~DATA
    First line
    Second line
    Third line
    $/ = "\n$$$\n"
    records.each_line(chomp: true).to_a # => ["First line", "Second line", "Third line"]
    # The same effect, though, can be achieve with normal `separator` method argument:
    records.each_line("\n$$$\n", chomp: true).to_a # => ["First line", "Second line", "Third line"]
    

    #empty? method for filesystem objects

    New method empty? was introduced into several classes to check if the file/directory is empty.

  • Discussion: Feature #10121 (Dir), Feature #9969 (File), Feature #12596 (Pathname)
  • Documentation: Dir#empty?, File#empty?, (stdlib) Pathname#empty?
  • Code:
    Dir.empty?('emptydir')    # => true
    Dir.empty?('nonemptydir') # => false
    Dir.empty?('nonexistent') # Errno::ENOENT (No such file or directory @ rb_dir_s_empty_p - nonexistent)
    Dir.empty?('file')        # => false
    File.empty?('emptyfile')    # => true
    File.empty?('nonemptyfile') # => false
    File.empty?('nonexistent')  # => false -- unlike Dir.empty?
    File.empty?('dir')          # => false
    require 'pathname'
    Pathname('emptydir').empty?    # => true
    Pathname('emptyfile').empty?   # => true
    Pathname('nonexistent').empty? # => false
    Pathname('nonempty').empty?    # => false
    

    Thread#report_on_exception and Thread.report_on_exception

    Global and thread-local boolean flag to set what should the thread do when ended with exception: die silently (default, old behavior) or print the exception and backtrace to $stderr.

  • Reason: Threads silently dying without any indication could be a lot of confusion, and before this feature top-level exception reporting for each thread should’ve been implemented manually.
  • Discussion: Feature #6647
  • Documentation: Thread.report_on_exception, Thread.report_on_exception=, Thread#report_on_exception, Thread#report_on_exception
  • Code:
    Thread.new { puts 1 / 0 }
    sleep(1)
    # => nothing happens, thread is dead
    Thread.report_on_exception = true
    Thread.new { puts 1 / 0 }
    sleep(1)
    # #<Thread:0x0055b070475fb0> terminated with exception:
    # in `/': divided by 0 (ZeroDivisionError)
    # Or instance-level method:
    t = Thread.new { sleep(1); puts 1 / 0 }
    t.report_on_exception = false # silence it again
    sleep(1)
    # => Thread dies in a sad silence.
      
  • Follow-up: Since 2.5, Thread.report_on_exception is true by default.
  • TracePoint#callee_id

    Returns an actual name of the method being called, even if aliased.

  • Discussion: Feature #12747
  • Documentation: TracePoint#callee_id
  • Code:
    tp = TracePoint.new(:call) { |point| p [point.method_id, point.callee_id]}
    def real
    alias aliased real
    tp.enable { aliased } # prints method id and callee id: [:real, :aliased]
      
  • Set: #compare_by_identity and #compare_by_identity? methods added, behaving the same way as (existing since 1.9) Hash#compare_by_identity: only elements being the same object (same #object_id) are considered same set element. Discussion: Feature #12210
  • CSV.new: Add a liberal_parsing option, allowing to (try to) parse not-completely-valid CSV. Discussion: Feature #11839
  • Binding#irb start a REPL session like binding.pry.Follow-up: since Ruby 2.5, require 'irb' is not necessary for the feature to work, it is done automatically.
  • Logger.new adds keyword arguments level:, progname:, datetime_format:, formatter:, shift_period_suffix:. The latter allows specifying suffix for filenames on log rotation. Discussions: Feature #12224 (keyword args), Feature #10772 (shift_period_suffix).
  • Net::HTTP.post shortcut method. Discussion: Feature #12375
  • Net::FTP:
  • Support TLS.
  • Support hash style options for Net::FTP.new. While not reflected in the docs, “old” way (as it was before 2.4, with separate args for user, password etc.) still works for backwards compatibility.
  • Net::FTP#status: optional argument pathname (STAT path “is analogous to the “list” command, except that data shall be transferred over the control connection”). Discussion: Feature #12965
  • OptionParser (optparse): OptionParser#parse! and similar methods add into: option to parse, greatly simplifying trivial case of “parse into hash”. Discussion: Feature #11191
    require 'optparse'
    opts = OptionParser.new do |o|
      o.on '-p', '--port=PORT', 'port', Integer
      o.on '-v', '--verbose'
    result = {}
    opts.parse!(%w[-p 8080 -v], into: result)
    p result # => {:port=>8080, :verbose=>true}
      
  • Readline: ::quoting_detection_proc and quoting_detection_proc= to specify callable object (Proc or anything responding to #call), customizing the decision “if in this line, character in that position is quoted or not”. It is standard functionality of GNU readline which was not previously exposed by Ruby wrapper. Discussion: Feature #12659
  • Libraries promoted to default/bundled gems

    stdgems.org project has a nice explanations of default and bundled gems concepts, as well as a list of currently gemified libraries.

    “For the rest of us”:

  • default means libraries development extracted into separate GitHub repositories, and they are just packaged with main Ruby before release. It means you can do issue/PR to any of them independently, without going through more tough development process of the core Ruby;
  • bundled means libraries development also extracted, and they are not packaged with Ruby distribution, just automatically installed with it.
  • Libraries that became default in 2.4:

  • openssl
  • webrick
  • Libraries that became bundled in 2.4:

  • xmlrpc
  • Follow-up:

  • 16 more libraries gemified in 2.5;
  • 14 more libraries gemified in 2.6;
  • 16 more libraries gemified in 2.7, and 6 just dropped from the standard library;
  • 34 (!) more libraries gemified in 3.0, and 3 more just dropped from the standard library (including xmlrpc and webrick).
  •