GLI
Git風のインターフェースを持つコマンドラインツールを作成するためのRubyライブラリ。
フレームワーク
GLI
概要
GLIは、Git風のインターフェースを持つコマンドラインツールを作成するためのRubyライブラリです。サブコマンドを持つ複雑なCLIアプリケーションの構築に適しており、Git風のインターフェースを好む開発者に選ばれています。豊富な機能と柔軟なアーキテクチャにより、プロフェッショナルなCLIツールの開発を支援します。
詳細
GLI (Git-Like Interface) は、Gitのようなサブコマンド構造を持つCLIアプリケーションを構築するためのライブラリです。コマンド、サブコマンド、グローバルオプション、コマンド固有のオプションを組み合わせた複雑な階層構造を簡単に作成できます。
主な特徴
- Git風インターフェース: サブコマンドベースの階層構造
- DSLベース設計: 宣言的で読みやすいコマンド定義
- 豊富なオプション: グローバル・コマンド固有オプションをサポート
- 自動ヘルプ生成: 美しく整理されたヘルプページを自動生成
- 入力バリデーション: 引数とオプションの検証機能
- 設定ファイル: 自動的な設定ファイル管理
- スケルトン生成: プロジェクトテンプレートの自動生成
- テスト支援: Aruba統合によるテスト機能
メリット・デメリット
メリット
- Git風UI: 親しみやすいサブコマンド構造
- 豊富な機能: プロフェッショナルなCLI開発に必要な機能が揃っている
- 宣言的API: 読みやすく保守しやすいコード
- 自動生成: ヘルプ、設定ファイル、テンプレートの自動生成
- 柔軟性: 複雑なコマンド構造にも対応
- テスト可能: Aruba統合による包括的テスト
- ドキュメント: 充実した公式ドキュメント
デメリット
- 学習コスト: DSLの理解に時間が必要
- オーバーヘッド: シンプルなツールには重い場合がある
- Ruby依存: Ruby環境が必要
- 複雑性: 小規模プロジェクトには過剰な機能
主要リンク
書き方の例
インストール
gem install gli
基本的な使用例
#!/usr/bin/env ruby
require 'gli'
include GLI::App
program_desc 'ファイル管理ツール'
version '1.0.0'
# グローバルオプション
desc 'ログを有効にする'
switch [:l, :log]
desc 'デバッグモードを有効にする'
switch [:d, :debug]
desc '設定ファイルのパス'
flag [:c, :config], default_value: '~/.filemanager'
# listコマンド
desc 'ファイル一覧を表示'
long_desc <<-EOF
指定されたディレクトリのファイル一覧を表示します。
デフォルトでは現在のディレクトリを対象とします。
EOF
command :list do |c|
c.desc 'すべてのファイルを表示(隠しファイル含む)'
c.switch [:a, :all]
c.desc '詳細表示'
c.switch [:l, :long]
c.desc '対象ディレクトリ'
c.flag [:d, :directory], default_value: '.'
c.action do |global_options, options, args|
directory = options[:directory]
show_all = options[:all]
long_format = options[:long]
puts "ディレクトリ: #{directory}"
puts "すべて表示: #{show_all ? 'はい' : 'いいえ'}"
puts "詳細表示: #{long_format ? 'はい' : 'いいえ'}"
# ファイル一覧の取得と表示
pattern = show_all ? File.join(directory, '*') : File.join(directory, '[^.]*')
files = Dir.glob(pattern)
files.each do |file|
if long_format
stat = File.stat(file)
puts "#{File.basename(file)}\t#{stat.size}bytes\t#{stat.mtime}"
else
puts File.basename(file)
end
end
end
end
# copyコマンド
desc 'ファイルをコピー'
arg_name 'source destination'
command :copy do |c|
c.desc '強制上書き'
c.switch [:f, :force]
c.desc '再帰的コピー'
c.switch [:r, :recursive]
c.action do |global_options, options, args|
if args.length != 2
raise 'コピー元とコピー先の両方を指定してください'
end
source, destination = args
force = options[:force]
recursive = options[:recursive]
puts "コピー中: #{source} -> #{destination}"
puts "強制モード: #{force ? 'はい' : 'いいえ'}"
puts "再帰モード: #{recursive ? 'はい' : 'いいえ'}"
# コピー処理の実装
if File.directory?(source) && recursive
FileUtils.cp_r(source, destination, force: force)
elsif File.file?(source)
if force || !File.exist?(destination)
FileUtils.cp(source, destination)
else
puts "エラー: #{destination} は既に存在します。--force を使用してください。"
exit 1
end
else
puts "エラー: #{source} が見つかりません。"
exit 1
end
puts "コピー完了"
end
end
# deleteコマンド
desc 'ファイルを削除'
arg_name 'files...'
command :delete do |c|
c.desc '確認なしで削除'
c.switch [:f, :force]
c.desc '再帰的削除'
c.switch [:r, :recursive]
c.action do |global_options, options, args|
if args.empty?
raise '削除するファイルを指定してください'
end
force = options[:force]
recursive = options[:recursive]
args.each do |file|
unless File.exist?(file)
puts "警告: #{file} が見つかりません"
next
end
# 確認プロンプト
unless force
print "#{file} を削除しますか? [y/N]: "
answer = STDIN.gets.chomp.downcase
next unless answer == 'y' || answer == 'yes'
end
begin
if File.directory?(file) && recursive
FileUtils.rm_rf(file)
elsif File.file?(file)
FileUtils.rm(file)
else
puts "エラー: #{file} はディレクトリです。-r オプションを使用してください。"
next
end
puts "削除完了: #{file}"
rescue => e
puts "エラー: #{file} の削除に失敗しました - #{e.message}"
end
end
end
end
# pre/postフック
pre do |global, command, options, args|
if global[:log]
puts "コマンド実行: #{command.name}"
puts "グローバルオプション: #{global}"
puts "コマンドオプション: #{options}"
puts "引数: #{args}"
end
# 設定ファイルの読み込み
config_file = File.expand_path(global[:config])
if File.exist?(config_file)
puts "設定ファイル読み込み: #{config_file}" if global[:debug]
end
true # 処理を続行
end
post do |global, command, options, args|
puts "コマンド '#{command.name}' の実行が完了しました" if global[:log]
end
# エラーハンドリング
on_error do |exception|
puts "エラーが発生しました: #{exception.message}"
true # GLIにエラーハンドリングを委譲
end
exit run(ARGV)
より高度な例(サブコマンド階層)
#!/usr/bin/env ruby
require 'gli'
require 'json'
require 'yaml'
include GLI::App
program_desc 'プロジェクト管理ツール'
version '2.0.0'
# グローバルオプション
desc '詳細出力'
switch [:v, :verbose]
desc '設定ファイル'
flag [:c, :config], default_value: '~/.project-manager.yml'
# プロジェクト管理コマンドグループ
desc 'プロジェクト管理'
command :project do |c|
# project create サブコマンド
c.desc 'プロジェクトを作成'
c.long_desc <<-EOF
新しいプロジェクトを作成します。
テンプレートから基本構造を生成し、必要な設定ファイルを作成します。
EOF
c.command :create do |create|
create.desc 'プロジェクト名'
create.flag [:n, :name], required: true
create.desc 'プロジェクトタイプ'
create.flag [:t, :type], default_value: 'web', must_match: %w[web api cli library]
create.desc 'テンプレート'
create.flag [:template], default_value: 'default'
create.desc '作成先ディレクトリ'
create.flag [:d, :directory], default_value: '.'
create.action do |global, options, args|
name = options[:name]
type = options[:type]
template = options[:template]
directory = options[:directory]
project_path = File.join(directory, name)
if File.exist?(project_path)
puts "エラー: プロジェクト '#{name}' は既に存在します"
exit 1
end
puts "プロジェクトを作成中: #{name}" if global[:verbose]
puts " タイプ: #{type}"
puts " テンプレート: #{template}"
puts " パス: #{project_path}"
# プロジェクト構造の作成
FileUtils.mkdir_p(project_path)
case type
when 'web'
create_web_project(project_path, name, template)
when 'api'
create_api_project(project_path, name, template)
when 'cli'
create_cli_project(project_path, name, template)
when 'library'
create_library_project(project_path, name, template)
end
puts "プロジェクト '#{name}' が正常に作成されました"
end
end
# project list サブコマンド
c.desc 'プロジェクト一覧を表示'
c.command :list do |list|
list.desc 'JSONフォーマットで出力'
list.switch [:json]
list.desc 'プロジェクトディレクトリ'
list.flag [:d, :directory], default_value: '.'
list.action do |global, options, args|
directory = options[:directory]
json_format = options[:json]
projects = find_projects(directory)
if json_format
puts JSON.pretty_generate(projects)
else
puts "プロジェクト一覧 (#{directory}):"
projects.each do |project|
puts " #{project[:name]} (#{project[:type]}) - #{project[:path]}"
end
end
end
end
# project delete サブコマンド
c.desc 'プロジェクトを削除'
c.command :delete do |delete|
delete.desc 'プロジェクト名'
delete.flag [:n, :name], required: true
delete.desc '確認なしで削除'
delete.switch [:f, :force]
delete.action do |global, options, args|
name = options[:name]
force = options[:force]
project_path = File.join('.', name)
unless File.exist?(project_path)
puts "エラー: プロジェクト '#{name}' が見つかりません"
exit 1
end
unless force
print "プロジェクト '#{name}' を削除しますか? [y/N]: "
answer = STDIN.gets.chomp.downcase
unless answer == 'y' || answer == 'yes'
puts "削除をキャンセルしました"
exit 0
end
end
FileUtils.rm_rf(project_path)
puts "プロジェクト '#{name}' を削除しました"
end
end
end
# 設定管理コマンドグループ
desc '設定管理'
command :config do |c|
c.desc '設定を表示'
c.command :show do |show|
show.action do |global, options, args|
config = load_config(global[:config])
puts YAML.dump(config)
end
end
c.desc '設定値を設定'
c.arg_name 'key value'
c.command :set do |set|
set.action do |global, options, args|
if args.length != 2
raise 'キーと値の両方を指定してください'
end
key, value = args
config = load_config(global[:config])
# ネストしたキーをサポート (例: database.host)
keys = key.split('.')
current = config
keys[0..-2].each do |k|
current[k] ||= {}
current = current[k]
end
current[keys.last] = value
save_config(global[:config], config)
puts "設定を更新しました: #{key} = #{value}"
end
end
c.desc '設定値を取得'
c.arg_name 'key'
c.command :get do |get|
get.action do |global, options, args|
if args.empty?
raise 'キーを指定してください'
end
key = args.first
config = load_config(global[:config])
keys = key.split('.')
value = keys.reduce(config) { |hash, k| hash[k] if hash }
if value
puts "#{key}: #{value}"
else
puts "設定が見つかりません: #{key}"
exit 1
end
end
end
end
# ヘルパーメソッド
def create_web_project(path, name, template)
FileUtils.mkdir_p("#{path}/src")
FileUtils.mkdir_p("#{path}/public")
FileUtils.mkdir_p("#{path}/tests")
File.write("#{path}/package.json", {
name: name,
version: "1.0.0",
description: "Web project created with project manager",
main: "src/index.js"
}.to_json)
File.write("#{path}/README.md", "# #{name}\n\nWeb project created with project manager\n")
end
def create_api_project(path, name, template)
FileUtils.mkdir_p("#{path}/src")
FileUtils.mkdir_p("#{path}/config")
FileUtils.mkdir_p("#{path}/tests")
File.write("#{path}/Gemfile", "source 'https://rubygems.org'\n\ngem 'sinatra'\ngem 'rspec'\n")
File.write("#{path}/README.md", "# #{name}\n\nAPI project created with project manager\n")
end
def create_cli_project(path, name, template)
FileUtils.mkdir_p("#{path}/lib")
FileUtils.mkdir_p("#{path}/bin")
FileUtils.mkdir_p("#{path}/tests")
File.write("#{path}/#{name}.gemspec", "# #{name}.gemspec\nGem::Specification.new do |s|\n s.name = '#{name}'\n s.version = '1.0.0'\nend\n")
File.write("#{path}/README.md", "# #{name}\n\nCLI project created with project manager\n")
end
def create_library_project(path, name, template)
FileUtils.mkdir_p("#{path}/lib")
FileUtils.mkdir_p("#{path}/spec")
File.write("#{path}/#{name}.gemspec", "# #{name}.gemspec\nGem::Specification.new do |s|\n s.name = '#{name}'\n s.version = '1.0.0'\nend\n")
File.write("#{path}/README.md", "# #{name}\n\nLibrary project created with project manager\n")
end
def find_projects(directory)
projects = []
Dir.glob("#{directory}/*").each do |path|
next unless File.directory?(path)
name = File.basename(path)
type = detect_project_type(path)
projects << {
name: name,
type: type,
path: path
}
end
projects
end
def detect_project_type(path)
return 'web' if File.exist?("#{path}/package.json")
return 'ruby' if File.exist?("#{path}/Gemfile")
return 'cli' if File.exist?("#{path}/bin")
'unknown'
end
def load_config(config_path)
expanded_path = File.expand_path(config_path)
return {} unless File.exist?(expanded_path)
YAML.load_file(expanded_path) || {}
rescue
{}
end
def save_config(config_path, config)
expanded_path = File.expand_path(config_path)
FileUtils.mkdir_p(File.dirname(expanded_path))
File.write(expanded_path, YAML.dump(config))
end
# エラーハンドリング
on_error do |exception|
case exception
when GLI::BadCommandLine
puts "コマンドラインエラー: #{exception.message}"
puts "help コマンドを実行してください"
else
puts "エラー: #{exception.message}"
end
true
end
exit run(ARGV)
実行例
# 基本的なファイル操作
./filemanager list --all --long --directory /tmp
./filemanager copy source.txt destination.txt --force
./filemanager delete old_file.txt --force
# プロジェクト管理
./projectmanager project create --name myapp --type web --template react
./projectmanager project list --json
./projectmanager project delete --name myapp
# 設定管理
./projectmanager config set database.host localhost
./projectmanager config get database.host
./projectmanager config show