rubyのワンライナーに見る驚きの省略記法

rubyには、省略されたコードが隠されていることがある。その省略されたコードをちゃんと理解しておかないと、rubyの中で何が起こっているのか?見失ってしまう...。調べてみた。

  • 一般的なソースコードの中では、可能な限り省略せずに書いた方が良いと思われる。
  • きっと、他人を悩ますか、1カ月後の別人の自分が悩む。
  • しかし、直接タイプすることが多いワンライナーでは、素早く、簡潔に入力できる省略表現は便利である。
  • 無駄に$や!を入力しないので、エスケープの問題で悩むことも少なくなると思われる。

作業環境

rubyコマンドのオプション基本

  • テスト用にtest.txtを作った。
cat << EOS > test.txt
りんご 200
みかん 100
メロン 500
EOS
  • test.txtを出力してみる。
    • getsは与えられたtest.txtを1行ずつ読み取る。
    • 読み取ったデータは、変数$_に代入される。
    • printで$_を出力する。
$ ruby -e 'while gets; print $_; end' test.txt
りんご 200
みかん 100
メロン 500
  • -nオプションは、while gets 〜 endループを作る。
$ ruby -ne 'print $_' test.txt
りんご 200
みかん 100
メロン 500
  • -pオプションは、そのループの最後にprint $_する。
$ ruby -pe '' test.txt
りんご 200
みかん 100
メロン 500
  • 何もrubyコード書いてないのに、-pオプションだけで出力されるなんて素敵!

区切り文字で区切る

  • -aオプションは、読み込みながらsplitを実行する。
$ ruby -a -ne 'p $F' test.txt
["りんご", "200"]
["みかん", "100"]
["メロン", "500"]
  • でも、区切り文字が空白文字(スペース・タブ・改行*1)以外だと...
cat << EOS > test.txt
りんご;200
みかん;100
メロン;500
EOS
  • splitはうまく区切れない。
$ ruby -a -ne 'p $F' test.txt
["りんご;200"]
["みかん;100"]
["メロン;500"]
  • そんな時は、-Fオプションで区切り文字を指定する。
$ ruby -aF';' -ne 'p $F' test.txt
["りんご", "200\n"]
["みかん", "100\n"]
["メロン", "500\n"]
  • 末尾の改行が邪魔な時は、-lオプションを指定する。
$ ruby -alF';' -ne 'p $F' test.txt
["りんご", "200"]
["みかん", "100"]
["メロン", "500"]
  • 省略しないと、こんな感じだろうか。
$ ruby -e 'while gets; $F = $_.chop.split(";"); p $F; end' test.txt
["りんご", "200"]
["みかん", "100"]
["メロン", "500"]
  • chompでなく、chopだった
  • それから「$\を$/と同じ値に設定し、printでの出力時に改行を付加する」処理もあるそうだ。

入力ファイルへの上書き

  • -iオプションを指定すると、ファイルに上書きする。
$ ruby -i -alF';' -ne 'p $F' test.txt

$ cat test.txt
["りんご", "200"]
["みかん", "100"]
["メロン", "500"]
  • いや、まだ、このテストは続くので上書きは困る。
cat << EOS > test.txt
りんご;200
みかん;100
メロン;500
EOS

入力ファイルをバックアップしてから上書き

  • そんな時は、-iオプションにバックアップ拡張子を指定することもできる。
$ ruby -i'.back' -alF';' -ne 'p $F' test.txt

$ cat test.txt
["りんご", "200"]
["みかん", "100"]
["メロン", "500"]

$ cat test.txt.back
りんご;200
みかん;100
メロン;500
  • test.txt.backにバックアップしてから、test.txtに上書きされるのだ。
  • test.txtを元に戻しておく。
$ mv -f test.txt.back test.txt

$ cat test.txt
りんご;200
みかん;100
メロン;500

BEGINとENDブロック

  • 金額の集計をしてみる。
$ ruby -alF';' -ne 'BEGIN{total=0}; total += $F.last.to_i; END{puts total}' test.txt
800
$ ruby -e 'total = 0; while gets; $F = $_.split(";"); total += $F.last.to_i; end; puts total;' test.txt
  • 分かりやすく改行して表現すると、こんな感じ。
$ ruby -e '
total = 0

while gets
  $F = $_.split(";")
  total += $F.last.to_i
end

puts total
' test.txt

「ん」を含む行を取り出す

  • -nオプションを指定して、一般的には以下のように書く。
$ ruby -ne 'print $_ if $_ =~ /ん/' test.txt
りんご;200
みかん;100
  • ところで、rubyでは$_を省略できる場合がある。
    • 引数を省略したprintは、その引数に$_を補う。
    • 正規表現の比較文字列が省略された時も、$_と比較する。
  • まさしく、上記コードは$_を削除できるケース。
$ ruby -ne 'print if /ん/' test.txt
りんご;200
みかん;100

2〜3行目を取り出す

  • $_省略の応用かもしれないけど、範囲指定した行を取り出すこともできる。
$ ruby -ne 'print if 2..3' test.txt
みかん;100
メロン;500
  • 本来は、範囲指定の条件式は以下のように書くので...
$ ruby -e '5.times {|n| puts n if (n == 2)..(n == 3)}'
2
3
  • よって、省略せずに書くと以下コードになると思う。
    • $.には、処理中の行番号が代入されている。
$ ruby -ne 'print $_ if ($. == 2)..($. == 3)' test.txt
みかん;100
メロン;500

みかんをオレンジに置き換える

  • -pオプションを指定して、普通に書くと以下のようになった。
 $ ruby -pe '$_.sub!(/みかん/, "オレンジ")' test.txt
 りんご;200
 オレンジ;100
 メロン;500
  • ところで、例によって、$_はこの場合も省略できる。
  • しかも、$_を省略した場合は、sub!でなくsubのみで破壊的メソッドと同等の扱いを受けるのだ!
 $ ruby -pe 'sub(/みかん/, "オレンジ")' test.txt
 りんご;200
 オレンジ;100
 メロン;500
  • 逆に、$_を省略して、破壊的sub!にしてしまうと、エラーになる。
 $ ruby -pe 'sub!(/みかん/, "オレンジ")' test.txt
 -e:1:in `
': undefined method `sub!' for main:Object (NoMethodError)

つまり、$_を省略するなら、破壊的メソッドは使ってはいけない。

$_を省略した時の連続処理

  • タブ区切りにして、末尾に円を付加してみる。
  • 個別に実行する例:
 $ ruby -pe 'gsub(/;/, "\t"); gsub(/(\d+$)/, "\\1円")' test.txt
 りんご	200円
 みかん	100円
 メロン	500円
  • 連続して実行する例:
    • 最初はgsubだが、2個目は破壊的gsub!にしておく必要がある。
 $ ruby -pe 'gsub(/;/, "\t").gsub!(/(\d+$)/, "\\1円")' test.txt
 りんご	200円
 みかん	100円
 メロン	500円
    • 2個目もgsubだと(破壊的gsub!でないと)、円が出力されない。
 $ ruby -pe 'gsub(/;/, "\t").gsub(/(\d+$)/, "\\1円")' test.txt
 りんご	200
 みかん	100
 メロン	500

まとめ

  • それでは、なんでもかんでも$_を省略できるかというと、そうではない。
  • 例えば、stripなんて使えると便利そうだが、$_を省略するとエラーになる。
 $ echo ' hello world! ' | ruby -pe 'strip'
 -e:1:in `
': undefined local variable or method `strip' for main:Object (NameError) $ echo ' hello world! ' | ruby -pe '$_.strip!' hello world!
省略できる場合

自分が試した限り、以下の省略が可能だった。(他にもあるかもしれない)

  • sub!gsub!chomp!chop!print正規表現との比較で、$_を省略可能。
  • 範囲指定の条件式で、$.を省略可能。
$ echo ' hello world! ' | ruby -pe '$_.chomp!; $_.gsub!(/^\s+|\s+$/,""); $_.chop!; $_.sub!(/^./){|m| m.upcase}'
$ echo ' hello world! ' | ruby -pe '   chomp ;    gsub (/^\s+|\s+$/,"");    chop ;    sub (/^./){|m| m.upcase}'
Hello world
$ ruby -ne 'print $_ if $_ =~ /ん/' test.txt
$ ruby -ne 'print    if       /ん/' test.txt
りんご;200
みかん;100
    • 範囲指定の条件式 での省略例
$ ruby -ne 'print $_ if ($. == 2)..($. == 3)' test.txt
$ ruby -ne 'print    if        2 ..       3 ' test.txt
みかん;100
メロン;500
rubyのオプション一覧
  • この日記で実際に試したオプションの一覧
rubyコマンド 実行されるコードまたは動作
ruby -e 'rubyコード' rubyコード
ruby -ne 'rubyコード' while gets; rubyコード; end
ruby -pe 'rubyコード' while gets; rubyコード; print; end
ruby -a -ne 'rubyコード' while gets; $F = $_.split; rubyコード; end
ruby -aF';' -ne 'rubyコード' while gets; $F = $_.split(";"); rubyコード; end
ruby -alF';' -ne 'rubyコード' while gets; $F = $_.chop.split(";"); rubyコード; end
ruby -ne 'BEGIN{total=0}; rubyコード; END{puts total}' total=0; while gets; rubyコード; end; puts total
ruby -i -ne 'rubyコード' test.txt test.txtを読み込んで、rubyコードの処理をして、test.txtに上書き
ruby -i'.back' -ne 'rubyコード' test.txt test.txtを読み込んで、rubyコードの処理をして、test.txt.backにバックアップしてから、test.txtに上書き
getsと連携する変数
  • この日記で実際に試した省略可能な組込変数
$_ getsで読み込んだ内容が1行ごとに代入される
$. getsで読み込み中の行番号が代入されている

エスケープ問題の解決

  • 突然ではあるが、以下のコード...ruby -pe に続けて、シングルクォートを取り除くワンライナーを完成できるだろうか?
    • 出力結果が「私はzariganiです。」となればOK。
 $ echo "私は'zarigani'です。" | ruby -pe 
bashとrubyの連携に見るエスケープな悩み - ザリガニが見ていた...。
  • という訳で、前回の問題の最もシンプルな表現は、こうなる。
$ echo "私は'zarigani'です。" | ruby -pe "gsub(/'/,'')"
私はzariganiです。
  • 省略記法によって、ダブルクォート内にそのままrubyコードを書けるようになった。

*1:もう少し正確には、正規表現の\sである。(=[ \t\r\n\f]、=スペース、タブ、CR改行、LF改行、改ページ)