ぴよログ

↓に移転したのでこっちは更新されません、多分。

Rubyの知られざるコマンドラインオプション

移転しました →

知られざるとか言ってるけど自分が知らなかっただけです。

グローバル変数やらコマンドラインオプションやらを駆使して、10数行あるコードを最終的には1行にまで圧縮するという技をやってのけている記事がめっちゃ面白かったので、登場したオプションなんかを整理してみようと思います。

Using Ruby command line options by Arjan van der GaagUsing Ruby command line options by Arjan van der Gaagはてなブックマーク - Using Ruby command line options by Arjan van der Gaag

記事のネタ

  1. あるCSVを読み込んで
  2. コメント行と条件を満たさない行を除外して
  3. 区切り文字を変えて出力

というお題です。

before

#!/usr/bin/env ruby -w
# This tranforms input files that look like CSV and strips comments and
# filters out every line not about "Suriname".
 
# Define some basic variables that control how records and fields
# are defined.
input_record_separator  = "\n"
field_separator         = ','
output_record_separator = "\n"
output_field_separator  = ';'
filename = ARGV[0]
 
File.open(filename, 'r+') do |f|
 
  # Read the entire contents of the file in question
  # in an input array.
  input = f.readlines(input_record_separator)
  output = ''
 
  # Loop over all the lines in the file with a counter
  input.each_with_index do |last_read_line, i|
 
    # Remove the ending newline from the line for easier
    # processing.
    last_read_line.chomp!(input_record_separator)
 
    # Extract all fields in this record.
    fields = last_read_line.split(field_separator)
 
    # Only proceed for non-comment lines about Suriname
    if fields[5] == 'Suriname' && !(last_read_line =~ /^# /)
 
      # Write the output lines including the line number
      # and combine fields using our custom separator
      fields.unshift i
      output << fields.join(output_field_separator)
      output << output_record_separator
    end
  end
 
  # Rewind back to the start of the file and replace all its
  # contents with the content in `output`.
  f.rewind
  f.write output
  f.flush
  f.truncate(f.pos)
end

after

#!/usr/bin/env ruby -w -n -i -F, -l -a
BEGIN { $, = ';' }
print $., *$F unless $F[5] != 'Suriname' || /^# /

なにがどうなったの?

afterの方を擬似コードだと思って読んでみると、「index==5の要素の条件に合わない」ときや「正規表現にマッチしない」とき「でなければ」何かをprintするという処理になっているのがわかります。

それ以外の、ファイルを読んだり行をループしたりカンマでsplitしたりという処理は全て、コマンドラインオプションや内部で使われるグローバル変数で処理されてしまっているということになります。

具体的に見てみる

グローバル変数

Rubyにはシステム内部で使われるグローバル変数があります。

Ruby Programming/Syntax/Variables and Constants - Wikibooks, open books for an open world

例えば

  • $/ 入力データの行セパレータ
  • $\ 出力データの行セパレータ
  • $, 出力行のセパレータ

などなどいろいろあります。

before/afterであれだけ省略できたのはこのあたりのグローバル変数をうまく使ったから、というのが一つです。beforeのコードにあったセパレータなど変数がafterのコードでほとんどなくなっているのは、コマンドラインの指定により暗黙的にRubyのデフォルト値を使ったからなんですね〜。

コマンドラインオプション

beforeがruby -wであるのに対し、afterではruby -w -n -i -F, -l -aとオプションがかなり増えています。変わった分を見てみます。

-n

入力を読んで処理をループするような部分を省略して書くことができるようになります。

ファイル各行をputsするだけのプログラムを書くとしたらこうなりますが、

while gets
  puts $_
end

-nオプションをつけることでこうなってしまいます。ちなみに$_は最後に読み込んだ行を表すRubyグローバル変数

puts $_

-i

-iオプションを使うと標準出力への書き込み処理をファイル対しての処理にしてくれます。

例えば各行に"hoge"と追加して出力するだけのプログラムがあるとします。

# main.rb
puts "hoge #{$_}"

-nオプションをつけて普通に呼び出すと標準出力に結果が出ます。これは想定通り。

$ ruby -w -n main.rb data.csv

これに-iをつけることで元ファイルが標準出力先のような感じになり上書きされます。読み込みと書き込みを同時に行えるようになるわけです。

ちなみに-iのあとに文字列を続けて入力すると、元ファイル名+付加した文字列のファイルが書き出し先になります。-i.bakのような感じ。

-F

-Fの直後に指定した文字を入力行のデフォルトセパレータとして用いるというオプションです。例のケースではCSVなのでカンマ,を指定します。

-l

このオプションをつけると入力の際はからは行区切り(改行)を除去し、出力の際は行区切りを付加するという処理になります。データ処理に必ず必要な改行文字の処理を代わりにやってくれるというわけ。

-a

入力行を自動でSplitするためのオプションです。コードで書くとこう。

$F = $_.split

ここで$Fが出てきました。$FにはSplit後の文字列が入っていたということです。そしてこのSplitに用いられる文字は-Fオプションで指定したものです。

まとめ

ファイルを読み込んで処理して出力するという定番の処理のためにこんなコマンドラインオプションが用意されているらしい、ということでまとめてみました。こんなん覚えらんねーよと思いましたが、知ってるとちょっとだけドヤれるかも。