install-shim


File under: ruby.

This is a script that I use to make Ruby projects accessible in my $PATH. It writes a shim in ~/bin that executes the program with the right settings for rbenv and bundler. You can use it too for those cases where a simple symlink is not good enough. Obviously, this is not for you if you prefer RVM.

I’ve been using it for a few years already and I’m really quite pleased with it.

$ cat ~/bin/install-shim
#!/usr/bin/env ruby
require 'erb'
require 'optparse'
require 'pathname'

PROJECT_ROOT_FILES = %w(.ruby-version Gemfile .env)
$verbose = false
$force = false
$dry_run = false

optionparser = OptionParser.new do |parser|
  parser.banner = "Usage: #{$0} [options] PATH"
  parser.separator ''
  parser.separator 'Install a shim in ~/bin for your Ruby project. Use this instead of a symlink if your script depends on bundler or rbenv.'
  parser.separator ''

  parser.on("-d", "--[no-]dry-run", "print shim to stdout; do not write to file") do |d|
    $dry_run = d
  end

  parser.on("-f", "--[no-]force", "Overwrite shim if it already exists") do |f|
    $force = f
  end

  parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    $verbose = v
  end

  parser.on_tail("-h", "--help", "Show this message") do
    puts parser
    exit
  end
end
optionparser.parse!(ARGV)

if ARGV.empty?
  $stderr.puts "PATH is missing."
  $stderr.puts
  abort optionparser.to_s
end

def info(*string)
  $stderr.puts(*string) if $verbose
end

def detect_project_root(dir)
  Pathname.new(dir).ascend do |path|
    return path.to_s if PROJECT_ROOT_FILES.any? { |filename| File.exist?("#{path}/#{filename}") }
  end
  abort "Cannot find #{PROJECT_ROOT_FILES.join(' or ')} for executable."
end

class String
  def lchomp(match)
    if index(match) == 0
      self[match.size..-1]
    else
      self.dup
    end
  end
end

TARGET_DIRS = ["#{ENV['HOME']}/bin/shims", "#{ENV['HOME']}/bin"]
def target_dir
  TARGET_DIRS.detect { |dir| File.directory? dir }
end

# Set project_root, subdir, and basename
target = ARGV.first
full_path = File.absolute_path target

project_root = detect_project_root(full_path)
subdir = File.dirname(full_path).lchomp(project_root)
basename = File.basename(target)

info "project root = #{project_root}"
info "subdir = #{subdir}"
info "basename = #{basename}"

if project_root.start_with?(ENV['HOME'])
  project_root = project_root.sub(ENV['HOME'], '$HOME')
  info "normalized project root = #{project_root}"
end

# Generate the script
script = ERB.new(DATA.read).result(binding) # the call to #binding gives the template access to all vars

if $dry_run
  puts script
  exit 0
end

# Write the shim
shim = "#{target_dir}/#{basename}"
abort "File #{shim} already exists. Use --force to overwrite it." if File.exist?(shim) && !$force
File.write(shim, script)
`chmod +x #{shim}`

__END__
#!/bin/sh

# A very very stupid shim that seems to work fine for my projects
# The good thing about it is that it:
#
# 1. is stupidly simple
# 2. does not change the working directory!
# 3. does not change anything besides two ENV variables

dir=<%= project_root %>

export RBENV_DIR=$dir
export BUNDLE_GEMFILE=$dir/Gemfile
$dir<%= subdir %>/$(basename "$0") "$@"