Ruby gemでのコマンドライン・ツールの作り方

たまにgemでツールを作りたくなるのですが、毎回イチから調べ回る羽目になるので、ひと通りの作り方をまとめておきます。

環境

bundler 1.7.6
thor 0.19.1
Ruby 2.1.2p95
Mac OS X Yosemite 10.10.2

gemの雛形を作成

gemの雛形を作るには bundle gem コマンドを実行します。

コマンドで実行するgemの場合は、-b オプションを付けて bin にコマンドファイルを生成させます。

※以降では、DiskSizeRec というツールを作った時の例を示します。ファイル名等は適宜読み替えてください。

$ bundle gem disksizerec -b
      create  disksizerec/Gemfile
      create  disksizerec/Rakefile
      create  disksizerec/LICENSE.txt
      create  disksizerec/README.md
      create  disksizerec/.gitignore
      create  disksizerec/disksizerec.gemspec
      create  disksizerec/lib/disksizerec.rb
      create  disksizerec/lib/disksizerec/version.rb
      create  disksizerec/bin/disksizerec
Initializing git repo in /tmp/disksizerec

他のオプションは bundle help gem で確認できます。

このコマンドを実行すると雛形が生成され、Gitリポジトリも作成されます。

.gitignoreの設定

必要に応じて、.gitignore に以下の項目を追加します:

vendor/bundle/
.idea

1つ目は依存モジュールを格納するディレクトリ、2つ目はRubyMineの設定情報のディレクトリです。

この時点でGitリポジトリにコミットしておきます。

リモートリポジトリの作成

ソースをGitHubやBitbucketなどで管理する場合は、リモートのGitリポジトリを作成します。

GitHubの手順:
  1. GitHubにログインして上部の「+」ボタンから「New repository」を選択。
  2. 各項目を入力して作成するが、README等のファイルは追加しないようにする。
  3. 「Quick setup」の説明が表示されるので「…or push an existing repository from the command line」の内容に従ってコマンドを実行。
    ※僕が作る時には、SSHの方のパスを使っています。
  4. ページをリロードすると、リポジトリのページが表示されるはず。

参考:
Adding an existing project to GitHub using the command line - User Documentation

依存モジュールのインストール

僕は依存gemを vendor/bundle 以下に格納しているので、そのために以下のコマンドを実行します:

$ bundle install --path vendor/bundle

この時、以下のようなエラーが表示されますが、気にしなくてよいです:

disksizerec at /tmp/disksizerec did not have a valid gemspec.
This prevents bundler from installing bins or native extensions, but that may not affect its functionality.
The validation message from Rubygems was:
  "FIXME" or "TODO" is not a description

gemspecファイルの設定

TODO: となっている個所を記述します。

GitHub等でソースを公開している場合は、そのURLを spec.homepage に記述します。

その他の必要な項目を記述します。

Thorの導入

コマンドラインツールを作る場合、Thor を使うのが一般的です。

gemspecファイルに以下の記述を追加します:

spec.add_dependency "thor"

そして、Thorをインストールするために bundle install を実行します。

※他のgemを追加する場合も同様にします。

コマンドライン・インターフェース用のクラスを記述

コマンドライン・インターフェース用のファイル lib/discsizerec/cli.rb を作成し、以下のようなクラスを記述します:

# coding: utf-8

require 'thor'

module Disksizerec
  class CLI < Thor
    
  end
end

lib/disksizerec.rbファイルでcli.rbファイルをロードします:

require "disksizerec/cli"

bin/discsizerecの最後に、CLIを起動するコードを追加します:

Disksizerec::CLI.start

コマンドを記述

ツールのコマンドは、CLIクラスの中に以下のように記述します:

desc "hello NAME", "say hello to NAME"
def hello(name)
  puts "Hello #{name}"
end

この場合、ビルドすると、以下のようにコマンドを指定できるようになります:

$ discsizerec hello NAME

NAMEはパラメーターです。

動作チェック

$ bundle exec bin/disksizerec hello world
Hello world

オプションの記述

特定のコマンドに対してオプションを設定するには option で記述します:

desc "hello NAME", "say hello to NAME"
option :from, type: :string, default: "none"
def hello(name)
  puts "Hello #{name} from #{options[:from]}"
end

実行例:

$ bundle exec bin/disksizerec hello world --from=John
Hello world from John

すべてのコマンドに共通のオプションを設定する場合は class_option で記述します:

class_option :silent, type: :boolean, default: false, desc: 'Not write log messages', aliases: '-s'

参考:
Thor - Home

デフォルト・コマンドの設定

コマンド名を指定しない場合に実行されるデフォルト・コマンドを設定するには default_command を使います:

default_command :hello

なお、デフォルト・コマンドを使う場合(コマンドを指定しない場合)はパラメーターを指定できないので、デフォルト・コマンドにはパラメーターのないコマンドを設定するべきでしょう。

また、ヘルプの表示では、どれがデフォルト・コマンドかわからないので、その点も注意が必要です。

参考:
サブコマンドのないコマンドをthorで管理する方法 - sonots:blog

コマンド内で別のコマンドを呼び出す

invoke を使います:

desc "greeting", "greeting"
def greeting
  invoke :hello, ["everyone"]
end

パラメーターは配列で指定します。

なお、メンバー変数は渡らないので、データはパラメーターで渡す必要があります。

参考:
#invoke(name = nil, *args) ⇒ Object - Module: Thor::Invocation — Documentation for wycats/thor (master)

配置

最も手軽に配置するには、Gitリポジトリからプロジェクトをクローンして、上記の「動作チェック」の項目で記述したように、bundle exec で実行します。

gem install でインストールできるようにするには、RubyGems.org に登録する必要がありますが、今回は行わなかったので省略します。

cronで実行した場合のエラーの対処

作ったツールをcronで実行した場合に(bundle exec で実行)、以下のようなエラーが発生することがあります。

There was a Errno::ENOENT while loading disksizerec.gemspec:
No such file or directory - git from
/usr/local/disksizerec/disksizerec.gemspec:16:in ``'

これは git コマンドが見つからないというエラーです。

調べてみると、僕の環境では git/usr/local/bin に入っており、cronのデフォルトの状態ではパスに /usr/local/bin が含まれていませんでした。

この場合、cronファイルの最初に PATH=/usr/local/bin:/bin:/usr/bin などと追加すると、解決できます。