よりよい Perl, Awk, sed としての Ruby

[Perl正規表現] [Puby more basics]
[2014/06/21新規] [2014/09/12更新]

Contents

主な形式

ruby -[n|p][l]e 'script' [file...]
ruby [-n|-p][-l] script_file [file...]

sed, Awk 風な説明

ruby -n は『レコード』と呼ばれる行を一つずつパターンスペース '$_' に入力する。ruby -p はさらにそのパターンスペース '$_' を出力する。Ruby スクリプトを '-e script''script_file' で指定することにより様々な処理をすることができる。

まず、入力行は sed, Awk とは異なり、Perl 同様、レコードセパレータである改行コード '\n' が取り除かれずにパターンスペース '$_' に入る。但し、'-l' オプションを指定すると、改行コードが取り除かれ、出力のレコードセパレータに改行コード '\n' が設定される。

また、ruby -na は、Awk のように、フィールドセパレータである空白を区切りとして '$F[0]', '$F[1]', '$F[2]', 〜 にその行の『フィールド』群が入る。

sed, Awk 風な Ruby スクリプトの概要

Awk のように 'BEGIN', 'END' のような前処理と、後処理を記す特殊ブロックが使えるが、他はすべて主処理となる。そして、sed, Awk のようなマッチの範囲「式, 式」に処理されるブロックはサポートされないが、条件式で '..' 演算子(2つの式が sed スタイルのときは '...' 演算子)を用いることでそれと似た制御ができる。

例えば以下は、Awk では awk '/^<pre>/,/<\/pre>$/' となる、HTML の 'pre' タグを含むそれに囲まれた行を表示する Ruby スクリプトである。

	ruby -ne 'print if (/^<pre>/../<\/pre>$/)'

ここで、'print''print $_' と等価である。

このように Ruby スクリプトは、オプションにより sed, Awk 風に書けるようになっている。しかし、正規表現による置換についてはその限りではない。

例えば、sed では sed -e 's/&/\&amp;/g;s/</\&lt;/g;s/>/\&gt;/g' となる、ソースコードを HTML にペーストできるように「<」から「&lt;」への変換等を行なう Ruby スクリプトは以下のように書かなければならない。

	ruby -pe 'gsub(/&/,"&amp;"); gsub(/</, "&lt;"); gsub(/>/, "&gt;")'

簡単な説明

ruby -n は『レコード』と呼ばれる行を一つずつパターンスペース '$_' に入力する、以下とほぼ等価なスクリプトとなる。

	ruby -e 'while gets do … end'

これはさらに以下とほぼ等価なスクリプトとなる。

	ruby -e '
ARGV.unshift("-") unless ARGV.length > 0
while (filename = ARGV.shift) do
  argf = filename == "-" ? STDIN : open(filename)
  while ($_ = argf.gets) do
    …
  end
  argf.close
end'

ruby -p はさらにそのパターンスペース '$_' を出力する、以下とほぼ等価なスクリプトとなる。

	ruby -e 'while gets do …; print end'

これはさらに以下とほぼ等価なスクリプトとなる。

	ruby -e '
ARGV.unshift("-") unless ARGV.length > 0
while (filename = ARGV.shift) do
  argf = filename == "-" ? STDIN : open(filename)
  while ($_ = argf.gets) do
    …
    print $_
  end
  argf.close
end'

まず、入力行は sed, Awk とは異なり、レコードセパレータである改行コード '\n' が取り除かれずにパターンスペース '$_' に入る。但し、'-l' オプションを指定すると、改行コードが取り除かれ、出力のレコードセパレータに改行コード '\n' が設定される、以下とほぼ等価なスクリプトとなる。

	ruby -e 'BEGIN{ $\ = $/ }; while gets do chomp!; … end'

また、ruby -na は、Awk のように、フィールドセパレータである空白を区切りとして '$F[0]', '$F[1]', '$F[2]', 〜 にその行の『フィールド』群が入る、以下とほぼ等価なスクリプトとなる。

	ruby -e 'while gets do $F = $_.split; … end'

このように Ruby は様々な場面で省略可能な引数などのサポートがあり、簡素に書ける反面、一見不明瞭なコードになりがちだが Perl ほどではない。

Ruby スクリプトの概要

例えば先の、HTML の 'pre' タグを含むそれに囲まれた行を表示する Ruby スクリプトは、省略せずに書けば以下のようになる。

	ruby -e '
while $<.gets do
  $stdout.print $_ if ($_ =~ /^<pre>/ .. $_ =~ /<\/pre>$/)
end

例えば先の、ソースコードを HTML にペーストできるように「<」から「&lt;」への変換等を行なう Ruby スクリプトは、省略せずに書けば以下のようになる。

	ruby -e '
while $<.gets do
  $_.gsub!(/&/, "&amp;")
  $_.gsub!(/</, "&lt;")
  $_.gsub!(/>/, "&gt;")
  $stdout.print $_
end'

スクリプトは式のみからなり、プログラミング言語 C などとは異なり、制御構文さえも式である。式には、変数と定数、さまざまなリテラル、それらからなる演算子式、if や while などの制御構造、メソッド呼び出し、クラス/メソッドの定義がある。式と式の区切りは改行か ';' で区切り文となる。ブロック '{''}''do''end' 内の最後の区切り ';' は不要、改行は省略できる。よって、Perl と異なり ';' は改行がある限りまず不要である。

変数、定数、リテラル

以下の変数、定数、リテラルがある。

さて、Ruby には Perl における「リファレンス」などは無い、が、オブジェクトのインスタンス変数へのアクセスによって、それが不要となっている。

詳しくは、Ruby リファレンスマニュアル「変数と定数」, 「リテラル」を参照のこと。

真偽値

Ruby では数値 '0' は「偽」ではない。これは Perl や C/C++ などのプログラマが最初に戸惑う仕様であろう。Ruby での「偽」は 'nil''false' のみで、他のオブジェクトはすべて「真」である。

つまり、Ruby では、なんと '!0' は「真」ではない。これは言語仕様に C/C++ のような「bool 値への暗黙の型変換」が採り入れられていないからであろうが、Perl のように意図せず「偽」になる作用とのトレードオフなのだろう。

変数展開、式展開

文字列リテラルの "…"'%Q!…!' のなかで変数展開を行なうには、変数を '$_' だとして、"#{$_}"'%Q!#{$_}!' のように '#{…}' で囲む必要がある。Perl やシェルと比べて多少面倒だが、展開されるのは変数のみというわけではなく、囲まれた式そのものなので、Perl やシェルよりも高機能である。よって、以下のような式展開もできてしまう。

#!/usr/bin/ruby
a = [ 0, 1, 2, 3, ]
p %Q!#{a.join(",")}\n!	#=> "0,1,2,3\n"

演算子式

以下の演算子式がある。

::スコープ(再定義不可)
[]配列またはハッシュ添字
+ ! ~単項正、論理否定、ビット否定
**二項累乗
-単項負
* / %二項乗、除、法
+ -二項加、減
<< >>ビット左、右シフト
&ビットAND
| ^ビットOR、ビットXOR
> >= < <=関係不等号
<=> == === != =~ !~関係等号、関係不等号、マッチ等号、マッチ不等号
&&論理AND(再定義不可)
||論理OR(再定義不可)
.. ...範囲式(再定義不可)
?:三項条件(再定義不可)
= += -= *= /= %= **= &= |= ^= <<= >>= &&= ||=単純代入、複合代入(再定義不可)
not論理否定(再定義不可)
and or論理AND、論理OR(再定義不可)

特に、Ruby の演算子は Perl よりもむしろ C++ に近く、文字列の連結は Perl のように '.' ではなく '+' 二項加算演算子である。

しかし、多くのプログラミング言語にある ++, -- の前置・後置ともに、Ruby には存在しない。これは設計思想によるもので、慣れれば Ruby では不要であることがわかってくる。とは言え、++, -- の前置・後置は「返値」のあるメソッドによるオブジェクトの破壊的操作と捉えれば導入可能に思えるのだが… すると +=, -= に加えて =+, =- のような新たな演算子も導入可能であろう。

詳しくは、Ruby リファレンスマニュアル「演算子式」を参照のこと。

多重代入

Ruby には C/C++ などにはない「多重代入」なる式がある。Perl のリスト代入 '($a, $b, $c) = (1, 2, 3)' ともまた異なり、Ruby では '$a, $b, $c = 1, 2, 3' というスタイルとなる。

必ず、Ruby リファレンスマニュアル「多重代入」は一読しておいた方がよいだろう。

制御構造

以下の制御構造がある。

ここで、then, do は省略できると公式には書いてあるが、正確には改行を伴う時のみ省略可であることに注意。

特に、'for (i=0; i<10; i++)' のような繰り返し構文がないので、代わりに '10.times do |i| … end' のようにイテレータによるブロック評価で実現する。しかしこれだと、'for (i=10; i>0; i--)' のような降順の繰り返しができないので、降順の場合は '10.downto(1) do |i| … end' のようにイテレータによるブロック評価で実現する。

詳しくは、Ruby リファレンスマニュアル「制御構造」を参照のこと。

正規表現

Ruby の正規表現は Perl とほとんど同じだ。特に Ruby 1.9 以降はほぼ上位互換となっている。そして、Ruby 1.8 以前は名前付きグループはサポートされていないので注意。

但し、Perl における '%-' に対応する機能がサポートされていないようだ。

定数と特殊変数

Ruby における定数と特殊変数は、すべてのクラスのスーパークラス 'Object' の定数や 'Kernel' モジュールの特殊変数がある。

Object クラス

主な定数は以下の通りである。

Kernel モジュール

まず、グローバルスコープの Kernel 特殊変数には主に以下がある。

次に、スレッドローカルスコープの Kernel 特殊変数には主に以下がある。

ちなみに、グローバル変数のスコープには「グローバルスコープ」と「ローカルスコープ」と「スレッドローカルスコープ」がある。

エスケープ文字

Ruby におけるエスケープ文字(バックスラッシュ記法と正規表現におけるメタ文字)は以下の通りである。

このように Ruby 1.9 以降では非常に多くのエスケープ文字がサポートされる。

組み込みライブラリ

Ruby にはクラス、モジュール、オブジェクトにより、組み込みライブラリが標準でサポートされる。

ここで主な組み込みライブラリのリファレンスを列挙しておく。

リンク先のメソッドや定数をよく参考にすること。

以下に '$_' が省略可能なメソッドや式を列挙する。

メソッド 推奨代替 説明
chomp $_.chomp! ('-n'|'-p'オプション時のみ)文字列 '$_' の末尾の改行を除去する Kernel モジュール関数
chop $_.chop! ('-n'|'-p'オプション時のみ)文字列 '$_' の末尾を除去する Kernel モジュール関数
gets $_ = ARGF.gets 'ARGF' から一行を '$_' に読み込み(ファイル終端なら 'nil')、それを返す Kernel モジュール関数
readline $_ = ARGF.readline 'ARGF' から一行を '$_' に読み込み(ファイル終端なら、例外)、それを返す Kernel モジュール関数
print print $_ '$_''$stdout' に出力する Kernel モジュール関数
sub(/…/, '…') $_.sub!(/…/, '…') ('-n'|'-p'オプション時のみ)文字列 '$_' を置換する Kernel モジュール関数
gsub(/…/, '…') $_.gsub!(/…/, '…') ('-n'|'-p'オプション時のみ)文字列 '$_' を大域置換するKernel モジュール関数
if /…/if $_ =~ /…/正規表現リテラルを伴う制御構造の if 条件式
unless /…/unless $_ =~ /…/正規表現リテラルを伴う制御構造の unless 条件式
while /…/while $_ =~ /…/正規表現リテラルを伴う制御構造の while 繰り返し式
until /…/until $_ =~ /…/正規表現リテラルを伴う制御構造の until 繰り返し式

例題

代表的な Unix コマンドに相当する Ruby スクリプトを以下にあげる。

`cat`

	ruby -pe ''

このように、cat と同じ ruby スクリプトは「空」となるが、「ruby -ne 'print'」でもよいし、「ruby -ne 'print $_'」でもよいし、「ruby -e 'print while gets'」でもよいし、「perl -e 'while gets do print end'」でもよい。

[sed] [Awk] [Perl] [Ruby] [Python]

`head -n 1`

	ruby -ne 'print if $. == 1'

このように、head -n 1 と同じ ruby スクリプトは以上のようになるが、「perl -ne 'if ($. == 1) { print; last }'」の方が効率がよいだろう。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 1`

	ruby -ne 'print if $<.eof'

このように、tail -n 1 と同じ ruby スクリプト以上のようになるが、Awk のように「END{ print }」では動作せず、この場合「perl -ne '$b = $_; END{ print $b }'」となる。

[sed] [Awk] [Perl] [Ruby] [Python]
	ruby -ne 'print if !($. > 8)'

このように、head -n 8 と同じ ruby スクリプトは以上のようになるが、「ruby -ne 'if (!($. > 8)) then print else break end'」の方が効率がよいだろう。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 8`

さて、`head -n 1`, `tail -n 1`, `head -n n` は以上のように大変簡単であるが、`tail -n n` は少し工夫が必要であり、以下のようになる。

	ruby -ne '
BEGIN{
  $n = 8
  $a = Array.new($n)
}
$a[$. % $n] = $_
END{
  $n.times {|i| print $a[($.+i+1)%$n] }
}'

ここでは、配列にラウンドロビン的に行を格納していき、最後にそれらを出力している。

このように、tail -n 8 と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -l`

	ruby -ne 'END{ print "#{$.}" }'

このように、wc -l と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -c`

sed では大変面倒になる `wc -c`ruby だと算術演算があるので簡単である。

	ruby -ne '
BEGIN{
  $l = 0
}
$l += $_.length
END{
  print "#{$l}\n"
}'

Perl だと 'BEGIN' ブロックで行なっている変数の初期化は不要なのだが、Ruby だと変数の型を決定する為に必要になる。

このように、wc -c と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -w`

sed では大変面倒になる `wc -w`ruby だと算術演算があるので簡単であるが、少々工夫が必要であり、以下のようになる。

	ruby -ne '
BEGIN{
  $w = 0
}
begin
  h = $_
  while h =~ /[^\t\n ]+/
    $w += 1
    h = $'
  end
end
END{
  print "#{$w}\n"
}'

この "$'" によるマッチの繰り返しは Ruby では大域マッチによりさらに簡単に書けて、以下のようになる。

	ruby -ne '
BEGIN{
  $w = 0
}
begin
  gsub(/[^\t\n ]+/) { $w += 1 }
end
END{
  print "#{$w}\n"
}'

Perl では、置換の手続きが置換の数をカウントしてくれるのでさらに簡単になるのだが、Ruby ではそれはないのでこれ以上簡単にはならないようだ。

このように、wc -w と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep '^$'`

	ruby -ne 'print if ~/^$/'

このように、基本正規表現の grep '^$' と同じ ruby スクリプトは以上のようになるが、Ruby では基本正規表現はサポートされないので、他のパターンではRuby正規表現に書き直す必要がある。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -v '^$'`

	ruby -ne 'print unless ~/^$/'

このように、マッチの否定、基本正規表現の grep -v '^$' と同じ ruby スクリプトは以上のようになるが、Ruby では基本正規表現はサポートされないので、他のパターンではRuby正規表現に書き直す必要がある。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E '^.+'`

	ruby -ne 'print if ~/^.+/'

このように、拡張正規表現の grep -E '^.+' と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E -v '^.+'`

	ruby -ne 'print unless ~/^.+/'

このように、拡張正規表現のマッチの否定、grep -E -v '^.+' と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6`

	ruby -F':' -ane 'begin if ($F.length >= 6) then print $F[0], ":", $F[5], "\n" else print end end'

ここで "-F':'""-a" オプションと共にフィールドセパレータを指定するものである。

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行はそのまま出力と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6 -s`

	ruby -F':' -ane 'begin if ($F.length >= 6) then print $F[0], ":", $F[5], "\n" end end'

ここで "-F':'""-a" オプションと共にフィールドセパレータを指定するものである。

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行は出力しないと同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold -b`

この例、1行あたりの文字数(既定値は80)を越えたら改行を挿入する ruby スクリプトを示そう。まずは正規表現を使わない方法:

	ruby -nle '
BEGIN{
  $w = 80
}
begin
  h = $_
  while h
    if h.length > $w
      print h[0, $w]
      h = h[$w, h.length - $w]
    else
      print h
      h = nil
    end
  end
end'

もしくは、正規表現を使った方法:

	ruby -nle '
BEGIN{
  $w = 80
}
begin
  h = $_
  while h !~ /^.{#{$w}}$/ && h =~ /^.{#{$w}}/
    print $&
    h = $'
  end
  print h
end'

後者の方が少しだけ単純だ。

このように、fold -b と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tee filename`

	ruby -ne 'BEGIN{ $f = open("filename.out", "w") }; begin print; $f.print end; END{ $f.close }'

このように、標準入力を標準出力とファイルに書き出し、tee と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tr 'A-Za-z' 'N-ZA-Mn-za-m'`

この例、ROT13(と呼ばれる暗号化と言うより難読化)は tr コマンドを使うと表題のように簡単に実現できる。そして、ruby には tr コマンドに似た 'y' コマンドがあるので、以下のようになる。

	ruby -pe '$_.tr!("\r", "\n")'

このように、文字置換、tr 'A-Za-z' 'N-ZA-Mn-za-m' と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cat -n`

sed ではいささか面倒になる `cat -n`ruby だと極めて簡単である。

	ruby -ne 'printf("%6d\t%s", $., $_)'

このように、cat -n と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cat -b`

sed では大変面倒になる `cat -n`ruby だと極めて簡単である。しかし、変数の型を決める為の初期化を要するので Perl よりも繁雑になる。

	ruby -pe 'BEGIN{ $i = 0 }; if (!/^$/) then $i += 1; $_ = sprintf("%6d\t%s", $i, $_) end'

このように、cat -b と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`uniq`, `uniq -d`, `uniq -u`

ところで、これらは GNU sedGNU Awk で示される好例となっている。

  1. `uniq` - 重複する行を一行にする in GNU sed
  2. `uniq -d` - 重複する行のみを一行にして表示する in GNU sed
  3. `uniq -u` - 重複しない行のみを表示する in GNU sed
  4. `uniq [-du]` - 以上すべてに対応 in GNU Awk

sed ではいずれも 'N' コマンドで入力行をパターンスペースの末尾に '\n' に続いて追加し、正規表現にて '^\(.*\)\n\1$' のように後方参照 '\1' を利用していることにある。awk においては、基本的には以下に示す例と同等である。

これらを ruby で実現すると、単純に文字列の比較と制御の組み合わせとなり、順に以下のようになる。

	ruby -ne 'if ($. == 1) then h = $_; print else if (h != $_) then h = $_; print end end'
	ruby -ne 'if ($. == 1) then h = $_ else if (h != $_) then h = $_; d = false else print if (!d); d = true end end'
	ruby -ne 'if ($. == 1) then $h = $_ else if ($h != $_) then print $h if (!$d); $h = $_; $d = false else $d = true end end; END{ print $h if (!$d) }'

このように、uniq, uniq -d, uniq -u と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`expand`

このタブを複数の空白に置換するコマンドを ruby スクリプトで示そう。まずは正規表現を使った方法:

	ruby -nle '
BEGIN{
  $\ = nil
  $n = $n ? $n.to_i : 8
  $r = Regexp.new("^([^\t]{0,#{($n-1)}}\t|[^\t]{#{$n}})")
}
begin
  h = $_
  while (h =~ $r)
    u = $&
    h = $'
    p = u.index("\t")
    p = u.length if (!p)
    u = u[0, p]
    ($n-p).times { u += " " }
    print u
  end
  print h, "\n"
end'

もしくは、正規表現を使わない方法:

	ruby -nle '
BEGIN{
  $\ = nil
  $n = 8
}
begin
  l = 0
  $_.length.times do |i|
    c = $_[i, 1]
    d = (c == "\t") ? $n - (l % $n) : 1
    if (c == "\t")
      c = ""
      d.times { c += " " }
    end
    print c
    l += d
  end
  print "\n"
end'

両者の効率と柔軟性におけるメリット・デメリットを考えてみるのも興味深い。個人的には、前者の方が判り易くてよいと思うが、例えば、'\b' が一文字戻るとみなす処理を加えるには後者の方が簡単である。

このように、expand と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`unexpand -a`

この複数の空白をタブに置換するコマンドを ruby スクリプトを示そう。まずは正規表現を使った方法:

	ruby -nle '
BEGIN{
  $\ = nil
  $n = 8
  $r = [
        Regexp.new("^([^\t]{0,#{($n-1)}}\t|[^\t]{#{$n}})"),
        Regexp.new("^([^\t]{#{($n-1)}})\t\$"),
        /^ /,
        / {1,}$/,
        / {2,}$/,
       ]
}
begin
  h = $_
  while (h =~ $r[0])
    u = $&
    h = $'
    if (h =~ $r[2])
      if (u =~ $r[3])
        u = $` + "\t"
      end
    else
      if (u =~ $r[1])
        u = $1 + " "
      end
      if (u =~ $r[4])
        u = $` + "\t"
      end
    end
    print u
  end
  print h, "\n"
end'

ちなみに、最初の 'if' 文は、完全に `unexpand -a` の挙動を再現するためのものである。

もしくは、一文字ずつ調べる方法:

	ruby -nle '
BEGIN{
  $\ = nil
  $n = $n ? $n.to_i : 8
  $r = [
        Regexp.new("^([^\t]{#{($n-1)}})\t\$"),
        / +$/,
        /  +$/,
       ]
}
begin
  buf = "";
  l = 0;
  $_.length.times do |i|
    c = $_[i, 1];
    d = (c == "\t") ? $n - (l % $n) : 1;
    l += d;
    buf += c;
    if (l % $n == 0)
      if ($_[i+1, 1] == " ")
        if (buf =~ $r[1])
          buf = $` + "\t"
        end
      else
        if (buf =~ $r[0])
          buf = $1 + " "
        end
        if (buf =~ $r[2])
          buf = $` + "\t"
        end
      end
      print buf;
      buf = ""
    end
  end
  print buf, "\n"
end'

両者の効率と柔軟性におけるメリット・デメリットを考えてみるのも興味深い。個人的には、前者の方が判り易くてよいと思うが、例えば、'\b' が一文字戻るとみなす処理を加えるには後者の方が簡単である。

このように、unexpand -a と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`rev`

行毎に文字列を反転する BSD rev(1) コマンド。実用したことはないが、sed で実現するには秀逸な技法が必要であった。しかし、ruby では極めて単純に、文字列を逆順に取り出して出力すればよい。

	ruby -ne 'chomp; ($_.length - 1).downto(0){|i| print $_[i, 1] }; print "\n"'

このように、BSD rev(1) と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tac`, `tail -r`

最終行から先頭行まで逆順で出力する GNU tac コマンド、`tail -r` に同じ。これも実用したことはないが、GNU sed における、好ましくない実現例のように、ruby における好ましくない実装は以下の通りである。

	ruby -ne '
if ($. == 1)
  $b = $_
else
  $b = $_ + $b
end
END{ print $b }'

変数に行を逆順に連結して最後にそれを出力するという処理なので、メモリが足りなくなるか、仮想メモリで非常に遅くなるだろう。しかし、Ruby には 'tell', 'seek' とそれらのアクセサ 'pos' がある。より安全な実装は以下のようになる。

	ruby -e '
f = open(ARGV[0])
p = []
p.push(f.pos)
while f.gets
  p.push(f.pos)
end
p.pop
i = p.length - 1
while (i >= 0)
  f.pos = p[i]
  print f.gets
  i -= 1
end'

ここではファイルを入力しながらすべての改行の次のファイルハンドルの位置を配列に覚えておき、改めてその配列の逆順にファイルハンドルを移動する処理を行なっている。ファイル全体をオンメモリで処理しないので、先の例よりは安全であるが、改行が膨大に存在する長大なファイルの場合、配列のためのメモリ不足になるかもしれない。よって、以下のような安全な実装も考えられる。

	ruby -e '
f = open(ARGV[0])
f.seek(0, 2)
p = f.pos - 2
while (p >= 0)
  f.pos = p
  p -= 1
  c = f.read(1) unless (p < 0)
  if (p < 0 || c == "\n")
    print f.gets
  end
end'

ここでは始めにファイルハンドルの位置をファイルのの末尾に移動し、1バイト読み込みつつ、そのファイルハンドルの位置を1バイトずつ戻す処理を行なっている。ファイル全体をオンメモリで処理しないので、先の例よりは安全である。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold`

この例、1行あたりの制御コードを考慮した文字数(既定値は80)を越えたら改行を挿入する ruby スクリプトを示そう。これはカウントが必要になるので sed では困難な好例となっている。

	ruby -nle '
BEGIN{
  $\ = nil
  $n = 8
  $w = 80
}
begin
  l = 0
  $_.length.times do |i|
    c = $_[i, 1]
    d = (c == "\b") ? ((l > 0) ? -1 : 0) : (c == "\r") ? -l : (c == "\t") ? $n - (l % $n) : 1
    if (l+d > $w)
      print "\n"
      l = d
    else
      l += d
    end
    print c
  end
  print "\n"
end'

このように、fold と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold -s`

この例、1行あたりの制御コードを考慮した文字数(既定値は80)を越えようとするブランクに変わり改行を挿入する ruby スクリプトを示そう。これもカウントが必要になるので sed では困難な好例となっているだけでなく、ruby でもかなり複雑にならざるを得ない。

	ruby -nle '
BEGIN{
  $n = 8
  $w = 80
}
def increment(l, c)
  return (c == "\b") ? ((l > 0) ? -1 : 0) : (c == "\r") ? -l : (c == "\t") ? $n - (l % $n) : 1
end
begin
  buf = ""
  l = len = 0
  if ($_ == "")
    print ""
    next
  end
  $_.length.times do |i|
    c = $_[i, 1]
    if (l + increment(l, c) > $w)
      begin
        j = len - 1
        while (j >= 0 && buf[j, 1] !~ /[\t-\r ]/)
          j -= 1
        end
        space = j
      end
      if (space != -1)
        space += 1
        printf("%.*s\n", space, buf)
        buf = buf[space, len - space]
        len -= space
        l = 0
        len.times {|j| l += increment(l, buf[j, 1]) }
      else
        printf("%.*s\n", len, buf)
        l = len = 0
      end
    end
    l += increment(l, c)
    buf = buf[0, len] + c
    len += 1
  end
  if (len != 0)
    printf("%.*s\n", len, buf)
  end
end'

ここでユーザ定義メソッドを効果的に使用していることに注目したい。しかし、もっと簡単になるような気もする…

このように、fold -s と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`strings -a [-n 4] [-t d|o|x]`

この例、ファイルの連続した印字可能な4文字以上の文字列を表示する ruby スクリプトを示す。この例では strings.rb という実行権のついたファイルに記述するものとする。

#!/usr/bin/ruby -nl -s
BEGIN{
  $n = $n ? $n.to_i : 4
  if ($t)
    if ($t == "x")
      $fmt = "%x %s\n"
    elsif ($t == "o")
      $fmt = "%o %s\n"
    else
      $fmt = "%d %s\n"
    end
  end
}
if (!$t)
  gsub(/([\f[:print:]]{#{$n},})/) {
    print $1;
  }
else
  while (m = /([\f[:print:]]{#{$n},})/.match($_))
    printf $fmt, (ARGF.pos - ($_.length + 1) + m.begin(1)), m[0]
    $_ = $'
  end
end

この strings.rb では、'-n=4', '-t=d|o|x' オプションが指定でき、このように、オプション解析は Ruby の '-s' オプションに任せると柔軟な ruby スクリプトが書ける。

このように、strings -a と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`printenv`

さて、ここまでは入力ファイル駆動型のプログラムばかりであったが、そうではなく自律型のプログラムを書くには、Ruby では普通に '-n''-p' オプションを指定しなければよい。

この例、環境変数をすべて表示する ruby スクリプトを示す。

	ruby -l -e 'ENV.each {|k,v| print k, "=", v }'

ハッシュのキー値をすべて取り出すには、このように 'each' メソッドを使う。

このように、printenv と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`yes [expletive]`

この例、プロセス停止まで永遠に "yes^J" を印字し続ける ruby スクリプトを示す。但しこのコマンド、第一引数を指定した場合には "yes" の代わりにそれを印字する仕様となっている。この例では yes.rb という実行権のついたファイルに記述するものとする。

#!/usr/bin/ruby
y = ARGV.length > 0 ? ARGV[0] : "yes"
print y, "\n" while (true)

このように、yes と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cmp [-l|-s] file1 file2`

この例、二つのファイルのバイトの差異を返す ruby スクリプトを示す。バイナリファイルとしての扱いが可能な Ruby での好例となる。

#!/usr/bin/ruby -s
blksize = 4096
bc = lc = rv = 0
exit 0 if (ARGV[0] == ARGV[1])
f = [ open(ARGV[0]), open(ARGV[1]), ]
f[0].binmode
f[1].binmode
bf = [ " " * blksize, " " * blksize, ]
begin
  b = [ f[0].read(blksize, bf[0]), f[1].read(blksize, bf[1]), ]
  sz = (b[0].length < b[1].length) ? b[0].length : b[1].length
  if (!$l)
    sz.times do |i|
      lc += 1 if (b[0][i, 1] == "\n")
      if (b[0][i] != b[1][i])
        print ARGV[0], " ", ARGV[1], " differ: char ", bc + i + 1, ", line ", lc + 1, "\n" if (!$s)
        rv = 1
        break
      end
    end
    bc += sz
  else
    sz.times do |i|
      if (b[0][i] != b[1][i])
        printf "%4d %3o %3o\n", bc + i + 1, b[0][i], b[1][i]
        rv = 1
      end
    end
    bc += sz
  end
  if (!(b[0].length == blksize && b[1].length == blksize))
    STDERR.print "cmp: EOF on ", (b[0].length < b[1].length) ? ARGV[0] : ARGV[1], "\n"
    rv = 1
  end
end while (b[0].length == blksize && b[1].length == blksize)
f[0].close
f[1].close
exit rv

このように cmp と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`paste [-d delimiter] [-s] file ...`

この例、複数のファイルの内容を行毎に結合する ruby スクリプトを示す。但しこのコマンド、'-s' オプションを指定した場合はファイル毎に改行を除去して結合する仕様となっており、特に難しくはない。この例では paste.rb という実行権のついたファイルに記述するものとする。

Ruby ではファイル操作関数が揃っているので、以下のように sed では困難な、複数のファイルの平行な入力に対応できる。

#!/usr/bin/ruby -l -s
def getlinepos(f, p)
  return [ nil, -1 ] if (p < 0)
  fh = open(f)
  fh.seek(p, 0) if (p > 0)
  b = fh.gets
  p = (!fh.eof) ? fh.tell : -1
  fh.close
  [ b, p ]
end
$d ||= "\t"
if (!$s)
  po = Array.new(ARGV.length, 0)
  c = ARGV.length
  while (c > 0)
    l = ""
    ARGV.length.times do |a|
      l += sprintf("%s", $d) if (a != 0)
      next if (po[a] < 0)
      (b, po[a]) = getlinepos(ARGV[a], po[a])
      b.chomp!
      l += sprintf("%s", b)
      c -= 1 if (po[a] < 0)
    end
    print l
  end
else
  previous_nr = 0
  while gets
    fnr = $. - previous_nr
    chomp!
    printf("%s", $\) if (fnr == 1 && previous_nr > 0)
    printf("%s", $d) if (fnr != 1)
    printf("%s", $_)
    previous_nr = $. if $<.eof
  end
  if ($. > 0)
    printf("%s", $\)
  end
end

このように、paste と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`comm [-1] [-2] [-3] file1 file2`

この例、複数のファイルの内容を行毎に比較する ruby スクリプトを示す。この例では comm.rb という実行権のついたファイルに記述するものとする。

入力ファイルは、行でソートされていることが前提となっているので、複数のファイルをキーの文字列の大小で並行して読み進めていけば良い。

#!/usr/bin/ruby
def getlinepos(f, p)
  return [ nil, -1 ] if (p < 0)
  fh = open(f)
  fh.seek(p, 0) if (p > 0)
  b = fh.gets
  p = (!fh.eof) ? fh.tell : -1
  fh.close
  [ b, p ]
end

while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-1') then $s0 = true
  elsif (ARGV[0] == '-2') then $s1 = true
  elsif (ARGV[0] == '-3') then $s2 = true
  else break end
  ARGV.shift
end

$\ = "\n"

cf, s, sc, po = [ Array.new(ARGV.length, -1), Array.new(ARGV.length+1, false), Array.new(ARGV.length+1, 0), Array.new(ARGV.length, 0) ]
s[0] = $s0
s[1] = $s1
s[2] = $s2
(1 .. ARGV.length).each do |a|
  s[0, a].each {|v| sc[a] += 1 if (v) }
end
$km = nil
c = ARGV.length
begin
  last_a = -1
  ceq = 0
  while (c > 0)
    ARGV.length.times do |a|
      cf[a] = -1
      while (!(po[a] < 0) && cf[a] == -1)
	(b, po[a]) = getlinepos(ARGV[a], po[a])
        b.chomp!
	if (!$km)
	  $km = b
	  cf[last_a = a] = nil
          ceq = 0
	else
	  if ($km < b)
            if ceq + 1 != ARGV.length
              printf("%s%s\n", "\t"*(last_a-sc[last_a]), $km) if (!s[last_a])
            end
	    $km = b
            cf[last_a = a] = 1
            ceq = 0
	  elsif ($km == b)
            if ceq + 1 != ARGV.length
              printf("%s%s\n", "\t"*(ARGV.length-sc[ARGV.length]), b) if (!s[ARGV.length])
              ceq += 1
            else
              ceq = 0
            end
            cf[last_a = a] = 0
	  else
            printf("%s%s\n", "\t"*(a-sc[a]), b) if (!s[a])
            cf[a] = -1
            ceq = 0
	  end
	end
	if (po[a] < 0)
	  c -= 1
          if c == 0 && $km != nil
            if ceq + 1 != ARGV.length
              printf("%s%s\n", "\t"*(last_a-sc[last_a]), $km) if (!s[last_a])
            end
          end
	  next
	end
      end
    end
  end
end

このように、comm と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`join [-a file_number|-v file_number] ... [-t char] [-1 field] [-2 field] file1 file2`

この例、複数のファイルの内容を行のキー毎に結合する ruby スクリプトを示す。この例では join.rb という実行権のついたファイルに記述するものとする。

入力ファイルは、キーとなるフィールドでソートされていることが前提となっているので、複数のファイルをキーの文字列の大小で並行して読み進めていけば良い。とは言え、以下のように多少繁雑になるだろう。

#!/usr/bin/ruby
def getlinepos(f, p)
  return [ nil, -1 ] if (p < 0)
  fh = open(f)
  fh.seek(p, 0) if (p > 0)
  b = fh.gets
  p = (!fh.eof) ? fh.tell : -1
  fh.close
  [ b, p ]
end

def printout
  n, m, o = [ 0, 0, nil ]
  ARGV.length.times do |i|
    next if (!$va[i])
    n = i + 1
    m += 1
    if (!o)
      o = $va[i]
    else
      o += $, + $va[i]
    end
    $va[i] = nil
  end
  print $km, o if (o && (m == ARGV.length || $na.index(n)))
end

$na = Array.new
$nv = Array.new
$t = ' '
$n1 = 1
$n2 = 1
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-a' && 1 < ARGV.length) then ARGV.shift; $na.push(ARGV[0].to_i)
  elsif (ARGV[0] == '-v' && 1 < ARGV.length) then ARGV.shift; $nv.push(ARGV[0].to_i)
  elsif (ARGV[0] == '-t' && 1 < ARGV.length) then ARGV.shift; $t = ARGV[0]
  elsif (ARGV[0] == '-1' && 1 < ARGV.length) then ARGV.shift; $n1 = ARGV[0].to_i
  elsif (ARGV[0] == '-2' && 1 < ARGV.length) then ARGV.shift; $n2 = ARGV[0].to_i
  else break end
  ARGV.shift
end

$, = $t
$\ = "\n"

cf, kn, $va, po = [ Array.new(ARGV.length, -1), Array.new(ARGV.length, 1), Array.new(ARGV.length), Array.new(ARGV.length, 0) ]
kn[0] = $n1
kn[1] = $n2
$km = nil
c = ARGV.length
begin
  last_a = -1
  while (c > 0)
    ARGV.length.times do |a|
      cf[a] = -1
      while (!(po[a] < 0) && cf[a] == -1)
	(b, po[a]) = getlinepos(ARGV[a], po[a])
        b.chomp!
	f = b.split($t)
	k = f[kn[a]-1]
	v = nil
        f.length.times do |i|
	  next if (i+1 == kn[a])
	  if (!v)
	    v = f[i]
	  else
	    v += $, + f[i]
	  end
	end
	if (!$km)
	  $km = k
	  $va[a] = v
	  cf[last_a = a] = 0
	else
	  if ($km < k)
	    if ($nv.index(last_a+1))
	      print $km, $va[last_a] if ($va[last_a])
	      $va[last_a] = nil
	    end
	    print k, v if ($nv.index(a+1) && last_a == a)
	    printout if ($nv.length == 0)
	    $km = k
	    $va[a] = v if ($nv.length == 0)
	    cf[last_a = a] = 1
	  elsif ($km == k)
	    $va[a] = v if ($nv.length == 0)
	    cf[last_a = a] = 0
	  else
	    print k, v if ($na.index(a+1) || $nv.index(a+1))
	    cf[a] = -1
	  end
	end
	if (po[a] < 0)
	  c -= 1
	  printout if (c == 0)
	  next
	end
      end
    end
  end
end

このように、join と同じ ruby スクリプトは以上のようになる。但し、正確には単一ファイル内の重複するキーにおける挙動には対応していない。

[sed] [Awk] [Perl] [Ruby] [Python]

`split [-l line_count|-b number[k|m]] [-a suffix_length] [file [name]]`

この例、単一のファイルの内容を行数で複数のファイルに分割する ruby スクリプトを示す。この例では split.rb という実行権のついたファイルに記述するものとする。

Ruby では、`split` コマンドにおける '-b' オプションによるバイナリファイルとしての分割にも対応可能である。

#!/usr/bin/ruby
def outputfilename_digit(nf)
  sprintf('%s%0*d%s', $pre, $n, nf, $suf)
end
def outputfilename_lower(nf)
  b = d = 26
  while ((nf / d).to_i > 0)
    d *= b
  end
  d /= b
  xxxxxx = ''
  begin
    r = (nf / d).to_i
    nf -= d * r
    xxxxxx = xxxxxx + sprintf('%c', r + 97)
    d = (d / b).to_i
  end while (d > 0)
  while (xxxxxx.length < $n)
    xxxxxx = sprintf('%c', 0 + 97) + xxxxxx
  end
  sprintf('%s%s%s', $pre, xxxxxx, $suf)
end
def outputfilename(nf)
  ($d ? outputfilename_digit(nf) : outputfilename_lower(nf))
end

$units = {
  'b'	=>            512, # blocks
  'KB'	=>           1000, # KiloBytes
  'K'	=>           1024, # KibiBytes
  'k'	=>           1024, # KibiBytes
  'MB'	=>      1000*1000, # MegaBytes
  'M'	=>      1024*1024, # MebiBytes
  'm'	=>      1024*1024, # MebiBytes
  'GB'	=> 1000*1000*1000, # GigaBytes
  'G'	=> 1024*1024*1024, # GibiBytes
  'g'	=> 1024*1024*1024, # GibiBytes
}
$k = nil
$s = true
$d = nil
$pre = nil
$suf = nil
$n = 2
$lc = 1000
$bc = 0
$f = nil
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-k') then $k = true
  elsif (ARGV[0] == '--verbose') then $s = nil
  elsif (ARGV[0] == '-d') then $d = true
  elsif (ARGV[0] == '-f' && 1 < ARGV.length) then ARGV.shift; $pre = ARGV[0]
  elsif (ARGV[0] == '-x' && 1 < ARGV.length) then ARGV.shift; $suf = ARGV[0]
  elsif (ARGV[0] == '-a' && 1 < ARGV.length) then ARGV.shift; $n = ARGV[0].to_i
  elsif (ARGV[0] == '-l' && 1 < ARGV.length) then ARGV.shift; $lc = ARGV[0].to_i
  elsif (ARGV[0] == '-b')
    ARGV.shift
    if (ARGV[0] =~ /^(\d+)(KB|MB|GB|[KMGbkmg])?$/)
      $bc = $1.to_i
      $bc *= $units[$2] if ($2)
    end
  else if (!$f) then $f = ARGV[0] else break end end
  ARGV.shift
end
$pre = ARGV[0] if (ARGV.length)
if ($bc == 0)
  l = 0
  nr = 0
  nf = 0; of = open(outputfilename(nf), 'w')
  f = open($f)
  while f.gets
    nr += 1
    of.print $_
    if (nr % $lc == 0)
      puts l if (!$s)
      l = 0
      of.close; nf += 1; of = open(outputfilename(nf), 'w')
    end
    l += $_.length
  end
  puts l if (!$s)
else
  $_ = ' ' * $bc
  nf = 0
  f = open($f); f.binmode
  while f.read($bc, $_)
    of.close if (!nf)
    of = open(outputfilename(nf), 'w'); of.binmode; nf += 1
    of.print $_
  end
end

このように、split と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`csplit [-k] [-s] [-f prefix] [-n number] file arg1 ...argn`

この例、単一のファイルの内容を行のパターンや行番号で複数のファイルに分割する ruby スクリプトを示す。この例では csplit.rb という実行権のついたファイルに記述するものとする。

Ruby では、'seek' 等のファイル操作関数が揃っているので、なんら問題はない。

#!/usr/bin/ruby
def outputfilename(nf)
  sprintf('%s%0*d%s', $pre, $n, nf, $suf)
end

def nextsplit
  $previous_stl = $stl
  $ope, $rep, $stl, $ln = ARGV.length > 0 ? ARGV.shift : nil, 0, false, 0
  ARGV.shift if ($ope && ARGV[0] =~ /^\{(\d+)\}$/ && ($rep = $1.to_i))
  if ($ope =~ /^[\/\%](.*)[\/\%]([-+]?\d+)?$/)
    $reg, $ofs = Regexp.new($1), ($~.length == 2+1) ? $2.to_i : 0
    $np, $c, $oc = -$ofs+1, 0, 0
    $stl = $ope =~ /^\%(.*)\%([-+]?\d+)?$/
    $ope = 1
  elsif ($ope =~ /^(\d+)$/)
    $ln = $1.to_i
    $ope = 2
  else
    $ope = 0
  end
end

p, op = [], []

$k = nil
$s = nil
$d = nil
$pre = 'xx'
$suf = nil
$n = 2
$f = '-'
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-k') then $k = true
  elsif (ARGV[0] == '-s') then $s = true
  elsif (ARGV[0] == '-d') then $d = true
  elsif (ARGV[0] == '-f' && 1 < ARGV.length) then ARGV.shift; $pre = ARGV[0]
  elsif (ARGV[0] == '-x' && 1 < ARGV.length) then ARGV.shift; $suf = ARGV[0]
  elsif (ARGV[0] == '-n' && 1 < ARGV.length) then ARGV.shift; $n = ARGV[0].to_i
  else $f = ARGV[0]; ARGV.shift; break end
  ARGV.shift
end

$ofs = 0
$c = $oc = 0
ol = 0

f = open($f)
nf = 0
of = open(outputfilename(nf), 'w'); nf += 1
nextsplit
if ($ope == 1 && $ofs < 0) then p[$c % ($np+1)] = f.tell; $c += 1 end
if ($ope == 1 && $ofs < 0 && !$stl) then op[$oc % ($np+1)] = of.tell; $oc += 1 end
while f.gets
  if ($ofs < 0) then p[$c % ($np+1)] = f.tell; $c += 1 end
  if ($ope == 1)
    if ~$reg
      if (!($ofs < 0))
	$ofs.times do
	  if (!$stl)
	    of.print $_
	    ol += $_.length
	  end
	  break if !(f.gets)
	end
      end
      if (!$stl)
	if ($ofs < 0)
          of.truncate(op[($oc-1-($np-1)) % ($np+1)]) rescue warn("cannot truncate: #{$!}")
	  ol -= of.tell - op[($oc-1-($np-1)) % ($np+1)]
	end
	print ol, "\n" if (!$s)
	of.close; of = open(outputfilename(nf), 'w'); nf += 1
	ol = 0
      end
      if ($ofs < 0)
        B = $_
	($np-1).times do |i|
	  f.seek(p[($c-1-($np-i)) % ($np+1)], 0) == 0 || warn("cannot seek")
	  f.gets
	  of.print $_
	  ol += $_.length
	end
        $_ = B
	$. -= $np-1
	f.seek(p[($c-1-($np-$np)) % ($np+1)], 0) == 0 || warn("cannot seek:")
	if (!$stl)
	  File.truncate(outputfilename(nf-2), op[($oc-1-($np-1)) % ($np+1)]) rescue warn("cannot truncate: #{$!}")
	end
      end
      if ($rep == 0) then nextsplit else $rep -= 1 end
    end
  elsif ($ope == 2)
    if ($. % $ln == 0)
      print ol, "\n" if (!$s)
      of.close; of = open(outputfilename(nf), 'w'); nf += 1
      ol = 0
      if ($rep == 0) then nextsplit else $rep -= 1 end
    end
  end
  if ($previous_stl || !$stl)
    of.print $_
    ol += $_.length
  end
  if ($ope == 1 && $ofs < 0 && !$stl) then op[$oc % ($np+1)] = of.tell; $oc += 1 end
end
print ol, "\n" if (!$s)
f.close
of.close

このように、csplit と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`seq [-f format] [-s separator] [low [increment]] hi`

この例、GNU coreutils seq コマンドのように、単調増加する数列を生成する ruby スクリプト seq.rb を示す。

#!/usr/bin/ruby
$s = "\n"
$rep, $beg, $dlt, $end = nil, 1, 1, nil
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-f' && 1 < ARGV.length) then ARGV.shift; $f = ARGV[0]
  elsif (ARGV[0] == '-s' && 1 < ARGV.length) then ARGV.shift; $s = ARGV[0]
  else break end
  ARGV.shift
end
if (ARGV.length == 1)
  $end = ARGV[0].to_f
elsif (ARGV.length == 2)
  $beg, $end = ARGV[0].to_f, ARGV[1].to_f
elsif (ARGV.length == 3)
  $beg, $dlt, $end = ARGV[0].to_f, ARGV[1].to_f, ARGV[2].to_f
else
  exit 1
end
if (!$f)
  $f = "%d"
  $f = "%g" if ($beg =~ /\./ || $end =~ /\./)
end
$rep = (($end - $beg)/$dlt).to_i
printf $f, $beg if ($rep > 0)
(1 .. $rep).each {|$_| printf $s + $f, $dlt*$_ + $beg }
print "\n" if ($rep > 0)

このように、GNU coreutils seq と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`jot [-c|-n|-r] [-b word] [-w word] [-s string] [reps [begin [end [s]]]]`

この例、BSD jot コマンドのように、単調増減する数列、乱数列、定数列を生成する ruby スクリプト jot.rb を示す。

#!/usr/bin/ruby
$s = "\n"
$rep, $beg, $dlt, $end = nil, 1, 1, nil
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-w' && 1 < ARGV.length) then ARGV.shift; $w = ARGV[0]
  elsif (ARGV[0] == '-c') then $f = "%c"
  elsif (ARGV[0] == '-b' && 1 < ARGV.length) then ARGV.shift; $b = ARGV[0]
  elsif (ARGV[0] == '-s' && 1 < ARGV.length) then ARGV.shift; $s = ARGV[0]
  elsif (ARGV[0] == '-r') then $r = true; $dlt = nil
  else break end
  ARGV.shift
end
if (ARGV.length == 1)
  $rep = ARGV[0].to_i-1
elsif (ARGV.length == 2)
  $rep, $beg = ARGV[0].to_i-1, ARGV[1]
elsif (ARGV.length == 3)
  $rep, $beg, $end = (ARGV[0] == "-") ? nil : ARGV[0].to_i-1,
                     (ARGV[1] == "-") ? nil : ARGV[1],
                     (ARGV[2] == "-") ? nil : ARGV[2]
elsif (ARGV.length == 4)
  $rep, $beg, $end, $dlt = (ARGV[0] == "-") ? nil : ARGV[0].to_i-1,
                           (ARGV[1] == "-") ? nil : ARGV[1],
                           (ARGV[2] == "-") ? nil : ARGV[2],
                           (ARGV[3] == "-") ? nil : ARGV[3].to_f
else
  exit 1
end
if (!$f)
  $f = "%d"
  $f = "%g" if ($beg =~ /\./ || $end =~ /\./)
end
if ($w)
  if ($w =~ /%/)
    $f = $w
  else
    $f = $w + $f
  end
end
if ($beg =~ /^\D$/) then $beg = $beg[0].ord else $beg = $beg.to_f end if ($beg)
if ($end =~ /^\D$/) then $end = $end[0].ord else $end = $end.to_f end if ($end)
if ($r)
  srand($dlt)
  $dlt = nil
end
if ($rep == -1)
elsif ($rep)
  $beg = $end - $dlt*$rep if (!$beg)
  $end = $beg + $dlt*$rep if (!$end)
  $dlt = ($end - $beg)/$rep
else
  $rep = (($end - $beg)/$dlt).to_i
end
if ($b)
  if ($rep == -1)
    print $b + $s while (true)
  else
    print $b if ($rep > 0)
    (1 .. $rep).each { print $s + $b }
    print "\n" if ($rep > 0)
  end
elsif ($r)
  $dlt = ($end - $beg)
  if ($rep == -1)
    $_ = 0
    printf $f + $s, rand($dlt) + $beg while (true)
  else
    printf $f, rand($dlt) + $beg if ($rep > 0)
    (1 .. $rep).each { printf $s + $f, rand($dlt) + $beg }
    print "\n" if ($rep > 0)
  end
else
  if ($rep == -1)
    $_ = 0
    while (true) do printf $f + $s, $dlt*$_ + $beg; $_ += 1 end
  else
    printf $f, $beg if ($rep > 0)
    (1 .. $rep).each {|$_| printf $s + $f, $dlt*$_ + $beg }
    print "\n" if ($rep > 0)
  end
end

オリジナルと微妙に既定の書式の扱いが異なるが、ほぼ等価な実装となっている。

このように、BSD jot と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`shuf [-r [-n times]] -e arg...`, `shuf [-r [-n times]] -i 1-6`, `shuf [-r [-n times]] [filename]`

この例、GNU coreutils shuf コマンドはオプションの有無によって以下の機能を持つ。

  1. `shuf [-r [-n times]] -e arg...` - コマンドライン引数の配列をシャッフル。 '-r' のとき無限回数または '-n times' で指定回数で生成。
  2. `shuf [-r [-n times]] -i 1-6` - 1から6までの数列(数値は任意)をシャッフル。 '-r' のとき無限回数または '-n times' で指定回数で生成。
  3. `shuf [-r [-n times]] [filename]` - ファイルの行をシャッフル。 '-r' のとき無限回数または '-n times' で指定回数で生成。

ここでは、shuf.rb という実行権のついたファイルに記述するものとし、以下のように使用するものとする。

  1. `shuf.rb [-r [-n=times]] -e arg...`
  2. `shuf.rb [-r [-n=times]] -i=1-6`
  3. `shuf.rb [-r [-n=times]] [filename]`
#!/usr/bin/ruby -s
if (!$r)
  if ($e)
    srand()
    while (ARGV.length > 0)
      puts ARGV.delete_at(rand(ARGV.length).to_i)
    end
  elsif ($i)
    lo, hi = 1, 6
    lo, hi = $1.to_i, $2.to_i if ($i =~ /(\d+)-(\d+)/)
    !(lo > hi) || exit(1)
    a = []
    (lo .. hi).each do |i|
      a.push(i)
    end
    srand()
    while (a.length > 0)
      puts a.delete_at(rand(a.length).to_i)
    end
  else
    ARGV.unshift("-") unless (ARGV.length > 0)
    f = ARGV[0] == "-" ? STDIN : open(ARGV[0])
    a = []
    a.push(f.tell)
    while (f.gets)
      a.push(f.tell)
    end
    a.pop
    srand()
    while (a.length > 0)
      f.seek(a.delete_at(rand(a.length).to_i), 0)
      print f.gets
    end
    f.close if (f != STDIN)
  end
else
  if ($e)
    srand()
    if ($n)
      (1..$n.to_i).each do
	puts ARGV[rand(ARGV.length).to_i]
      end
    else
      while (true) do
	puts ARGV[rand(ARGV.length).to_i]
      end
    end
  elsif ($i)
    lo, hi = 1, 6
    lo, hi = $1.to_i, $2.to_i if ($i =~ /(\d+)-(\d+)/)
    !(lo > hi) || exit(1)
    a = (hi - lo + 1).to_i
    srand()
    if ($n)
      (1..$n.to_i).each do
        puts rand(a).to_i + lo
      end
    else
      while (!0)
	puts rand(a).to_i + lo
      end
    end
  else
    ARGV.unshift("-") unless (ARGV.length > 0)
    f = ARGV[0] == "-" ? STDIN : open(ARGV[0])
    a = []
    a.push(f.tell)
    while f.gets
      a.push(f.tell)
    end
    a.pop
    srand()
    if ($n)
      (1..$n.to_i).each do
	f.seek(a[rand(a.length).to_i], 0)
	print f.gets
      end
    else
      while (true)
	f.seek(a[rand(a.length).to_i], 0)
	print f.gets
      end
    end
    f.close if (f != STDIN)
  end
end

このように、shuf と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`sort [-c|-m] [-u] [-r] [-d|-f|-i]`

POSIX sort コマンドは主に以下の3つの機能がある。

  1. `sort -c file` - 一つのファイルがソート済みか否かをチェックする。
  2. `sort -m file[...]` - ソート済みの複数のファイルをソートしてマージする。
  3. `sort file[...]` - 複数のファイルをソートしてマージする。

また、'-u' オプションで重複する行の出力の抑止、'-r' オプションで逆順、'-d|-f|-i' オプションで順に、blank と alnum のみの比較、小文字を大文字と見なして比較、印字可能文字のみの比較でソートする。

さらに、'-k key' オプションでソートのキーフィールド指定、'-t char' オプションでフィールド区切りを指定できる。'-k key', '-t char' オプションの実装は多少複雑になるので、行そのものをキーとするソートの実装を示す。

`sort -c [-u] [-r] [-d|-f|-i]`

#!/usr/bin/ruby
$r = 1
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-r') then $r = -1
  elsif (ARGV[0] == '-d') then $d = true
  elsif (ARGV[0] == '-f') then $f = true
  elsif (ARGV[0] == '-i') then $i = true
  elsif (ARGV[0] == '-u') then $u = true
  else break end
  ARGV.shift
end

def normalize_line(b)
  b.chomp!
  b.gsub!(/[^[:blank:][:alnum:]]/, '') if ($d)
  b.upcase! if ($f)
  b.gsub!(/[^[:print:]]/, '') if ($i)
end
def compare_lines(a, b)
  r, s = a.dup, b.dup
  normalize_line(r)
  normalize_line(s)
  (r <=> s)*$r
end
begin
  ARGV.unshift("-") unless (ARGV.length > 0)
  fh = ARGV[0] == "-" ? STDIN : open(ARGV[0])
  b0 = fh.gets
  while fh.gets
    if ((!$u && compare_lines(b0, $_) == 1) || ($u && compare_lines(b0, $_) != -1))
      abort "sort: #{ARGV[0]}:#{$.}: disorder: #{$_}\n"
    end
    b0 = $_
  end
  fh.close
end

行を読み込み、前の行と比較して 1 以外なら正常終了となる。'-u' オプションのときは 0 つまり重複する行も異常終了となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`sort -m [-u] [-r] [-d|-f|-i]`

#!/usr/bin/ruby
def getlinepos(f, p)
  return [ nil, -1 ] if (p < 0)
  fh = open(f)
  fh.seek(p, 0) if (p > 0)
  b = fh.gets
  p = (!fh.eof) ? fh.tell : -1
  fh.close
  [ b, p ]
end

ofh = STDOUT
$r = 1
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-o' && 1 < ARGV.length) then ARGV.shift; ofh = open(ARGV[0], 'w')
  elsif (ARGV[0] == '-r') then $r = -1
  elsif (ARGV[0] == '-d') then $d = true
  elsif (ARGV[0] == '-f') then $f = true
  elsif (ARGV[0] == '-i') then $i = true
  elsif (ARGV[0] == '-u') then $u = true
  else break end
  ARGV.shift
end

def normalize_line(b)
  b.chomp!
  b.gsub!(/[^[:blank:][:alnum:]]/, '') if ($d)
  b.upcase! if ($f)
  b.gsub!(/[^[:print:]]/, '') if ($i)
end
def compare_lines(a, b)
  r, s = a.dup, b.dup
  normalize_line(r)
  normalize_line(s)
  (r <=> s)*$r
end
begin
  a, p = [], []
  ARGV.length.times do |i|
    a.push(i)
    p[i] = 0
  end
  b0 = nil
  while (a.length > 1)
    a.sort! {|i, j|
      r, q = getlinepos(ARGV[i], p[i])
      s, q = getlinepos(ARGV[j], p[j])
      compare_lines(r, s)
    }
    b, p[a[0]] = getlinepos(ARGV[a[0]], p[a[0]])
    ofh.print((b0=b)) if (!$u || ($u && (!b0 || compare_lines(b0, b) != 0)))
    a.length.times do |i|
      a.delete_at(i) if (i < a.length && p[a[i]] == -1)
    end
  end
  while (p[a[0]] != -1)
    b, p[a[0]] = getlinepos(ARGV[a[0]], p[a[0]])
    ofh.print((b0=b)) if (!$u || ($u && (!b0 || compare_lines(b0, b) != 0)))
  end
end

各ファイルはソート済みであることが前提としてあるので、すべてのファイルを並行して読み、それらの行を並び変えて先頭のファイルの行のみを出力して進めばよい。

[sed] [Awk] [Perl] [Ruby] [Python]

`sort [-r] [-u] [-d|-f|-i]`

#!/usr/bin/ruby
ofh = STDOUT
$r = 1
while (ARGV.length > 0)
  if (ARGV[0] == '--') then ARGV.shift; break
  elsif (ARGV[0] == '-o' && 1 < ARGV.length) then ARGV.shift; ofh = open(ARGV[0], 'w')
  elsif (ARGV[0] == '-r') then $r = -1
  elsif (ARGV[0] == '-d') then $d = true
  elsif (ARGV[0] == '-f') then $f = true
  elsif (ARGV[0] == '-i') then $i = true
  elsif (ARGV[0] == '-u') then $u = true
  else break end
  ARGV.shift
end

def normalize_line(b)
  b.chomp!
  b.gsub!(/[^[:blank:][:alnum:]]/, '') if ($d)
  b.upcase! if ($f)
  b.gsub!(/[^[:print:]]/, '') if ($i)
end
def compare_lines(a, b)
  r, s = a.dup, b.dup
  normalize_line(r)
  normalize_line(s)
  (r <=> s)*$r
end

class File_Position
  attr_reader :Name, :Position
  def initialize(name, position)
    @Name = name
    @Position = position
  end
end

file_position_list = []

def file_position_getline(file_position)
  fh = open(file_position.Name)
  fh.seek(file_position.Position, 0)
  b = fh.gets
  fh.close
  return b
end
begin
  ARGV.length.times do |a|
    fh = open(ARGV[a])
    begin
      file_position_list.push(File_Position.new(ARGV[a], fh.tell))
    end while (fh.gets)
    fh.close
    file_position_list.pop
  end
  file_position_list.sort! {|a, b|
    r = file_position_getline(a)
    s = file_position_getline(b)
    compare_lines(r, s)
  }
  b0 = nil
  for file_position in file_position_list
    b = file_position_getline(file_position)
    ofh.print((b0=b)) if (!$u || ($u && (!b0 || compare_lines(b0, b) != 0)))
  end
end

各ファイルの行頭のファイルポジションをファイル名とのペアで保持しておき、それを行の比較でソートすればよい。

このように、sort (但し、キーフィールド指定なし) と同じ ruby スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

参考文献

  1. Ruby リファレンスマニュアル
Written by Taiji Yamada <taiji@aihara.co.jp>