The letter A styled as Alchemists logo. lchemists
Published February 15, 2023 Updated December 30, 2023
Cover
Ruby Bundler Inline

One of the most useful features of Bundler is the ability to craft an inline script (a.k.a. single file script) so you can encapsulate your dependencies and code within a single file. This gives you the following benefits:

  • Reusablity: Once the script has been written you can reuse it multiple times or with different permutations.

  • Sharability: Due to all dependencies and code encapsulated in a single file, you can quickly share the file with others, use for documentation purposes, and so on.

In addition to the above benefits, there are several use cases as well. I’ll spend the rest of this article delving into what each of those are so you can level up your workflow as well. 🚀

Use Cases

There are multiple use cases so I won’t be exhaustive but I do want to highlight the ones I use the most in case it’s of interest.

Code Spikes

The first use case is the ability to quickly experiment with code in a throw-away fashion. Here’s the default template I use which, by the way, is generated via a code snippet as found in the Sublime Text Setup project so I can quickly craft inline scripts:

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

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "", "~> "
end

As you can see, the above sets the proper pragmas, documents how to use the script, and provides the boilerplate I need so I can focus on my code spike implementation.

What you don’t see in the above template, is my cursor is immediately placed within the first pair of quotes on the second to last line where I define my gem dependencies. This allows me to immediately define the dependencies I need in order to experiment and learn new ideas. To make this easier, I generally include these gems for debugging purposes:

  • Debug: Allows you to set breakpoints and debug your code more easily.

  • Amazing Print: Allows you to pretty print your objects for improved readability.

So, in truth, my starting script looks a bit more like this:

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

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
end

At this point I’m ready to spike some code. So for example, maybe I want to experiment with Dry Schema. To do that, I could craft the following code spike:

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

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
  gem "dry-schema"
end

Demo = Dry::Schema.Params { required(:label).filled :string }

puts Demo.call(label: "Test").inspect
puts Demo.call(bogus: "Danger!").inspect

Now, when I run the above script as ./snippet, I get the following output:

Fetching gem metadata from https://rubygems.org/....
Resolving dependencies...
#<Dry::Schema::Result{:label=>"Test"} errors={} path=[]>
#<Dry::Schema::Result{} errors={:label=>["is missing"]} path=[]>

Great, I’ve learned how to verify good and bad input. From this point I can continue experimenting or throw away the script entirely in order to explore new ideas.

Issues

As they say, images are worth a thousand words. Well, with inline scripts, I’d say they are more valuable than images — although a screenshot/screencast doesn’t hurt either 😉 — because the maintainer of a codebase can respond to you more quickly with a working solution. Not only does this make the maintainer’s job easier but you benefit, as the reporter, with faster feedback and ultimate resolution of your issue. This is a win-win for both parties so definitely get into the habit of doing this. A good template I like to use for reporting issues — especially when maintainers don’t provide one for you — is:

## Overview
<!-- Required. Describe, briefly, the behavior experienced and desired. -->

## Screenshots/Screencasts
<!-- Optional. Attach screenshot(s) and/or screencast(s) that demo the behavior. -->

## Steps to Recreate
<!-- Required. List exact steps (numbered list) to reproduce errant behavior. -->

## Environment
<!-- Required. What is your operating system, software version(s), etc. -->

Example:

## Overview

Hello. 👋 I'm seeing the following issue (add summary here) but would expect the following (add summary here).

## Steps to Recreate

Here's how to reproduce the problem:

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

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
  gem "http"
end

# The following answers 400 when I'd expect to see 200 instead.
puts HTTP.get("https://www.example.com").status.code
```

## Environment

- macOS 13.2 (22D49)
- ruby 3.2.1 (2023-02-08 revision 31819e82c8) +YJIT [arm64-darwin22.3.0]

Granted, the above is a contrived example but hopefully you get an idea how effective Bundler Inline scripts can be for reporting issues.

Benchmarks

Benchmarking code, especially micro-benchmarks, is another great use case for Bundler Inline scripts. Consider the following:

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

# Save as `benchmark`, then `chmod 755 benchmark`, and run as `./benchmark`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"
  gem "benchmark-ips"
  gem "debug"
end

Warning[:performance] = false

example = {a: 1, b: 2, c: 3}

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "#[]" do
    example[:b]
  end

  benchmark.report "#fetch" do
    example.fetch :b
  end

  benchmark.report "#fetch (default)" do
    example.fetch :b, "default"
  end

  benchmark.report "#fetch (block)" do
    example.fetch(:b) { "default" }
  end

  benchmark.report "#dig" do
    example.dig :b
  end

  benchmark.compare!
end

Running the above will result in the following output:

ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin22.6.0]
Warming up --------------------------------------
                 #[]     1.699M i/100ms
              #fetch     1.560M i/100ms
    #fetch (default)     1.595M i/100ms
      #fetch (block)     1.569M i/100ms
                #dig     1.593M i/100ms
Calculating -------------------------------------
                 #[]     26.681M (± 2.0%) i/s -    134.237M in   5.033089s
              #fetch     24.250M (± 1.3%) i/s -    121.710M in   5.019746s
    #fetch (default)     22.453M (± 0.6%) i/s -    113.219M in   5.042732s
      #fetch (block)     23.579M (± 2.9%) i/s -    119.227M in   5.060744s
                #dig     23.588M (± 7.9%) i/s -    117.912M in   5.044537s

Comparison:
                 #[]: 26681112.5 i/s
              #fetch: 24250174.8 i/s - 1.10x  slower
                #dig: 23588471.7 i/s - 1.13x  slower
      #fetch (block): 23578554.4 i/s - 1.13x  slower
    #fetch (default): 22452731.8 i/s - 1.19x  slower

Yet again, another handy use case for inline scripts. In fact, this is how all of the scripts work in my Benchmarks project. You can use the output from the above (or both the script + the output) to write a well written commit message especially if you are fixing a performance concern in your codebase.

Automation

Before wrapping up, I’d like to leave you with additional automation to supercharge the above use cases further by wrapping your inline scripts with shell aliases. I don’t like to type more than I have too — and I suspect you don’t either — so reducing any/all inline scripting friction is key to a performant workflow. In my case, I use Bash but feel free to translate for your own environment. Here you go:

# Ruby Benchmark Edit
rbbe='$EDITOR $HOME/Engineering/Misc/benchmark'

# Ruby Benchmark Run
rbbr='$HOME/Engineering/Misc/benchmark'

# Ruby Snippet Edit
rbse='$EDITOR $HOME/Engineering/Misc/snippet'

# Ruby Snippet Run
rbsr='$HOME/Engineering/Misc/snippet'

# Ruby Snippet Watch
rbsw='viddy $HOME/Engineering/Misc/snippet'

With the above — which are part of my Dotfiles project — I can easily edit/run a benchmark/snippet with minimal effort. By the way, the last alias uses Viddy — which you can find as part of my macOS Configuration — to automatically rerun my inline script every couple of seconds for fast feedback. ⚡️

Evolution

Once you outgrow your simple single-file Bundler Inline script, you might find yourself with an implementation you’d like to evolve into a more sophisticated application. This is where the following tooling can come into play to help you scale as necessary:

  • Rubysmith: Is specifically designed to quickly craft Ruby projects — both small and large — when inline scripts aren’t enough. Rubysmith shines when you need to share a Ruby project which might need images, corresponding specs, multiple objects, etc. so having Rubysmith in your back pocket will let you scale as needed.

  • Gemsmith: If a Ruby application built by Rubysmith isn’t enough, then packaging your code as a reusable library might be a good alternative. This is where a tool like Gemsmith can help you.

  • Hanamismith: Should neither Rubysmith or Gemsmith fit your needs, then Hanamismith might be an option as it will allow you to build a full blown Ruby API and/or web application where you can do anything.

Bundler Inline scripts are great, don’t get me wrong, but should you find yourself needing to turn your initial experiments into something more full fledged, the above will give you multiple paths in which to evolve your implementation into something greater.

Conclusion

I hope this exploration of Bundler Inline scripts has been useful. May your workflow be quick and any previous friction you might have had be reduced. Enjoy!