Thor
コマンドラインツールを構築するための人気のRubyライブラリ。Railsでも使用されており、使いやすく、テストしやすく、拡張しやすい設計です。
GitHub概要
rails/thor
Thor is a toolkit for building powerful command-line interfaces.
ホームページ:http://whatisthor.com/
スター5,179
ウォッチ69
フォーク551
作成日:2008年5月7日
言語:Ruby
ライセンス:MIT License
トピックス
なし
スター履歴
データ取得日時: 2025/7/25 02:06
フレームワーク
Thor
概要
ThorはRubyで強力なコマンドラインインターフェースを構築するためのツールキットです。Ruby on Railsプロジェクトで公式に使用されており、自己文書化機能、オプション解析、コマンド実行機能を提供します。Rakeのような構文でコマンドを定義でき、引数とオプションの処理を簡潔に記述できます。Ruby 2.6.0以降をサポートし、BundlerやVagrant、Rails自体など多くの著名なRubyプロジェクトで採用されている実績あるライブラリです。
詳細
Thorは、コマンドライン実行、オプション解析、「USAGE:」バナーの作成にまつわる煩雑な作業を取り除く、シンプルで効率的なツールです。Rakeビルドツールの代替としても使用でき、特にコード生成やタスク自動化において威力を発揮します。
Ruby on Railsでの採用
ThorはRails v3以降でコード生成に使用されており、以下のようなコマンドがThorで実装されています:
rails new
- 新しいRailsアプリケーションの作成rails console
- Railsコンソールの起動rails server
- 開発サーバーの起動rails routes
- ルート一覧の表示rails generate
- ジェネレーターの実行
主な特徴
- 自動ドキュメント生成: コマンドとオプションの説明から自動的にヘルプメッセージを生成
- メソッドベースのコマンド定義: Rubyクラスの公開メソッドが自動的にコマンドになる
- オプション管理: 型安全なオプション処理(string, hash, array, numeric, boolean)
- 排他オプション:
exclusive
やat_least_one
による高度なオプション制御 - コマンドエイリアス:
map
メソッドによるコマンドのエイリアス設定 - デフォルトコマンド:
default_command
による暗黙のコマンド実行 - サブコマンド:
register
メソッドによる階層的なコマンド構造 - Thor::Group: 連続実行タスクやジェネレーター向けの特殊クラス
- Thor::Actions: ファイル操作やユーザー対話のためのヘルパーメソッド
最新動向(2024年)
- Ruby 2.6.0以降対応: 最新のRubyバージョンに対応
- 継続的なメンテナンス: バグ修正と機能改善が継続的に行われている
- エコシステムの安定性: Railsコミュニティの中核ツールとして安定した開発が続いている
Thor vs Rake
Thorは以下の点でRakeと異なります:
- コマンドライン特化: CLIアプリケーション構築に最適化
- 引数処理: 位置引数とオプションの自然な処理
- ヘルプ生成: 自動的なドキュメント生成
- 型安全性: オプションの型チェック機能
- ユーザー対話: 組み込みの対話型機能
メリット・デメリット
メリット
- Railsとの親和性: Rails公式ツールとして完全に統合されている
- 豊富な実績: Bundler、Vagrant、Rails等の主要プロジェクトで使用
- 直感的なAPI: Rubyらしい自然な記述でコマンドを定義できる
- 自動ドキュメント:
desc
メソッドで自動的にヘルプを生成 - 型安全なオプション: オプションの型を指定して安全に処理
- ファイル操作支援: Thor::Actionsによる豊富なファイル操作ヘルパー
- ユーザー対話:
ask
、yes?
、no?
等の対話メソッド内蔵 - テスト支援: CLIアプリケーションのテストが容易
- Ruby標準: Rubyエコシステムの事実上の標準
デメリット
- Ruby依存: Rubyプロジェクト以外では使用できない
- 学習コスト: 多機能ゆえに全機能を把握するのに時間がかかる
- 設計思想: システムツール向けの設計のため、アプリケーションユーザー入力は非推奨
- パフォーマンス: Rubyの起動時間が影響する場合がある(特に小さなコマンド)
主要リンク
書き方の例
基本的なコマンド定義
#!/usr/bin/env ruby
require 'thor'
class MyCLI < Thor
desc "hello NAME", "NAME に挨拶します"
def hello(name)
puts "こんにちは、#{name}さん!"
end
desc "goodbye NAME", "NAME にお別れの挨拶をします"
method_option :polite, type: :boolean, default: false, aliases: "-p", desc: "丁寧な挨拶"
def goodbye(name)
if options[:polite]
puts "#{name}さん、さようなら"
else
puts "じゃあね、#{name}!"
end
end
end
MyCLI.start(ARGV) if __FILE__ == $0
オプションと引数の詳細な例
#!/usr/bin/env ruby
require 'thor'
class FileManager < Thor
desc "process FILES", "複数のファイルを処理します"
method_option :format, type: :string, default: "txt", aliases: "-f", desc: "出力形式"
method_option :verbose, type: :boolean, default: false, aliases: "-v", desc: "詳細出力"
method_option :exclude, type: :array, default: [], aliases: "-e", desc: "除外パターン"
method_option :config, type: :hash, default: {}, aliases: "-c", desc: "設定オプション"
method_option :count, type: :numeric, default: 1, aliases: "-n", desc: "処理回数"
def process(*files)
puts "処理形式: #{options[:format]}"
puts "詳細モード: #{options[:verbose] ? '有効' : '無効'}"
puts "除外パターン: #{options[:exclude].join(', ')}" unless options[:exclude].empty?
puts "設定: #{options[:config]}" unless options[:config].empty?
puts "処理回数: #{options[:count]}"
files.each do |file|
puts "処理中: #{file}"
puts " 詳細情報..." if options[:verbose]
end
end
end
FileManager.start(ARGV) if __FILE__ == $0
サブコマンドの例
#!/usr/bin/env ruby
require 'thor'
# ユーザー管理サブコマンド
class UserCLI < Thor
desc "create NAME EMAIL", "新しいユーザーを作成"
method_option :admin, type: :boolean, default: false, desc: "管理者権限"
def create(name, email)
puts "ユーザーを作成中..."
puts " 名前: #{name}"
puts " メール: #{email}"
puts " 権限: #{options[:admin] ? '管理者' : '一般ユーザー'}"
end
desc "delete USER_ID", "ユーザーを削除"
method_option :force, type: :boolean, default: false, aliases: "-f", desc: "確認なしで削除"
def delete(user_id)
unless options[:force]
exit unless yes?("ユーザー #{user_id} を削除しますか? [y/N]")
end
puts "ユーザー #{user_id} を削除しました"
end
desc "list", "ユーザー一覧を表示"
method_option :format, type: :string, default: "table", desc: "表示形式 (table/json)"
def list
users = [
{ id: 1, name: "田中太郎", email: "[email protected]" },
{ id: 2, name: "佐藤花子", email: "[email protected]" }
]
case options[:format]
when "json"
require 'json'
puts JSON.pretty_generate(users)
else
puts "ID\t名前\t\tメール"
puts "-" * 40
users.each do |user|
puts "#{user[:id]}\t#{user[:name]}\t#{user[:email]}"
end
end
end
end
# プロジェクト管理サブコマンド
class ProjectCLI < Thor
desc "init NAME", "新しいプロジェクトを初期化"
method_option :template, type: :string, default: "basic", aliases: "-t", desc: "プロジェクトテンプレート"
def init(name)
puts "プロジェクト '#{name}' を初期化中..."
puts "テンプレート: #{options[:template]}"
empty_directory(name)
create_file("#{name}/README.md", "# #{name}\n\n新しいプロジェクトです。")
create_file("#{name}/Gemfile", "source 'https://rubygems.org'\n\n# gems here")
end
desc "build", "プロジェクトをビルド"
method_option :environment, type: :string, default: "development", aliases: "-e", desc: "環境"
def build
puts "#{options[:environment]} 環境でビルド中..."
puts "ビルド完了"
end
end
# メインCLI
class MainCLI < Thor
desc "user SUBCOMMAND ...ARGS", "ユーザー管理"
subcommand "user", UserCLI
desc "project SUBCOMMAND ...ARGS", "プロジェクト管理"
subcommand "project", ProjectCLI
desc "version", "バージョン情報を表示"
def version
puts "MyApp v1.0.0"
end
# デフォルトコマンドを設定
default_command :help
end
MainCLI.start(ARGV) if __FILE__ == $0
Thor::Groupを使ったジェネレーター
#!/usr/bin/env ruby
require 'thor'
class AppGenerator < Thor::Group
include Thor::Actions
# ジェネレーターの説明
desc "アプリケーションの雛形を生成します"
# 引数の定義
argument :name, type: :string, desc: "アプリケーション名"
# オプションの定義
class_option :database, type: :string, default: "sqlite", desc: "データベース種別"
class_option :testing, type: :string, default: "rspec", desc: "テストフレームワーク"
class_option :skip_git, type: :boolean, default: false, desc: "Git初期化をスキップ"
# テンプレートファイルのソースパス
def self.source_root
File.dirname(__FILE__) + "/templates"
end
# 以下のメソッドが順次実行される
def create_app_directory
say "アプリケーションディレクトリを作成中...", :green
empty_directory(name)
end
def create_config_files
say "設定ファイルを作成中...", :green
template("config.rb.tt", "#{name}/config/application.rb")
template("database.yml.tt", "#{name}/config/database.yml")
end
def create_app_files
say "アプリケーションファイルを作成中...", :green
create_file "#{name}/app/models/.keep"
create_file "#{name}/app/controllers/.keep"
create_file "#{name}/app/views/.keep"
end
def create_test_files
say "テストファイルを作成中...", :green
case options[:testing]
when "rspec"
template("spec_helper.rb.tt", "#{name}/spec/spec_helper.rb")
create_file "#{name}/spec/models/.keep"
when "minitest"
create_file "#{name}/test/test_helper.rb"
create_file "#{name}/test/models/.keep"
end
end
def create_gemfile
say "Gemfileを作成中...", :green
template("Gemfile.tt", "#{name}/Gemfile")
end
def init_git
return if options[:skip_git]
say "Gitリポジトリを初期化中...", :green
inside(name) do
run("git init")
create_file(".gitignore", "/tmp\n/log/*.log\n")
run("git add .")
run("git commit -m 'Initial commit'")
end
end
def show_readme
say "\n" + "="*50, :green
say "アプリケーション '#{name}' が正常に作成されました!", :green
say "="*50, :green
say ""
say "次のステップ:"
say " cd #{name}"
say " bundle install"
say " # 開発を開始してください"
say ""
end
end
# 使用方法: ruby generator.rb myapp --database=postgresql --testing=rspec
AppGenerator.start(ARGV) if __FILE__ == $0
Thor::Actionsを使ったファイル操作
#!/usr/bin/env ruby
require 'thor'
class FileOperations < Thor
include Thor::Actions
desc "setup PROJECT_NAME", "プロジェクトのセットアップを行います"
method_option :force, type: :boolean, default: false, aliases: "-f", desc: "既存ファイルを上書き"
method_option :pretend, type: :boolean, default: false, aliases: "-p", desc: "実際には実行しない(ドライラン)"
def setup(project_name)
self.destination_root = project_name
# ディレクトリ作成
empty_directory("src")
empty_directory("tests")
empty_directory("docs")
# ファイル作成
create_file("README.md", "# #{project_name}\n\n新しいプロジェクトです。")
create_file("src/main.rb", "#!/usr/bin/env ruby\n\nputs 'Hello, #{project_name}!'")
# テンプレートからファイル作成
template("config.rb.erb", "config/#{project_name}.rb", {
project_name: project_name,
created_at: Time.now
})
# ファイルコピー
copy_file("templates/gitignore", ".gitignore")
# ディレクトリ内での操作
inside("src") do
create_file("utils.rb", "# ユーティリティクラス")
end
# コマンド実行
run("chmod +x src/main.rb")
# ユーザーに質問
if yes?("Gitリポジトリを初期化しますか?")
run("git init")
run("git add .")
run("git commit -m 'Initial commit'")
end
# 結果表示
say("プロジェクト #{project_name} のセットアップが完了しました!", :green)
end
desc "interactive", "対話的なセットアップ"
def interactive
# 各種質問メソッド
name = ask("プロジェクト名を入力してください:")
database = ask("データベースを選択してください:", limited_to: %w[sqlite postgresql mysql])
use_git = yes?("Gitを使用しますか?")
if use_git
git_remote = ask("リモートリポジトリのURLを入力してください(オプション):")
end
# 確認
say("\n設定内容:", :blue)
say(" プロジェクト名: #{name}")
say(" データベース: #{database}")
say(" Git使用: #{use_git ? 'はい' : 'いいえ'}")
say(" リモートURL: #{git_remote}") if git_remote && !git_remote.empty?
if yes?("\nこの設定でプロジェクトを作成しますか?")
say("プロジェクトを作成中...", :green)
# セットアップ処理...
say("完了!", :green)
else
say("キャンセルされました", :red)
end
end
private
def self.source_root
File.dirname(__FILE__) + "/templates"
end
end
FileOperations.start(ARGV) if __FILE__ == $0
排他オプションと必須オプション
#!/usr/bin/env ruby
require 'thor'
class AdvancedOptions < Thor
desc "deploy", "アプリケーションをデプロイ"
# 排他オプション(どちらか一方のみ指定可能)
method_option :staging, type: :boolean, desc: "ステージング環境にデプロイ"
method_option :production, type: :boolean, desc: "本番環境にデプロイ"
# 最低一つ必要なオプション
method_option :version, type: :string, desc: "デプロイするバージョン"
method_option :branch, type: :string, desc: "デプロイするブランチ"
# その他のオプション
method_option :force, type: :boolean, default: false, desc: "強制デプロイ"
method_option :dry_run, type: :boolean, default: false, desc: "ドライラン"
def deploy
# 排他オプションチェック
if options[:staging] && options[:production]
say("エラー: --staging と --production は同時に指定できません", :red)
exit(1)
end
unless options[:staging] || options[:production]
say("エラー: --staging または --production のいずれかを指定してください", :red)
exit(1)
end
# 最低一つ必要なオプションチェック
unless options[:version] || options[:branch]
say("エラー: --version または --branch のいずれかを指定してください", :red)
exit(1)
end
# デプロイ実行
env = options[:staging] ? "staging" : "production"
target = options[:version] || options[:branch]
say("#{env} 環境に #{target} をデプロイ中...", :green)
if options[:dry_run]
say("(ドライランモード - 実際にはデプロイされません)", :yellow)
end
if options[:force]
say("強制デプロイが有効です", :yellow)
end
say("デプロイ完了!", :green) unless options[:dry_run]
end
end
AdvancedOptions.start(ARGV) if __FILE__ == $0
エラーハンドリングとテスト
#!/usr/bin/env ruby
require 'thor'
class MyTool < Thor
desc "risky_operation FILE", "リスクのある操作を実行"
method_option :backup, type: :boolean, default: true, desc: "バックアップを作成"
def risky_operation(file)
begin
unless File.exist?(file)
say("エラー: ファイル '#{file}' が見つかりません", :red)
exit(1)
end
if options[:backup]
backup_file = "#{file}.backup.#{Time.now.to_i}"
FileUtils.cp(file, backup_file)
say("バックアップを作成: #{backup_file}", :blue)
end
# 何らかの処理...
say("操作完了", :green)
rescue => e
say("エラーが発生しました: #{e.message}", :red)
exit(1)
end
end
# プライベートメソッドはコマンドにならない
private
def validate_file(file)
File.exist?(file) && File.readable?(file)
end
end
# テスト用のヘルパー
if $0 == __FILE__
if ARGV.include?("--test")
require 'minitest/autorun'
class MyToolTest < Minitest::Test
def setup
@cli = MyTool.new
end
def test_help_output
output = capture_output { MyTool.start(["help"]) }
assert_includes output, "risky_operation"
end
private
def capture_output
old_stdout = $stdout
$stdout = StringIO.new
yield
$stdout.string
ensure
$stdout = old_stdout
end
end
else
MyTool.start(ARGV)
end
end