RubyでCLIツールを作成する手順まとめ 

ちょっとしたCLIツールを自作しようと思い色々と調べてみたところ、Rubyの[Thor](https://github.com/erikhuda/thor)というgemを使用することで非常に簡単にCLIツールを作成できるようだったので試してみました。  

本記事ではRubyでThorを使用してCLIツールを作成し、コマンドとして実行するまでの手順についてざっくりとまとめていきます。

## 開発環境

* OS: macOS Sierra 10.12.6
* Ruby: 2.5.1（rbenvでインストール）

## 手順

### Step 1. gemパッケージの雛形を作成

`$ gem install bundler`でbundlerをインストールした後（インストール済みであれば省略）、`$ bundle gem {gemパッケージ名} -b`でgemパッケージの雛形を作成します。  

以下の例では、`str_convert_utils`という名称でgemを作成していきます。

```console
# bundlerをインストール
$ gem install bundler

# gemパッケージの雛形を作成
$ bundle gem str_convert_utils -b
Creating gem 'str_convert_utils'...
      create  str_convert_utils/Gemfile
      create  str_convert_utils/lib/str_convert_utils.rb
      create  str_convert_utils/lib/str_convert_utils/version.rb
      create  str_convert_utils/str_convert_utils.gemspec
      create  str_convert_utils/Rakefile
      create  str_convert_utils/README.md
      create  str_convert_utils/bin/console
      create  str_convert_utils/bin/setup
      create  str_convert_utils/.gitignore
      create  str_convert_utils/exe/str_convert_utils
Initializing git repo in /PATH/TO/str_convert_utils
```

### Step 2. gemspecファイルを修正

`{gemパッケージ名}.gemspec`を修正します。  
`spec.summary`と`spec.description`に`TODO`または`FIXME`の文言を含んでいる場合や、`spec.homepage`がURIとして不正な形式である場合、後ほど`$ bundle install`を実行する際にエラーとなるので修正しておきます。

```diff:str_convert_utils.gemspec

 lib = File.expand_path("../lib", __FILE__)
 $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
 require "str_convert_utils/version"

 Gem::Specification.new do |spec|
   spec.name          = "str_convert_utils"
   spec.version       = StrConvertUtils::VERSION

   ...

-  spec.summary       = %q{TODO: Write a short summary, because RubyGems requires one.}  # この行を修正
-  spec.description   = %q{TODO: Write a longer description or delete this line.}        # この行を修正
-  spec.homepage      = "TODO: Put your gem's website or public repo URL here."          # この行を修正
+  spec.summary       = "string convert utilities"
+  spec.description   = "string convert utilities (e.g. snake, camelize, etc.)"
+  spec.homepage      = "https://github.com/akisame338/str_convert_utils"

   ...

   spec.add_development_dependency "bundler", "~> 1.16"
   spec.add_development_dependency "rake", "~> 10.0"
+
+  spec.add_dependency "thor"  # この行を追加
end
```

### Step 3. CLIクラスを作成

続いて、`lib/{gemパッケージ名}/cli.rb`を新規作成します。  
`Thor`を継承したクラスでpublicメソッドを定義することで、当該メソッドをコマンドとして扱うことができます。  
メソッド上部の`desc`の部分については、`desc "{usage}", "{description}"`と定義することで、ヘルプにて使い方（`{usage}`部分）と説明（`{description}`部分）を表示できるようになります。

以下の例では、[こちらの記事](https://qiita.com/kumagi/items/04edfb73fd2f5a060510)を参考にしてsnake_caseをCamelCaseに変換して出力する`camelize`コマンドとCamelCaseをsnake_caseに変換して出力する`snake`コマンドを定義しています。

```ruby:lib/str_convert_utils/cli.rb
require "str_convert_utils"
require "thor"

module StrConvertUtils
  class CLI < Thor
    desc "camelize {snake_case_string}", "convert {snake_case_string} to {camelCaseString}"
    def camelize(str)
      puts str.split("_").map{|w| w[0] = w[0].upcase; w}.join
    end

    desc "snake {CamelCaseString}", "convert {CamelCaseString} to {snake_case_string}"
    def snake(str)
      puts str
        .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
        .gsub(/([a-z\d])([A-Z])/, '\1_\2')
        .tr("-", "_")
        .downcase
    end
  end
end
```

### Step 4. 実行部分の実装

`lib/{gemプロジェクト名}.rb`を修正して、Step. 3で作成した`lib/{gemパッケージ名}/cli.rb`を読み込むようにします。

```diff:lib/str_convert_utils.rb
 require "str_convert_utils/version"
+require "str_convert_utils/cli"  # この行を追加

 module StrConvertUtils
   # Your code goes here...
```

また、`exe/{gemプロジェクト名}`（実行ファイル）を修正して、`{gemパッケージのモジュール名}::{CLIクラス名}.start`を実行するようにします。

この実行ファイルは`bin/`と`exe/`のどちらに置くべきなのか？と少し悩みましたが、[こちらの記事](http://karur4n.hatenablog.com/entry/2015/02/20/155411)によると

> rubygems が bin を見てることはないし、bin は setup や console といった開発の際に使うファイルを置きたい。あなたが作った executables なコマンドは exe で面倒見ようよ。って感じ。

とのことなので、`exe/`以下に置いておけばよさそうです。（gemspecファイルの初期設定も`spec.bindir = "exe"`となっているので）

```diff:exe/str_convert_utils
 #!/usr/bin/env ruby

 require "str_convert_utils"
+
+StrConvertUtils::CLI.start  # この行を追加
```

### Step 5. コマンド実行

`$ bundle install`を実行して`Thor`等のインストールが完了したら、あとは`$ bundle exec exe/{gemパッケージ名} {コマンド名}`でコマンドが実行できるようになります！

```console
# Thor等のgemをインストール
$ bundle install

# コマンド一覧
$ bundle exec exe/str_convert_utils
Commands:
  str_convert_utils camelize {snake_case_string}  # convert {snake_case_string} to {CamelCaseString}
  str_convert_utils help [COMMAND]                # Describe available commands or one specific command
  str_convert_utils snake {CamelCaseString}       # convert {CamelCaseString} to {snake_case_string}

# camelizeコマンド実行
$ bundle exec exe/str_convert_utils camelize snake_case_string
SnakeCaseString

# snakeコマンド実行
$ bundle exec exe/str_convert_utils snake CamelCaseString
camel_case_string
```

### Step 6. より便利にコマンド実行できるようにする

Step. 5までの手順で一応CLIツールとして使えるようになりましたが、このままだと毎回gemパッケージのディレクトリに移動して`$ bundle exec ...`としなければコマンドが実行できないため少し不便です。そこで、作成したgemパッケージをインストールして`$ {gemパッケージ名} {コマンド名}`としてコマンド実行できるようにします。  
作成したgemをインストールできるようにするには[RubyGems.org](https://rubygems.org/)にアップロードして公開するのが一般的らしいですが、今回は[こちらの記事](https://qiita.com/jacoyutorius/items/7df32a1653b5572adbc0#%E5%A4%A7%E4%BA%BA%E3%81%97%E3%82%81%E3%81%AB%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8Brubygemsorg%E7%84%A1%E8%A6%96)を参考にして、RubyGems.orgを使用せずに作成したgemパッケージをインストールしてみます。

[specific_install](https://github.com/rdp/specific_install)というgemを使用するとgithubに公開しているgemをグローバルな場所（rbenvを使用している場合は`~/.rbenv/shims`以下）にインストールできるそうなので、こちらを使用して作成したgemパッケージをインストールします。  
これによって、どのディレクトリにいても今回作成したgemパッケージのコマンドを実行できるようになります！

```console
# specific_installをインストール
$ gem install specific_install

# 自作gemパッケージをグローバルな場所にインストール
$ gem specific_install -l https://github.com/akisame338/str_convert_utils.git

# camelizeコマンド実行
$ str_convert_utils camelize snake_case_string
SnakeCaseString

# snakeコマンド実行
$ str_convert_utils snake CamelCaseString
camel_case_string
```

## 参考記事

* [rubyでsnake_caseをCamelCaseへ変換する - Qiita](https://qiita.com/kumagi/items/04edfb73fd2f5a060510)
* [Gemを作成する準備 › なんか書いていこうぜー.com](https://muryoimpl.com/blog/2015/02/08/prepare-to-create-hoge-gem/)
* [Gemの作り方まとめ 普通のgem編 - masarakki's blog](http://masarakki.github.io/blog/2014/02/15/how-to-create-gem/)
* [Gemの作り方(入門編) - Qiita](https://qiita.com/takashi/items/672781ed1b6255303381)
* [Ruby で作る、簡単 CLI ツールのススメ - Qiita](https://qiita.com/rosylilly/items/d7f9667f3998cfc420d1)
* [gem の実行可能ファイルは exe に保存する - 大学生からの Web 開発](http://karur4n.hatenablog.com/entry/2015/02/20/155411)
* [RubyでオレオレCLIツールを作って大人しめに公開する - Qiita](https://qiita.com/jacoyutorius/items/7df32a1653b5572adbc0)