How to build a single file ruby app
Tags: ruby
Published: 2023-07-06 19:40:19

---
title: How to build a single file ruby app
published: true
tags: ruby
---

I recently discovered a little thing, which can be very helpful in some case. Every rubyist should be familiar with the following guideline:

- [One class per file](https://rubystyle.guide/#one-class-per-file)
- Somehow organize you project like the following:

```
project/
├── bin/
│   └── project     # command line entrypoint
├── lib/
│   └── project.rb  # business logic
├── spec/           # (or test, features…)
│   └── project_spec.rb
├── README
…
```

Said otherwise, usually you should put everything related to end-user interaction or CLI tools in an executable script into the `bin` folder; put all your actual code into multiple files in the `lib` directory; and finally, because tests are always good, you should have a dedicated folder for them, named `spec` or `test` or whatever the framework you use encourages you to name it.

But the world is never completely black or white and sometime you might want to work on very very simple tools, like a [little calendar CLI app](https://git.umaneti.net/shell_utils/tree/cal.rb), or a [git helper](https://git.umaneti.net/shell_utils/tree/git_tools/git_changelog.rb), or just use the power of ruby to write some system script you would have written in shell language otherwise. In those case, you end with only one file containing both the business logic and the command line interface.

This is all good… until you want to test or interact differently with your script. I mean, did you never write a very nice script and struggle to have one part working and you end up copy/pasting part of your script into `irb` until you make it work? Or try to `require` your script because it contains a nice feature into another, and you end up with unwanted `OptionParser` errors?

I did too.

And I finally [found a solution](https://stackoverflow.com/questions/37101151/what-does-file-program-name-mean-in-ruby): How to properly wrap the command line interface in order to mute it when you are not actually *running* your script, but in the contrary requiring it into another script or inside `irb`.

The solution is `return unless $PROGRAM_NAME != __FILE__`.

Let explain it:

- `$PROGRAM_NAME` is a ruby global variable, which will contain the name of the current running program, as seen by the ruby interpreter. So if you try `puts $PROGRAM_NAME` into `irb`, it will output… `irb`. But if you try it inside your script and you run it, you will see that it contains the full path to your script.
- `__FILE__` is another ruby variable, which will contain the full path of the file in which this variable occurs.
- you already know `return`, but you may be surprised to see it directly in the middle of the script, **not** in a function context. You may have expected an `exit` call instead. But the idea is to stop the execution of the file *when we are not directly calling it, but just requiring it inside another running environment*. In that case, calling `exit` would completely stop the ruby environment. You can try to `require` a file containing just an `exit` statement in `irb` and you will see that `irb` itself will quit. Here `return` will just stop the current execution flow and come back to the caller (the file requiring your script).

Knowing that, another idea comes in my mind: what if I use the same trick to also embed some test cases directly inside my script, and make them accessible only when my script is run by a test framework?

Yes, this is an ugly idea, but again, sometime it can help. And it works perfectly fine. The following listing shows such a script containing everything, from the actual code to the command line interface and the test cases. Because why not.

```ruby
# frozen_string_literal: true

# The actual code.
#
# Usually it would have been put alone in
# a file at ./lib/app.rb.
class App
  attr_reader :nam
  def initialize(name)
    @name = name
  end
end

# Make test cases only available for rspec
# execution context:
#
# $ rspec script.rb
# .
#
# Finished in 0.00167 seconds (files took 0.056 seconds to load)
# 1 example, 0 failures
if File.basename($PROGRAM_NAME) == 'rspec'
  RSpec.describe App do
    it 'returns my name' do
      app = App.new('test')
      expect(app.name).to eq 'test'
    end
  end
  return # Stop here when running tests

elsif $PROGRAM_NAME != __FILE__
  # Stop here when not actually running the script.
  # For exemple when trying to require it in irb:
  #
  # 3.2.2 :001 > require_relative 'script'
  #  => true
  # 3.2.2 :001 > App.new('in irb').name
  #  => "in irb"
  return
end

# Run the following when the script is directly 
# called from the command line:
#
# $ ruby script.rb 'Hello world!'
# Hello world!
# $
puts App.new(ARGV[0]).name
```