ruby

Writing shell scripts for xbar using Ruby, rbenv, and Bundler

Shell scripts for xbar can be written in any language, including Ruby! But I found that getting these scripts to run using a modern Ruby stack was a little tricky. Here’s what I figured out.

Using a modern Ruby version

Xbar runs as a background service that does not have access to the user’s interactive shell environment. That means that a Ruby script will run using the system-provided Ruby by default. I don’t want to use the system Ruby on macOS, because it is extremely old (Ruby 2.6, as of macOS Ventura in March 2023), and has reached end-of-life status.

To make Ruby shell scripts use an rbenv-managed Ruby instead, I’m now using this shebang header:

#!/usr/bin/env -S-P${HOME}/.rbenv/shims:${PATH} ruby
This prepends the rbenv shims directory to the path, so that when ruby is subsequently executed, rbenv’s Ruby is selected instead of the system default.

Managing gems with Bundler

When I first discovered xbar, I immediately thought of building scripts that would leverage external APIs (one of my first ideas was to display an air quality indicator using data from the airnow.gov API). To make those scripts easier to write, I would need to use some gems. Depending on the situation, I’ve learned there are a couple different ways to manage gems with Bundler in a shell script.

bundler/inline

Bundler can be embedded directly into a shell script using bundler/inline. Whenever the script is run, Bundler will download and install the specified gems automatically, as needed.

#!/usr/bin/env -S-P${HOME}/.rbenv/shims:${PATH} ruby

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "faraday"
end

# Use gems listed in the gemfile block, no `require` needed
Faraday.get(...)
The contents of the gemfile block work just like an external Gemfile. Similar to Rails, the gems do not need an explicit require before using.

Gemfile

In the scenario where multiple shell scripts use the same gem dependencies, an alternative is to place a separate Gemfile in the same directory as the scripts and reference it within each script like so:

#!/usr/bin/env -S-P${HOME}/.rbenv/shims:${PATH} ruby

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("Gemfile", __dir__)
require "bundler/setup"
Bundler.require

# Use gems listed in the Gemfile, no `require` needed
Faraday.get(...)
This is similar to how Bundler is initialized via config/boot.rb in a typical Rails app.

Unlike the bundler/inline method, bundle install needs to be run manually to ensure the gems are downloaded and installed prior to running the script.

Aside: What is xbar?

xbar is a plugin system for custom menubar items on macOS. Given a shell script (which can be written in any language), xbar will execute the script at regular intervals and turn the script’s stdout into a dynamic menubar item.

You just read

April 2023

Share this post?  Copy link

About the author

Hi! I’m a Ruby and CSS enthusiast, regular open source contributor, software engineer, and occasional blogger writing from the San Francisco Bay Area. Thanks for stopping by! —Matt

GitHub Email LinkedIn