The letter A styled as Alchemists logo. lchemists
Published December 1, 2023 Updated January 22, 2024
Cover
Interactive Ruby (IRB)

IRB has seen several enhancements over the years and I want to highlight those that will improve your workflow in case these enhancements are not part of your workflow already. There’s a lot of ground to cover. Buckle up!

Quick Start

In case you don’t have time to read the entirety of this article and want to cut to the chase, you can pilfer the irbrc file from my Dotfiles project and you’ll be in business. 🚀

The rest of this article will delve into how to configure, customize, and maximize your IRB experience using my irbrc file as a foundation for further discussion. Read on to learn more.

Setup

To get started, ensure you are using IRB 1.10.0 or higher. Example:

gem list irb
# irb (default: 1.10.0)

💡 Since IRB is a default gem, you can enforce the latest version by updating your Default Gems (as shown in the output above).

Next, you’ll want to create an .irbrc file which, for the uninitiated, ends up in the root of your $HOME directory. Unfortunately, placing all of your Dotfiles in your home directory is messy and difficult to maintain. A saner and more manageable solution is to use the XDG specification and create your configuration here: $HOME/.config/irb/irbrc. To use a XDG configuration, add the following to your Bash profile (or whatever shell you are using) as follows:

export XDG_CONFIG_HOME="$HOME/.config"

Ensure you run exec $SHELL after the above has been applied so your shell picks up the changes and then create a $HOME/.config/irb/irbrc via your favorite editor or, again, paste in what you want from my irbrc configuration to experiment further.

Configuration

With the latest IRB version installed, we can start configuring IRB. The following sections will use my irbrc configuration as a blueprint so you can customize further if desired.

Prompt

The default IRB prompt is rather boring:

irb
# irb(main):001:0>

We can do better! In my irbrc, you’ll notice I use a CK module — short for Console Kit — which defines a .prompt method for rendering context aware prompts. Here’s the relevant code (truncated for brevity):

module CK
  def self.prompt
    if defined? Hanami
      details Hanami::VERSION, Hanami.app.name.delete_suffix("::App"), Hanami.env
    elsif defined? Rails
      details Rails.version, Rails.application.class.module_parent_name, Rails.env
    else version_with_optional_project
    end
  end

  def self.details framework_version, application_name, environment
    [RUBY_VERSION, framework_version, application_name.downcase, color_for(environment)].join "|"
  end

  def self.color_for environment
    environment.to_sym == :production ? "\e[31m#{environment}\e[0m" : "\e[32m#{environment}\e[0m"
  end

  def self.version_with_optional_project
    File.basename(`git rev-parse --show-toplevel 2> /dev/null`.strip)
        .downcase
        .then { |project| [RUBY_VERSION, project].reject(&:empty?).join "|" }
  end

  private_class_method :details, :color_for, :version_with_optional_project
end

Notice that the .prompt method checks for Hanami, Rails, and then falls back to Ruby if no framework is found. This allows me to have context aware prompts. Examples:

Hanami

Cover

Rails

Cover

Ruby (with Git)

Cover

Ruby (without Git)

Cover

The private methods, which aid the .prompt method, are described as follows:

  • .details: Displays the Ruby version, framework version, current application name, and environment. All are separated by a pipe (i.e. |).

  • .color_for: Ensures the Production environment is always red while all other environments display as green so you’ll have a visual cue if working in a protected or safe environment.

  • .version_with_optional_project: Dynamically renders project name if initialized as a Git repository. Otherwise, only the current Ruby version is shown when using IRB in a basic directory.

Teaching IRB to provide project specific details, when working on multiple projects at once, allows me to have relevant information when context switching.

Auto Completion

You can use Reline::Face.config via the Reline gem to configure IRB with a color scheme you enjoy instead of using the default. Here’s what I’m using which is green highlights against a black background with a white scrollbar:

require "irb/completion"

Reline::Face.config :completion_dialog do |config|
  config.define :default, foreground: :white, background: :black
  config.define :enhanced, foreground: :black, background: :green
  config.define :scrollbar, foreground: :bright_white, background: :black
end

Example:

Cover

For additional details, see the Reline Face documentation to customize further.

In addition to configuring your IRB auto completion look and feel, you can add type completion by adding the following line to your configuration:

IRB.conf[:COMPLETOR] = :type

Example:

Cover

💡 You’ll want to install the ReplTypeCompletor gem which will leverage RBS type signatures too or you’ll get a warning the console.

Return Value Omission

In some situations, it can be nice to ignore the return value from an expression. This can be done by ending the expression with a semicolon. Example:

# Will fill your screen with the result.
result = "demo " * 100_000

# Will ignore the return value.
result = "demo " * 100_000;

Aliases

IRB supports aliases for commands you use the most. As per my irbrc configuration, I use the following:

IRB.conf[:COMMAND_ALIASES]
   .merge! b: :backtrace,
           c: :continue,
           e: :edit,
           h: :show_cmds,
           i: :info,
           l: :ls,
           n: :next,
           m: :measure,
           s: :step,
           w: :whereami

This allows me to use single letters for reduced typing. These aliases use a similar mapping to the aliases provided by the Debug gem so there is a natural flow when toggling between IRB and Debug when inspecting/debugging Ruby code.

💡 Your custom aliases will show up when showing help documentation (i.e. show_cmds).

Pager

Pagination is enabled, by default, but can be disabled by using irb --no-pager or adding the following to your irbrc configuration:

IRB.conf[:USE_PAGER] = false

History

Enabling a long history allows you to have more information you can reuse or capture for documentation purposes. Here’s what I have in my irbrc file:

IRB.conf[:EVAL_HISTORY] = 1_000
IRB.conf[:HISTORY_FILE] = "#{Dir.home}/.cache/irb/history.log"

Setting EVAL_HISTORY allows for 1,000 entries in your history file before being truncated. Setting HISTORY_FILE to a path of your choice allows you to not clutter your $HOME directory. In my case, an XDG cache path is used to store all of this information. Highly recommend since the XDG specification provides a sane — and clean — way to organize your Dotfiles.

Once you’ve applied your history configuration, you can use the up arrow to cycle through your history or type the history command to page through your history.

Inspection

To inspect your current IRB configuration with or without the Amazing Print gem, run the following within your IRB console:

# Without Amazing Print
IRB.conf.each { |key, value| puts "#{key}=#{value}" }

# With Amazing Print
require "amazing_print"
ap IRB.conf

Inspection

Should you ever need to know the version and configuration you are using, you can jump into an IRB console and use irb_info to print details. For example, at the time of this writing, here’s what I’m using:

Cover

Debugging

There are two ways you can debug by using an IRB or Debug break point:

# IRB
binding.irb

# Debug
binding.break

The advantage of using IRB is there is nothing to require. You can throw a break point in any file, run the code, and immediately start inspecting at the break point. With the Debug gem, you’ll need to require the gem first. All of my projects use the Debug gem so this isn’t a problem but can get cumbersome when working on a project that doesn’t require the Debug gem so you can use IRB’s binding.irb break point as a fallback in those situations. IRB’s debugger isn’t as feature rich as the Debug gem so definitely recommend requiring the Debug gem for a better experience.

IRB has a close integration with the Debug gem which means that if you start with a binding.irb breakpoint — and the Debug gem is required — you can switch to using Debug by typing debug in your console. Otherwise, you’ll end up with the session being abruptly ended.

Definitely check out IRB's documentation for more information.

Source Inspection

IRB provides several commands for importing code into your IRB session. Here’s the breakdown:

  • source: Loads given file into current session and displays each line.

  • irb_load: Same as the above but works like Kernel#load.

  • irb_require: Same as the above but works like Kernel#require.

Given the above, let’s say we have the following implementation (i.e. demo.rb) relative to the current directory in which you are using IRB:

# frozen_string_literal: true

module Demo
  def self.say = puts "HI"
end

We can launch an IRB console and see slightly different behavior:

Source

[3.2.2]> source "demo.rb"
[3.2.2]> # frozen_string_literal: true
=> nil
[3.2.2]>
[3.2.2]> module Demo
[3.2.2]|     def self.say = puts "HI"
[3.2.2]|   end
=> :say
[3.2.2]>
=> nil

irb_load

[3.2.2]> irb_load "#{Dir.pwd}/demo.rb"
[3.2.2]> # frozen_string_literal: true
=> nil
[3.2.2]>
[3.2.2]> module Demo
[3.2.2]|     def self.say = puts "HI"
[3.2.2]|   end
=> :say
[3.2.2]>
=> nil

irb_require

[3.2.2]> irb_require "./demo.rb"
=> true

You’ll notice that each command requires a different path syntax for each file being loaded and only source and irb_load print out each line of the file as the line is being evaluated. In all cases, once the file is parsed, you can immediately make use of the implementation by messaging Demo.say in your console to get expected output.

Show Source

The show_source command helps you view the source code of a method you are debugging. Additionally, you can use the -s option to move up a level and view the superclass source code. Using -ss allows you move up to the grandparent and so forth.

Measurements

IRB makes measuring code convenient via the measure command. Here’s an example of performing a time measurement (default) within an IRB console:

measure
# TIME is added.

(1..1_000_000).sum
# processing time: 0.000102s
# => 500000500000

measure :off
# => nil

With the above, I measured one operation but you could choose to leave measure on until you are done performing multiple measurements.

You can also use Stackprof by passing :stackprof as an argument to measure but I’ll leave that up to you to experiment with further.

By default, IRB uses the MEASURE_PROC key to store measurement operations. You can inspect the configuration by running the following in your IRB console (truncated for brevity):

IRB.conf[:MEASURE_PROC]

# {
#   TIME: #<Proc:0x000000010b5c5ba0 irb/init.rb:118>,
#   STACKPROF: #<Proc:0x000000010b5c5b78 irb/init.rb:128>
# }

Each measurement operation takes five positional parameters:

  1. context (IRB::Context): Captures the current IRB session.

  2. code (String): The code snippet to measure.

  3. line_number (Integer): The IRB console line number which increments with each new IRB entry.

  4. *arguments: Additional arguments which are passed on to your measure operation.

  5. block (Proc): The block of code you want to measure in case typing each line in IRB is too cumbersome.

Given the above, here’s a simple example of measuring Garbage Collection stats using the Refinements gem to calculate the diff between each measurement. You can copy and paste the following in your IRB console:

require "refinements"

using Refinements::Hash

IRB.conf[:MEASURE_PROC][:GC] = lambda do |_context, _code, _line_number, *arguments, &block|
  before = GC.stat
  block.call
  after = GC.stat

  puts "Garbage Collection Diff:"
  puts before.diff(after)
end

Then, to use the above while still in your IRB console, you can run as follows:

measure :gc
# GC is added.

(1..1_000_000).sum
# Garbage Collection Diff:
# {:heap_live_slots=>[153300, 153370], :heap_free_slots=>[736819, 736749], :total_allocated_objects=>[986171, 986241], :malloc_increase_bytes=>[528848, 532832], :oldmalloc_increase_bytes=>[10737264, 10741248]}
# => 500000500000

Again, the above is a simple example but there is plenty of potential for adding multiple custom measurements that meet your needs.

Extensions

There is talk of supporting extensions and helpers but none of this is ready at the moment. Will be interesting to see how this evolves since having additional support would improve IRB customization and maintenance by not having to shove everything into your irbrc file.

Easter Egg

This is silly — and you have to violate object encapsulation — but you can view the hidden easter egg by running the following:

IRB.__send__ :easter_egg

…​which will then produce the following animation:

Easter Egg

Conclusion

Hopefully the above has been helpful. Keep an eye on this space because, with so much active development going on, there’s bound to be new features and/or improvements to what I’ve documented above. Enjoy!