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.

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

RSS
View all posts →

Open source projects

mattbrictson/rails-template

App template for Rails 7 projects; best practices for TDD, security, deployment, and developer productivity. Now with optional Vite integration! ⚡️

1,055
Updated 1 month ago

mattbrictson/tomo

A friendly CLI for deploying Rails apps ✨

360
Updated 20 days ago

More on GitHub →