Rails Console-like Environment for a Plain Ruby Project

Published 01 Sep 2025 · 6 min read
If you're working on a plain Ruby project and need an interactive console for debugging and exploration, this post covers how to set one up. A console allows quick experimentation with project classes, making it easier to test calculations, inspect data, and validate logic without writing temporary scripts. The setup is simple and provides a Rails-like experience for loading and interacting with the code.

I've been working on a Ruby project without Rails. It's a CLI tool that simulates retirement drawdown strategies for Canadians (I'll write a future blog post with more details on that). While building it, I found myself missing the convenience of the Rails console (bin/rails console), which loads all application code, for interactive exploration and debugging.

In my project, for instance, I have both a tax calculator and a reverse tax calculator. Beyond running unit tests, it's often helpful to poke at these classes interactively. I also have market return sequence generators that I sometimes want to explore directly. A console makes this kind of exploratory coding easy, not a substitute for tests, but a fast way to validate ideas.

This post will explain how I setup a Rails-like console environment, for a plain Ruby project.

Project Structure

Here is my project structure (ignoring documentation and test folders). Aside from the main.rb entrypoint, all the code is organized in the lib folder, with sub-folders under that:

.
├── Gemfile
├── Gemfile.lock
├── README.md
├── lib
│   ├── account.rb
│   ├── app_config.rb
│   ├── first_year_cash_flow.rb
│   ├── numeric_formatter.rb
│   ├── output
│   │   ├── console_plotter.rb
│   │   └── console_printer.rb
│   ├── return_sequences
│   │   ├── base_sequence.rb
│   │   ├── constant_return_sequence.rb
│   │   ├── geometric_brownian_motion_sequence.rb
│   │   ├── mean_return_sequence.rb
│   │   └── sequence_selector.rb
│   ├── run
│   │   ├── app_runner.rb
│   │   ├── simulation_detailed.rb
│   │   └── simulation_success_rate.rb
│   ├── simulation
│   │   ├── simulation_evaluator.rb
│   │   └── simulator.rb
│   ├── strategy
│   │   ├── rrif_withdrawal_calculator.rb
│   │   ├── rrsp_to_taxable_to_tfsa.rb
│   │   └── withdrawal_planner.rb
│   ├── tax
│   │   ├── income_tax_calculator.rb
│   │   └── reverse_income_tax_calculator.rb
│   ├── withdrawal_amounts.rb
│   └── withdrawal_rate_calculator.rb
└── main.rb

Before getting into the automated solution, let's cover how you could load and run an individual class from a project manually.

In any Ruby project (Rails or otherwise), you can start an interactive Ruby session with irb. Since irb ships with Ruby itself, there’s nothing extra to install - if you have Ruby, you have irb. Out of the box, though, it doesn't know anything about your project's code, even if you launch it from the project's root directory. For example:

# Try to instantiate the tax calculator
irb(main):004> tx = Tax::IncomeTaxCalculator.new

# (irb):1:in `<main>': uninitialized constant Tax (NameError)
# tx = Tax::IncomeTaxCalculator.new
#      ^^^
# 	from <internal:kernel>:187:in `loop'

This could be resolved by running require_relative ... in the irb session, for one specific file.:

irb(main):002> require_relative "lib/tax/income_tax_calculator"
=> true

# Now the tax calculator class can be used
 tx = Tax::IncomeTaxCalculator.new
# => <Tax::IncomeTaxCalculator:0x000000011def7270
# ...

But this is tedious to have to do every time you want to experiment with some of your project code. It would be nice if all the project code was always available, any time you ran irb from your project root.

The next sections will walk through how to set this up.

Define config/environment.rb

The first step is to create a config directory, and an environment.rb file in that directory:

mkdir config
touch config/environment.rb

Edit config/environment.rb to load all project dependencies from the Gemfile, and all source files in the lib directory:

# frozen_string_literal: true

# Load all dependencies from Gemfile or standard Ruby library
require "descriptive_statistics"
require "tty-progressbar"
require "yaml"
# ...

# Load all project source files from lib dir and its subdirectories
Dir.glob(File.expand_path("../lib/**/*.rb", __dir__)) { |file| require file }

That one line Dir.glob(File.expand_path("../lib/**/*.rb", __dir__)).each { |file| require file } is doing all the heavy lifting of loading the project source.

Explanation:

__dir__ is a built-in method provided by the Kernel module. It returns the absolute path of the directory containing the current file. Since it's being called in the config/environment.rb file, it's value will be /path/to/project/config.

File.expand_path converts a pathname to an absolute pathname. When given an optional dir_string argument, which we're doing here by passing in __dir__, it uses the dir_string as a starting point. But the first argument we're passing in says to go up one directory. So this will return /path/to/project/lib/**/*.rb.

Dir.glob expands it's first argument, which in our case is a pattern string /path/to/project/lib/**/*.rb. It returns an array of all matching file names, which in this case will be all Ruby files contained in the lib directory and all of its subdirectories.

Finally, when given a block, Dir.glob will execute that block for each file matching the pattern. In our case, we pass in a block to require the file. When this code runs, all Ruby files in the project's lib directory will be available in memory.

You don’t have to place this file in config or name it environment.rb - this is just a suggestion for organizing code. Feel free to name it and place it wherever makes sense for your project.

Create Project Specific .irbrc

Now that we have the potential to load all project dependencies from a single file, the next step is to ensure this file is always run when starting an irb session. To achieve this, create a .irbrc file in the project directory. This provides an opportunity to customize the behaviour of irb when started from the project root.

Edit the project level .irbrc file so it has the following. Note that any valid Ruby can be placed in this file:

# Load all the project source
require_relative "config/environment"
puts "Loaded application"

# Optionally customize the prompt
IRB.conf[:PROMPT][:APP] = {
  PROMPT_I: "drawdown_simulator> ",  # Standard input prompt
  PROMPT_N: "drawdown_simulator* ",  # Multiline input
  PROMPT_S: "drawdown_simulator%l ", # String continuation
  PROMPT_C: "drawdown_simulator? ",  # Indentation level
  RETURN: "=> %s\n" # Format of return value
}

IRB.conf[:PROMPT_MODE] = :APP # Set custom prompt

Now when you run irb at the terminal, it will run all the code in .irbrc in the project root. If you chose to customize the prompt, it will display the project name instead of the default irb(main)> prompt.

All project classes will be loaded, so you can now interact with them, without having to explicitly load them. Additionally, irb's built-in autocomplete will help you quickly explore your project's classes and methods. For example:

rails console for ruby irb autocomplete tax namespace

rails console for ruby irb autocomplete return-sequences namespace

Optionally, you can add a reload! method to the .irbrc file. If you're used to this behavior from Rails, it's not part of IRB:

# .irbrc

# other code...

def reload!
  exec "irb"
end

Now if you make any changes to application code, rather than being forced to exit and run irb again, running reload! will refresh the code loaded in memory.

Optional Bin Script

If rather than running irb, you're used to the "muscle memory" of running a bin script, as has become standard on Rails projects, you can layer that on with the irb customization as follows:

Create a bin directory and a console file in that directory, which needs to be executable:

mkdir bin
touch bin/console
chmod +x bin/console

Edit bin/console as follow:

#!/usr/bin/env ruby

require "irb"

# This will use project level `.irbrc` so no need to load config/environment here.
IRB.start

Now, you can start an interactive session with:

bin/console

It will behave the same as having run irb.

Alternative

If for whatever reason you don't want to commit a custom .irbrc file into the project root - maybe it conflicts with other custom settings in your home ~/.irbrc or each team member prefers to maintain their own version, an alternative approach is to load the config/environment.rb in bin/console like this:

#!/usr/bin/env ruby
# frozen_string_literal: true

require "irb"
require_relative "../config/environment"

IRB.start

Now when running bin/console, it will load all the project source files.

Other Uses For config/environment.rb

A benefit of having taken the time to construct config/environment.rb is that it can be used in the project entrypoint and in a test helper. This avoids manually requiring lists of files in multiple places throughout the project. For example, main.rb:

require_relative "config/environment"

# Whatever code starts your project...
Run::AppRunner.new("inputs.yml", ARGV[0]).run

If using RSpec for testing, then spec/spec_helper.rb can require the config right after loading rspec:

require "rspec"
require_relative "../config/environment"

RSpec.configure do |config|
  # ...
end

Summary

This post has covered several approaches for providing a Rails-like console experience for any Ruby project, making interactive debugging and exploration just as easy as when using Rails. We started by defining config/environment.rb to load all project dependencies in one place. Then covered options for a project-specific .irbrc file and/or a bin script. If you’re working on a CLI tool or any non-Rails Ruby project, consider adding a console to your workflow.