Ruby - Getting Started

I tried REPL with Kotlin and Java, both sucked: REPL with Kotlin is deprecated, unsupported and getting removed; REPL with Java is just bloated. Gave up. I’m becoming a huge fan of the “No tools” movement - use as few tools as possible. I believe this approach leads to simplicity. Tools lead you to the vicious cycle of complexity: tools hide the complexity away from you, which means you’re getting away with complexity, which means it creeps in, and you solve that with more tools. That also means that Jetbrains has an incentive to make Kotlin as complex and feature-rich as they can get away with, since their business model is to sell you tools. I’m starting to get disinterested in that, I’m fed up with Merchants With Complexity. No-tools also simplifies the ramp-up for newbies: imagine a newbie having to study Maven/Gradle, Intellij just to run Hello World app and then do something just a tiny bit more complex (like parsing YAML/JSON/XML or such).

Anyways, REPL in Ruby is so simple. To remove blank lines and comments from a file, you just need to run:

$ ruby -ne 'puts $_ unless $_.strip.empty? or $_.lstrip.start_with? "#"' <file

Autocompletion? Works! Run irb, type in "foo". and press TAB to cycle through stuff.

The simplest way to get Ruby is from your distro:

$ sudo apt install ruby ruby-bundler

It’s not the newest Ruby, but it’s good enough, and it gets patched and updated along with everything else on the machine.

Additionally, I recommend to set up the build toolchain, which is required to install gems not via apt, but via gem command:

$ sudo apt install ruby-dev make gcc libffi-dev libssl-dev libyaml-dev
  • ruby-dev brings the headers and the mkmf machinery that native gems need in order to compile. Without it, every gem with a C extension fails to build.
  • make and gcc compile native gem parts

Fast

Ruby VM is so fast to start, it’s not even funny. Sure, Java programs start fast too, unless you’re running them from Gradle - and then Gradle either takes 2 seconds to run, or takes shitload of RAM for Gradle Daemon. This has also huge advantage with GitHub Actions: Ruby springs to life and Rake builds your project so fast, the entire thing is done in 10 seconds. Compared to that, Gradle builds always take at least 1 minute.

No Tooling Required

irb auto-completion is brilliant, and it comes straight with Ruby - no need to install tools or anything else. You only need a text editor and irb to get started. Excellent. rake is also baked in, so you can quickly start writing build scripts.

REPL is supported out-of-the-box. Not just that - Ruby is perfect for small scripts, possibly replacing bash.

gems

At some point you’ll need gems, for example when generating documentation via YARD. This exposes the fact that many Ruby gems are half-implemented in C and require some tooling to update. I don’t mind: I won’t be studying their sources, and I’m not forced to write C, so I’m good.

Where the gems go

Run bundle install in a fresh project and it blows up:

Bundler::PermissionError: There was an error while trying to write to
`/var/lib/gems/3.3.0/cache`. It is likely that you need to grant write
permissions for that path.

By default Bundler installs gems into the system-wide gem dir, /var/lib/gems/3.3.0, which is owned by root. Don’t reach for sudo here: that would dump your project’s dependencies into the directory apt manages. Instead point Bundler at your HOME, once, globally:

$ bundle config set --global path ~/.gem

That writes BUNDLE_PATH: "/home/you/.gem" into ~/.bundle/config and applies to every project from now on; gems end up in ~/.gem/ruby/3.3.0/. bundle install now runs happily as a plain user.

ruby-lsp, and which folder goes on the PATH

bundle config path only governs Bundler; gem install has its own idea of where things go. It won’t fail though - as a non-root user it defaults to the per-user gem dir:

$ gem install ruby-lsp
Defaulting to user installation because default installation directory (/var/lib/gems/3.3.0) is not writable.
WARNING:  You don't have /home/mavi/.gem/ruby/3.3.0/bin in your PATH,
      gem executables (ruby-lsp, ruby-lsp-check, ruby-lsp-launcher, ruby-lsp-test-exec) will not run.

So ~/.gem/ruby/3.3.0/bin is the folder to add. Two things not to hardcode there: 3.3.0 tracks your Ruby version, and the ~/.gem part isn’t even fixed. RubyGems picks the user dir like this (rubygems/defaults.rb):

gem_dir = File.join(Gem.user_home, ".gem")
gem_dir = File.join(Gem.data_home, "gem") unless File.exist?(gem_dir)

~/.gem wins if it already exists, otherwise you get the XDG location, ~/.local/share/gem/ruby/3.3.0. Which means the bundle config step above quietly decides this for you: pointing Bundler at ~/.gem creates that folder, and from then on gem install lands in the very same tree. Handy - one dir, one PATH entry.

Just ask RubyGems instead of guessing. In ~/.bashrc:

export PATH="$(gem env user_gemhome)/bin:$PATH"

or, for fish, in ~/.config/fish/config.fish:

command -q gem; and fish_add_path (gem env user_gemhome)/bin

The command -q gem guard is there only to keep the startup quiet if Ruby ever isn’t installed - otherwise fish greets you with Unknown command: gem on every new shell. Asking RubyGems costs some 70ms of Ruby VM startup per shell; my fish starts in under 100ms with this in place, so I can live with that.

You may be tempted to skip the Ruby startup and simply glob the thing:

fish_add_path ~/.gem/ruby/*/bin    # tempting, but no

That does work - as long as it matches. Two ways it bites you:

  • An unmatched wildcard is a hard error in fish, not a silent skip. Before you install your first gem, every new shell opens with No matches for wildcard '~/.gem/ruby/*/bin'; fish skips that line and carries on, but you get the noise. fish_add_path (path filter -d ~/.gem/ruby/*/bin) is the quiet form - the path builtin tolerates unmatched globs, and fish_add_path with zero arguments does nothing at all.
  • fish_add_path persists what it’s given into the universal fish_user_paths variable, and never prunes it. So the glob isn’t really re-evaluated on every startup, the way the config line makes it look: bump Ruby to 3.4 and you keep a dead ~/.gem/ruby/3.3.0/bin on your PATH forever. gem env user_gemhome always names the one dir that’s actually live.

Updating built-in gems (NOT NECESSARY)

This is not really necessary: Ruby projects use Bundler, and Bundler can install all necessary gems itself. Many gems aren’t half-implemented in C, and therefore do not require any additional tooling installed.

To update default gems:

$ gem update

Gem updates will be installed into your per-user gem folder (see above) - the system dir is left alone.

Toy project

Check out koans-ruby which also demoes rake, bundler, YARD and also shows how to run all that from GitHub Actions.

Written on October 15, 2025