BTW, we're ⚡ hiring Infra, SRE, Web, Mobile, and Data engineers at Doximity (see roles) -- find out more about our technical stack.
The Hidden Gems of Ruby's IRB
The hidden gems that make Ruby a delight are not always... gems. There are so many Ruby features built in to the language itself. IRB is a perfect example of this, as one of the most underrated features baked right into Ruby. Whether you're willing to admit it or not, this is where every Rubyist starts their adventure (thank you for showing me the light TryRuby!). The ability to interactively evaluate code before it is "complete" is arguably one of the most important aspects of any language; and even if you deny ever using it, chances are good whatever you were using was, in fact, wrapped up in IRB (here's looking at you Rails console :wink:).
There are so many great articles on using IRB, but this will focus on the bits nestled in the Ruby source. So, out of curiosity, let's crack it open and dig into the details of this wonderful gem nestled inside Ruby.
Feel free to skip to the parts that interest you:
- High-Level IRB
- How IRB evaluates code
- Multiple Sessions
- Loading Files With IRB
- Tracing execution
- New Features in 2.7
- New Features in 3.0
- Customizing IRB
- Easter Egg!
- More Built-in Commands
Note: All links provided to the Ruby source from here on out were based on the latest commit at the time of this writing (Jan 28, 2021).
High-Level IRB
⬇️
⬇️
⬇️
The IRB Workspace
At the core of IRB is the top-level Workspace. This keeps track of the current Session, the Session keeps track of the current Context, and the Context keeps track of the execution context at a particular place in the program (Binding).
For the most part, you're unlikely to use a Workspace directly. IRB uses workspaces under the hood when starting a session from a file. An interesting feature of workspaces though, is that you can evaluate statements within a higher-level binding.
One thing I found potentially useful with Workspaces is with the use of two IRB extensions: workspaces.rb, and change-ws.rb. Using these two extensions allows you to push objects to their own workspace with irb_push_workspace some_object
allowing you to explore from within the object itself. A benefit of this is that you can easily switch between which object you are inside of with irb_change_workspace some_other_object
.
irb(main):001:0> self
=> main
irb(main):002:0> irb_workspaces
=> []
irb(main):003:0> slimer = Struct.new(:name).new("Slimer")
=> <struct name="Slimer">
irb(main):004:0> venkman = Struct.new(:name).new("Venkman")
=> #<struct name="Venkman">
irb(main):005:0> irb_push_workspace slimer
=> [main]
irb(<struct name="Slimer">):006:0> self
=> #<struct name="Slimer">
irb(<struct name="Slimer">):007:0> name
=> "Slimer"
irb(<struct name="Slimer">):008:0> irb_push_workspace venkman
=> [main, <struct name="Slimer">]
irb(<struct name="Venkman">):009:0> self
=> #<struct name="Venkman">
irb(<struct name="Venkman">):010:0> name
=> "Venkman"
irb(<struct name="Venkman">):011:0> irb_change_workspace slimer
=> #<struct name="Slimer">
irb(<struct name="Slimer">):012:0> name
=> "Slimer"
The IRB Context - a.k.a. Session(s)
IRB Sessions are the meat and potatoes of it all. The Session houses all the incredible configurations at your disposal. It is in charge of evaluating the IO you provide it (whether it be a File, String, or STDIN
).
Before we dive into the magic of handling multiple sessions, let's take a closer look at how the IRB evaluation process works.
How IRB evaluates code
IRB depends on two other included Gems to process, analyze, and evaluate Ruby code in a Session: reline, and ripper.
reline – A pure ruby alternative for GNU Readline
IRB uses reline
for many calculations performed on the input provided to an IRB session. Reline was introduced to IRB for the new pretty multi-line input introduced in Ruby 2.7, but comes with extra benefits, like restoring history on subsequent runs and handling auto-completion tokens. If you're curious how the history works, check out the save-history IRB extension. If you want to dive even further, I recommend checking out the input-method IRB extension. The ReidlineInputMethod
class used in the save-history
extension essentially wraps input with reline
. There are a few other input methods IRB supports in there for handling generic IO and File(s). We'll get into more details on some ways IRB uses these.
This is a screen capture of IRB improved by Reline.
ripper – A lexical analyzer and AST generator
Ripper comes with a native C-extension that can generate abstract syntax trees or perform simple lexical analysis. In the context of IRB, ripper
is mostly used for lexical analysis of the Ruby code you feed it. It does this using the RubyLex class. This class does more than just analyzing Ruby input, but it also handles compiling code blocks via RubyVM::InstructionSequence.compile
(or JRuby.compile_ir
when using JRuby).
A simple example (taken from the docs) parsing a hello world method:
require 'ripper'
require 'pp'
pp Ripper.sexp('def hello(world) "Hello, #{world}!"; end')
#=> [:program,
[[:def,
[:@ident, "hello", [1, 4]],
[:paren,
[:params, [[:@ident, "world", [1, 10]]], nil, nil, nil, nil, nil, nil]],
[:bodystmt,
[[:string_literal,
[:string_content,
[:@tstring_content, "Hello, ", [1, 18]],
[:string_embexpr, [[:var_ref, [:@ident, "world", [1, 27]]]]],
[:@tstring_content, "!", [1, 33]]]]],
nil,
nil,
nil]]]]
Multiple Sessions
IRB has a special feature that allows you to manage many sessions at once using it's native multi-irb extension and the subirb command. This works similarly to Unix jobs and allows you access to them in similar ways using the JobManager.
command | what it does |
---|---|
jobs | lists all subsessions |
fg |
Jump to session |
kill |
Kill subsession |
exit | Terminate the current subsession |
irb(main):001:0> jobs
=> #0->irb on main (<Thread:0x00007fb54387c0d0 run>: running)
irb(main):002:0> irb Struct.new(:name).new("Slimer")
irb(1)(<struct name="Slimer">):001:0> jobs
=>
#0->irb on main (#<Thread:0x00007fb54387c0d0 sleep_forever>: stop)
#1->irb#1 on #<struct name="Slimer"> (#<Thread:0x00007fb53305ffd8 /Users/codenamev/.rubies/ruby-3.0.0/lib/ruby/3.0.0/irb/ext/multi-irb.rb:192 run>: running)
irb(1)(<struct name="Slimer">):002:0> irb Struct.new(:name).new("Venkman")
irb(2)(<struct name="Venkman">):001:0> jobs
=>
#0->irb on main (#<Thread:0x00007fb54387c0d0 sleep_forever>: stop)
#1->irb#1 on #<struct name="Slimer"> (#<Thread:0x00007fb53305ffd8 /Users/codenamev/.rubies/ruby-3.0.0/lib/ruby/3.0.0/irb/ext/multi-irb.rb:192 sleep_forever>: stop)
#2->irb#2 on #<struct name="Venkman"> (#<Thread:0x00007fb54393fbc0 /Users/codenamev/.rubies/ruby-3.0.0/lib/ruby/3.0.0/irb/ext/multi-irb.rb:192 run>: running)
irb(2)(<struct name="Venkman">):002:0> kill 1
=> [1]
irb(2)(<struct name="Venkman">):003:0> exit
=> #<IRB::Irb: @context=#<IRB::Context:0x00007fb5428f2948>, @signal_status=:IN_EVAL, @scanner=#<RubyLex:0x00007fb533037970>>
irb(main):003:0> jobs
=> #0->irb on main (#<Thread:0x00007fb54387c0d0 run>: running)
irb(main):004:0>
Loading Files With IRB
IRB comes with a handy loader extension to make it convenient to load files into an IRB session in different ways.
The loader
IRB extension uses the FileInputMethod
we talked about earlier for loading files into an IRB session's context and evaluating it. This is especially useful to pre-load other files we want to be available when loading the IRB session or when code you want to interact with is too long to copy/paste. This can be done with IRB's CLI using the -r
option.
Given a ruby file:
# ./hi.rb
puts "Hi #{`whoami`}!"
Now when we fire up an IRB session loading our hi.rb, we are greeted with a friendly message:
$ irb -r ./hi.rb
Hi codenamev!
irb(main):001:0>
This is a great way to test out pieces of your code in isolation!
Another great feature baked into this loader extension are the line-by-line evaluator helpers irb_load and irb_source.
Let's say we have this super friendly ruby file:
# ./friendly.rb
high_fives = 3
puts "Hello Ruby friend!"
high_fives.times do
puts "High five!"
end
puts "Nice to meet you."
Now, if we load this in an IRB session we can see the line-by-line execution of the file in our output:
Tracing execution
Another handy command built into IRB is tracer. It uses another built-in gem (also named tracer) to output source-level execution trace of a Ruby program. This is useful for seeing the bigger picture for methods that depend on many other objects or methods.
From their example, let's say we have the following Ruby file:
# square.rb
class Calcs
def square(a)
return a*a
end
end
c = Calcs.new
c.square(5)
Now we can turn on tracing in IRB, and load our example file to see the line-by-line execution process.
irb(main):001:0> context.use_tracer = true
=> true
irb(main):002:0" require_relative "square"
0:(irb):2::-: require_relative "square"
0:/Users/codenamev/square.rb:1::-: class Calcs
0:/Users/codenamev/square.rb:1::C: class Calcs
0:/Users/codenamev/square.rb:2::-: def square(a)
0:/Users/codenamev/square.rb:5::E: end
0:/Users/codenamev/square.rb:7::-: c = Calcs.new
0:/Users/codenamev/square.rb:8::-: c.square(5)
0:/Users/codenamev/square.rb:2:Calcs:>: def square(a)
0:/Users/codenamev/square.rb:3:Calcs:-: return a*a
0:/Users/codenamev/square.rb:4:Calcs:<: end
=> true
irb(main):003:1* Calcs.new.square(3)
0:/Users/codenamev/square.rb:2:Calcs:>: def square(a)
0:/Users/codenamev/square.rb:3:Calcs:-: return a*a
0:/Users/codenamev/square.rb:4:Calcs:<: end
=> 9
New Features in 2.7
Introducing colors!
Tab-completion and Double-tab documentation!
Go ahead and re-enable RDoc downloads in your local .gemrc
! Now in an IRB session, when the TAB
key is pressed twice you'll get a lovely display of documentation for the object under the cursor.
Details of how tab completion works can be found in the following sources:
New Features in 3.0
Measure – Output processing time
The new measure IRB extension allows you to easily toggle the output of how long things take to process.
irb(main):001:0> measure
TIME is added.
=> nil
irb(main):002:0> sleep 1
processing time: 1.004886s
=> 1
irb(main):003:0> puts "Measure me."
Measure me.
processing time: 0.000043s
=> nil
irb(main):004:0> measure :off
=> nil
If it will be a while before you're on 3.0, don't fret, you can get similar results using Ruby's native benchmark.
Customizing IRB
IRB is super duper extensible. Out of the box, it comes with many customizations. And while we won't cover it explicitly here, you can even configure how and where your history is saved.
Much like that of your shell, the IRB prompt can be customized as well. At Doximity, we have a few customizations I'd like to share that make working with IRB a little more enjoyable.
But first, let's start with the basics: customizing the prompt.
If you don't supply a file to load from, IRB will default to loading the a global .irbrc
file located in your home directory. The great thing about the RC file is that we can write any Ruby we want to load by default when IRB starts. Let's say we make the following:
# ~/.irbrc
# This gem needs to installed
require 'rainbow'
def me; `whoami`; end
def app_prompt
Rainbow("⚡").yellow
end
def env_prompt
"#{Rainbow("%N").yellow}.#{Rainbow("%m").bright}"
end
prompt = "#{app_prompt}#{env_prompt}"
IRB.conf[:PROMPT][:BANG] = {
PROMPT_I: "#{prompt} ⟩ ", # normal prompt
PROMPT_S: "#{prompt} ✌️ ", # prompt for continuing strings
PROMPT_C: "#{Rainbow("%02n").bold} … ", # Prompt for continuing statement
RETURN: "✨ %s\n" # Format to return value 📣
}
IRB.conf[:PROMPT_MODE] = :BANG
Now when we open up an IRB session, we'll get our new bangin' prompt!
Making it your own: A Custom Rails Console
Under the hood, Rails extends IRB in a somewhat similar way. At Doximity, we've added some of our own customizations:
- A label in the prompt for which environment the console has open.
- Automatically mark any data changes as made by the logged in user.
We keep these changes in an initializer:
module IRBExtension
def start
configure_irb
configure_auditing
super
end
def configure_irb
IRB.conf[:PROMPT] ||= {}
prompt = "#{irb_env}:%03n:%i"
IRB[:PROMPT][:CUSTOM] = {
PROMPT_I: "#{prompt}> ",
PROMPT_N: "#{prompt}> ",
PROMPT_S: "#{prompt}%l ",
PROMPT_S: "#{prompt}* ",
RETURN: "=> %s\n"
}
end
def irb_env
case Rails.env
when development
Rainbow("development").green
when "production"
Rainbow("production").red
else
Rainbow(Rails.env.to_s).yellow
end
end
def configure_auditing
warn(audit_setup_instructions) and return if audit_login.blank? || audit_user_id.blank?
warn "User Auditing now setup to assign all changes made to a user with you: #{audit_login}@example.com."
Thread.current.thread_variable_set(:audit_user_id, audit_user_id)
end
def audit_user_id
@audit_user_id ||= User.select(:id).where(login: "#{audit_login}@example.com").id
end
def audit_login
@audit_login ||= ENV.fetch("LOGGED_IN_USER", nil)
end
def audit_setup_instructions
<<~INSTRUCTIONS
In order to ensure changes you make are properly associated with your account,
please run the following command with your User's id in place of <user_id>:
Thread.current.thread_variable_set :audit_user_id, <user_id>
INSTRUCTIONS
end
end
module Rails
class Console
prepend IRBExtension
end
end
With this lovely magic, we're able to accomplish our two goals. First, we signal to developers what environment they're working in. Sometimes there's a need to jump into production, or a staging environment, and this is a great way to quickly tell you, "Hey! Be careful, you're DOING IT LIVE!"
Second, we establish accountability for production data changes. In order to make this work, we have a few pre-requisite assumptions:
- All engineers with access to the console have a User with a login
- The above login is the same used to log into the machine hosting the console
- The above login is also accessible from an environment variable.
With those assumptions, we're able to pre-fetch a user in our application based on the user that is logged into the machine running the Rails console. We then assign this user's id in a threaded variable that we can then use elsewhere in our code to audit any changes made to the database.
If you're looking for more Rails Console fun, check out Amy Hoy's great article, "Secrets of the Rails Console Ninjas".
Easter Egg!
More Built-in Commands
If by now you still haven't gotten enough, there's more! While not too much more, it might be worth checkout out some of these other built-in commands to see if you have a use for them:
- inspector.rb – Build your own Inspector to use in your sessions.
- notifier.rb – This is the output formatter used internally by the lexer, but could be used to extend your own.
- xmp.rb – An example printer for IRB similar to PrettyPrint.
- magic-file.rb – ??
Community Cheers
The following articles were instrumental in many aspects of making this post come true:
- What's new in Interactive Ruby Shell (IRB) with Ruby 2.7 – by Prajakta Tambe
- IRB: Let's Bone Up On The Interactive Ruby Shell – by Peter Cooper
- IRB's Built-in Measure – by Jemma Issroff
- How To Use IRB to Explore Ruby – by Brian Hogan
- Demystifying IRB Commands – by Gabriel Homer
- Programming Ruby - The Pragmatic Programmer's Guide – by Dave Thomas, Chad Fowler, and Andy Hunt
- Making Your Rails Console Interesting – by Akshay Birajdar
- IRB MixTape – by Chris Wanstrath
Wrapping it up
If you've take anything away from this, it should be that the Ruby source is not so scary! It's not all C code. There's quite a lot of Ruby code. And while it may be nice to reach for those external gems and tools like Pry, chances are you're missing out on what's already there.
Be sure to follow @doximity_tech if you'd like to be notified about new blog posts.