Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで本格的なコマンドラインアプリケーションを構築する

ちょっとしたユーティリティスクリプトを書くのは楽しいものですが、時には引数を受け取り、入力・出力・エラー報告などでUnixの慣習に従った、正真正銘のコマンドラインアプリケーションを作る必要があります。

幸い、Rubyにはコマンドラインアプリを比較的容易に作るためのすべての構成要素が揃っています。この記事では、特定のgemを使った「Xを実現する方法」という断片的なアプローチを超え、一流のコマンドラインアプリを構成するすべてのピースを広く概観します。

入力: 環境変数

環境変数は、しばらくの間保持しておきたい短い設定値に通常使用されます。APIキーが好例です。

環境変数の設定

ユーザーは通常、bashのようなシェルを通じて環境変数を設定します。特定のセッションのみで有効にする場合:

$ AWS_ACCESS_KEY_ID=FOO

現在のセッションおよび新しいbashセッションで有効にする場合(export):

$ export AWS_ACCESS_KEY_ID=FOO

単一のプログラム実行時のみ有効にする場合(envコマンド):

$ env AWS_ACCESS_KEY_ID=FOO aws s3 mb s3://mybucket

環境変数の読み取り

ユーザーがどの方法で環境変数を設定したかに関わらず、RubyのENVハッシュから読み取れます。

key_id = ENV["AWS_ACCESS_KEY_ID"]

環境変数は内部でどう動いているか?

すべてのプロセスには、環境変数のテーブルが関連付けられています。子プロセスを生成するとき、親プロセスのテーブルのコピーが子に渡され、親が望む変更が加えられます。

わかりにくいですか?

より平易な解説は、拙ブログ記事「Rubyistのための環境変数ガイド」を参照してください。

コマンドライン引数

ターミナルでプログラム名の後に指定するものがすべてコマンドライン引数です:

$ echo foo bar baz

上記では「foo」「bar」「baz」がすべてコマンドライン引数です。

より実践的な例:

$ honeybadger deploy -v --environment staging

しかしここでも、引数は単なるテキストです。--environment stagingに意味を持たせたいなら、自分で解釈する必要があります。

コマンドライン引数の渡し方

すでに見たように、以下のように渡せます:

$ echo foo bar baz

Rubyはインタプリタ言語なので、以下の形式にも遭遇するでしょう:

$ ruby myprog.rb foo

OSから見れば、「ruby」というプログラムを実行し、2つの引数を渡しています。しかしRubyは賢く、「foo」があなたのプログラム向けの引数だと判断します。したがって、あなたのプログラムは引数"foo"を1つだけ受け取ります。

コマンドライン引数の読み取り

コマンドライン引数は、ARGVという配列に格納されます。グローバル変数なので、プログラムのどこからでもアクセス可能です。

以下の例ではARGVの中身をすべて出力しています。コピペしてターミナルで試してみてください。

$ ruby -e "puts ARGV.inspect" foo bar baz
["foo", "bar", "baz"]

ruby -eの部分に惑わされないでください。プログラムと実行結果を1行で示すために使っているだけです。

コマンドライン引数のパース

引数はただのテキストです。意味を持たせるにはパースが必要です。幸い、優れたライブラリがいくつかあります。

長年のうちに、ある程度標準的な構文が確立されました:

$ program -a --option foo

このスタイルでは、-hでヘルプ表示といったブールフラグや、値を伴うオプションが扱えます。

OptionParserの紹介

OptionParserクラスはRubyの標準ライブラリに含まれ、上記スタイルのオプションを簡単にパースできます。

シンプルな挨拶アプリを作り、名前をコマンドライン引数で指定できるようにしてみましょう。

require 'optparse'

# パースしたオプションを保持するハッシュ
options = {}

OptionParser.new do |parser|

  # -n または --name が引数とともに現れたら、その引数を保存
  parser.on("-n", "--name NAME", "挨拶する相手の名前") do |v|
    options[:name] = v
  end
end.parse!

# オプションハッシュを自由に利用
puts "Hello #{ options[:name] }"  if options[:name]

実行すると期待通り動作します:

$ ruby hello.rb --name Starr
Hello Starr

$ ruby hello.rb -n Starr
Hello Starr

ヘルプ画面の追加

ヘルプ機能の追加も簡単です。テキストを用意し、-hコマンドを追加するだけ:

OptionParser.new do |parser|
  parser.banner = "Usage: hello.rb [options]"

  parser.on("-h", "--help", "このヘルプを表示") do
    puts parser
  end

  ...
end.parse!

これでヘルプが表示できます:

$ ruby hello.rb -h
Usage: hello.rb [options]
    -h, --help                       このヘルプを表示
    -n, --name NAME                  挨拶する相手の名前

引数の型変換

すべてのコマンドライン引数は文字列ですが、数値や日付として受け取りたいこともあります。手動で変換するのは面倒なので、OptionParserが自動でやってくれます。

以下では、挨拶を繰り返す回数を指定するcountオプションを追加し、Integerへのキャストを指定しています。

OptionParser.new do |parser|

  ...

  # `Integer`引数で、値を整数にキャストするよう指示
  # `Float`や`Date`なども指定可能
  parser.on("-c", "--count COUNT", Integer, "メッセージをCOUNT回繰り返す") do |v|
    options[:count] = v
  end

end.parse!

if options[:name]
  options.fetch(:count, 1).times do
    puts "Hello #{ options[:name] }"
  end
end

実行すると、指定回数だけ挨拶されます:

$ ruby hello.rb -n Starr -c 5
Hello Starr
Hello Starr
Hello Starr
Hello Starr
Hello Starr

命名規則

プログラミングで最も難しいのは命名と言われます。コマンドライン引数も例外ではありません。参考になるよう、よく使われるフラグとその一般的な意味をまとめました:

フラグ 一般的な意味
-aAll(すべて)、Append(追加)
-dDebug mode(デバッグモード)、またはディレクトリ指定
-eExecute(実行)、またはEdit(編集)
-fFile(ファイル指定)、またはForce(強制実行)
-hHelp(ヘルプ)
-mMessage(メッセージ指定)
-oOutput(出力ファイル/デバイス指定)
-qQuiet mode(静粛モード)
-vVerbose mode(詳細モード)、バージョン表示
-yYes(プロンプトに自動でyes回答)

OptionParserの代替ライブラリ

OptionParserは便利ですが、制約もあります。特に近年普及したコマンドベースの構文(サブコマンド)をサポートしていません:

$ myprog command subcommand -V

幸い、オプションパースライブラリは山ほどあります。お気に入りが見つかるはずです。ここでは特に興味深い3つを紹介します:

  • GLI - 「Git-like Interface」の名の通り、gitのように単一実行ファイルで複数のコマンドを持つアプリを容易に作れます。
  • CRI - ネストしたコマンドに対応した、使いやすいコマンドラインツール構築ライブラリです。
  • Methadone - 独自のオプションパースDSLを提供するだけでなく、ディレクトリ構造、テストスイート、ロギングまでセットアップしてくれます。CLI版Railsのような存在です。

大量データの入力: STDIN

コマンドライン引数は大量データの入力に向きません。そんなときはIOストリーム、中でもSTDINが最も手軽です。

ほとんどのプログラムは、OSから自動的にSTDINを割り当てられます。これはOSがアプリにデータを送るための読み取り専用ファイルのようなものです。キーボード入力を受け取るのにも使えますが、より重要なのは、パイプを通じて他のプログラムからデータを受信できる点です。

STDINは読み取り専用ファイルと同様に扱えます。ここではテキストをパイプでSTDINに渡し、readメソッドで取得しています。

$ echo "hello world" | ruby -e "puts STDIN.read.upcase"
HELLO WORLD

RubyではIOオブジェクトにEnumerableの機能が使え、STDINも例外ではありません。そのため、こんな芸当も可能です:

# 最初の20行を取得
STDIN.first(20)

# 整数に変換し、奇数を除外
STDIN.map(&:to_i).reject(&:odd)

# などなど

結果の出力: STDOUT

STDOUTは、OSがプログラムに割り当てる書き込み専用のIOストリームです。

STDOUTに書き込むことで、ユーザーのターミナルにテキストを表示できます。ユーザーはこれをファイルにリダイレクトしたり、別のプログラムにパイプしたりできます。

通常の書き込み専用ファイルと同じように使えます:

STDOUT.write("Hi!\n")
# Hi!

もちろん、putsprintもSTDOUTに出力します。

ステータス情報の出力: STDERR

STDERRも書き込み専用のIOストリームですが、一般的な出力用ではありません。ステータスメッセージ専用で、本物の出力の邪魔にならないように分離されています。

ユーザーがSTDOUTをファイルや別プログラムにリダイレクトしていても、STDERRは通常ターミナルに表示されます。

以下の例では、curlでウェブページを取得しています。ページの内容はSTDOUTに、進捗情報はSTDERRに出力されます:

$ curl "https://www.google.com/" > /tmp/g.html
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  151k    0  151k    0     0   277k      0 --:--:-- --:--:-- --:--:--  277k

自分のプログラムからSTDERRに書き込むのも簡単です。IOオブジェクトとして扱うだけ:

STDERR.write("blah\n")
STDERR.puts("blah")

見た目を整える

ここまでの例はデザイン賞を取れるものではありません。しかし、コマンドラインアプリが醜かったり、非インタラクティブだったりする理由はありません。

アプリを洗練させたいなら、重労働を肩代わりしてくれる優れたgemがたくさんあります。特におすすめの3つ:

  • highline - ユーザー入力の収集、検証、型変換の手間を大幅に省いてくれる優秀なライブラリです。
  • command_line_reporter - ASCIIベースの進捗レポート生成を容易にします。
  • paint - ANSIカラーコードを簡単に追加し、味気ないテキストに色を付けられます。

より包括的なリストはこちらを参照してください。

終了ステータス

プログラムがエラーで終了する場合、OSに終了コードで知らせるべきです。これにより、以下のようなbashコードが機能します:

$ prog1 && prog2

bashの&&演算子を知らない方のために説明すると、「prog1が正常終了した場合のみprog2を実行」という意味です。

Rubyはデフォルトで「成功」コードで終了し、例外発生時は「失敗」コードで終了します。それ以外はあなたが実装する必要がありますが、それは簡単です:

# `exit`に0以外の引数を渡すと失敗扱い
exit(1)

詳細は拙記事「Rubyプログラムを終了する方法」を参照してください。

プロセス名の設定

コマンドラインプログラムが少しの間実行されるなら、pstopでプロセス一覧を見たときに何のプロセスか判別できることが重要です。通常、Rubyプログラムのプロセス名は、実行時に入力したコマンドそのものになります。ruby myapp -vと打てば、それがプロセス名です。

引数が多いと読みにくくなるため、よりフレンドリーなプロセス名を設定したくなります。これは以下のように行えます:

Process.setproctitle("My Awesome Command Line App")

凝りたいなら、プロセス名で現在の処理内容をユーザーに伝えることもできます:

Process.setproctitle("Mail Sender: initializing")
init(...)
Process.setproctitle("Mail Sender: connecting")
connect(...)
Process.setproctitle("Mail Sender: sending")
send(...)

詳細は拙記事「topとpsに表示されるRubyスクリプトのプロセス名を変更する方法」を参照してください。

  1. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端

  2. iPadで使いたいおすすめライティングアプリ8選【執筆スタイル別に徹底解説】

    キーボードを接続したiPadは、持ち運びやすさと使い勝手を兼ね備えた理想的な執筆環境になります。しかし、その性能を最大限に引き出す鍵となるのが、どのライティングアプリを使うかという点です。iPadには標準で「Pages」というワープロアプリが付属していますが、機能が合わなかったり、操作感がしっくりこない場合は、他のアプリを試してみる価値が十分にあります。 本記事では、さまざまな執筆スタイルや目的に合わせて選んだ、iPad向けライティングアプリのベスト8をご紹介します。創作環境づくりから生産性向上まで、自分に合った一本がきっと見つかるはずです。 1. Just Write(ジャスト・ライト)