'^$'`'^$'`'^.+'`'^.+'`':' -f 1,6`':' -f 1,6 -s`'A-Za-z' 'N-ZA-Mn-za-m'`
ruby -[n|p][l]e 'script' [file...]
ruby [-n|-p][-l] script_file [file...]
ruby -n は『レコード』と呼ばれる行を一つずつパターンスペース '$_' に入力する。ruby -p はさらにそのパターンスペース '$_' を出力する。Ruby スクリプトを '-e script' や 'script_file' で指定することにより様々な処理をすることができる。
まず、入力行は sed, Awk とは異なり、Perl 同様、レコードセパレータである改行コード '\n' が取り除かれずにパターンスペース '$_' に入る。但し、'-l' オプションを指定すると、改行コードが取り除かれ、出力のレコードセパレータに改行コード '\n' が設定される。
また、ruby -na は、Awk のように、フィールドセパレータである空白を区切りとして '$F[0]', '$F[1]', '$F[2]', 〜 にその行の『フィールド』群が入る。
Awk のように 'BEGIN', 'END' のような前処理と、後処理を記す特殊ブロックが使えるが、他はすべて主処理となる。そして、sed, Awk のようなマッチの範囲「式, 式」に処理されるブロックはサポートされないが、条件式で '..' 演算子(2つの式が sed スタイルのときは '...' 演算子)を用いることでそれと似た制御ができる。
例えば以下は、Awk では awk となる、HTML の '/^<pre>/,/<\/pre>$/''pre' タグを含むそれに囲まれた行を表示する Ruby スクリプトである。
ruby -ne 'print if (/^<pre>/../<\/pre>$/)'
ここで、'print' は 'print $_' と等価である。
このように Ruby スクリプトは、オプションにより sed, Awk 風に書けるようになっている。しかし、正規表現による置換についてはその限りではない。
例えば、sed では sed -e となる、ソースコードを HTML にペーストできるように「<」から「<」への変換等を行なう Ruby スクリプトは以下のように書かなければならない。
's/&/\&/g;s/</\</g;s/>/\>/g'
ruby -pe 'gsub(/&/,"&"); gsub(/</, "<"); gsub(/>/, ">")'
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 ほどではない。
例えば先の、HTML の 'pre' タグを含むそれに囲まれた行を表示する Ruby スクリプトは、省略せずに書けば以下のようになる。
ruby -e ' while $<.gets do $stdout.print $_ if ($_ =~ /^<pre>/ .. $_ =~ /<\/pre>$/) end
例えば先の、ソースコードを HTML にペーストできるように「<」から「<」への変換等を行なう Ruby スクリプトは、省略せずに書けば以下のようになる。
ruby -e ' while $<.gets do $_.gsub!(/&/, "&") $_.gsub!(/</, "<") $_.gsub!(/>/, ">") $stdout.print $_ end'
スクリプトは式のみからなり、プログラミング言語 C などとは異なり、制御構文さえも式である。式には、変数と定数、さまざまなリテラル、それらからなる演算子式、if や while などの制御構造、メソッド呼び出し、クラス/メソッドの定義がある。式と式の区切りは改行か ';' で区切り文となる。ブロック '{' … '}' や 'do' … 'end' 内の最後の区切り ';' は不要、改行は省略できる。よって、Perl と異なり ';' は改行がある限りまず不要である。
以下の変数、定数、リテラルがある。
'$' から始まるグローバル変数'@' から始まるインスタンス変数'@@' から始まるクラス変数'self', 'nil', 'true', 'false' '__FILE__', '__LINE__' の代入不可の疑似変数'?C', '?' 記法がある。但し 1.8 以前)'?C', '?' 記法は 1.9 以降は文字列リテラル)、及び、バックスラッシュ記法、式展開と変数展開、コマンド展開、ヒアドキュメント'/…/', '%r(…)''[ 0, 1, …]''{ 0=>1, 1=>2, …}''1 .. 10'(但し、降順はできない), '/…/ .. /…/', '(…) ... (…)'':' から始まるシンボルさて、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 リファレンスマニュアル「多重代入」は一読しておいた方がよいだろう。
以下の制御構造がある。
if … [then|^J …] [elsif … then|^J …] [else …] end, unless … [then|^J …] [elsif … then|^J …] [else …] end… if 修飾子, … unless 修飾子case […] [when … then|^J …] [else …] endwhile … [do|^J …] end, until … [do|^J …] end… while 修飾子, … until 修飾子for … in … [do|^J …] endbreak […], next […]redoraise …begin … [rescue […] then|^J … [retry] [else …] [ensure …] end… rescure … 修飾子return …BEGIN{ … }, END{ … }
ここで、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' モジュールの特殊変数がある。
主な定数は以下の通りである。
ARGV … コマンドライン引数の配列 ARGV[0], ARGV[1] … ARGV[ARGV.size-1]ENV … 環境変数のハッシュ。ENV[名前]STDIN … 標準入力STDOUT … 標準出力STDERR … 標準エラー出力ARGF … 現在読み込み中のファイル(ARGV[0..] や STDIN)。'ARGF.file' でファイルオブジェクト、'ARGF.filename' でファイル名が得られる。まず、グローバルスコープの Kernel 特殊変数には主に以下がある。
$* … Object::ARGV の別名$0 … 実行中のスクリプト名$PROGRAM_NAME … 同上$, … 出力のフィールドセパレータ。既定値は "nil"$/ … 入力のレコードセパレータ。既定値は "\n"$-0 … 同上$; … 入力のフィールドセパレータ。既定値は空白$-F … 同上$. … 入力のレコード数$\ … 出力のレコードセパレータ。既定値は "nil"$_ … 既定のパターンスペース。gets や readline などで代入され、chomp, split などの既定のレシーバ, print などの既定の引数である。$< … Object::ARGF の別名$FILENAME … Object::ARGF.filename の別名$stdin … Object::STDIN が初期値$> … Object::STDOUT が初期値$stdout … 同上$stderr … Object::STDERR が初期値次に、スレッドローカルスコープの Kernel 特殊変数には主に以下がある。
$! … 最後に発生した例外オブジェクト$& … 正規表現で最後にマッチした文字列$` … 正規表現で最後にマッチした直前の文字列$' … 正規表現で最後にマッチした直後の文字列$+ … 正規表現で最後にマッチした最後のグループの文字列$1, $2 … 正規表現で最後にマッチしたグループの文字列$~ … 正規表現で最後にマッチしたマッチオブジェクトちなみに、グローバル変数のスコープには「グローバルスコープ」と「ローカルスコープ」と「スレッドローカルスコープ」がある。
Ruby におけるエスケープ文字(バックスラッシュ記法と正規表現におけるメタ文字)は以下の通りである。
\a - "^G,BEL,アラート(alert)"\b - "^H,BS,バックスペース(backspace)"、但し、[] 内のみ。\t - "^I,HT,水平タブ(horizontal tab)"\n - "^J,LF,改行(new-line)"\f - "^L,FF,改頁(form feed)"\r - "^M,CR,行頭復帰(carriage return)"\v - "^K,VT,垂直タブ(vertical tab)"、但し、[] 内のみ。\e - "^[,ESC,エスケープ(escape)"\OOO - 8進数の文字コード\xHH - 16進数の文字コード\cC - 制御文字('?C.ord & ~0x60')。C には任意の文字。\C-c - 制御文字('?c.ord & ~0x60')。c には任意の文字。\M-c - メタ文字('?c.ord | 0x80')。c には任意の文字。\M-\C-c - メタ制御文字('?c.ord & ~0x60 | 0x80')。c には任意の文字。\uHHHH - Unicode 文字 (1.9以降)\u{HHHH} - Unicode 文字 (1.9以降)\w - 英数字とアンダースコア「_」(以降、正規表現に関して)\W - 上記以外\s - 空白\S - 上記以外\d - 数字\D - 上記以外\p{} - Unicode プロパティの文字 (1.9以降)\P{} - 上記以外 (1.9以降)\X - Unicode「拡張書記素クラスタ」。[] 内では不可。 (1.9以降)\K - 直前を保持、$& に含めない。[] 内では不可。 (1.9以降)\h - 16進数字(水平空白の文字クラスではない) (1.9以降)\H - 上記以外(水平空白の文字クラスではない) (1.9以降)\R - 総称的な改行。[] 内では不可。 (1.9以降)\b - ワード境界。但し、[] 内では上述。\B - 上記以外。[] 内では不可。\A - 文字列の先頭。[] 内では不可。\Z - 文字列の末尾。[] 内では不可。\z - 同上。[] 内では不可。\G - 文字列の先頭か前回のマッチ直後。[] 内では不可。\1 - 後方参照。1 には正の整数。[] 内では不可。\g1 - 後方参照。1 には正の整数。[] 内では不可。0 なら再帰マッチ (1.9以降)\g<1> - 後方参照。1 には正の整数。[] 内では不可。0 なら再帰マッチ (1.9以降)\g<-1> - 相対後方参照。1 には負の整数。[] 内では不可。 (1.9以降)\k'name' - 名前後方参照。[] 内では不可。 (1.9以降)\k<name> - 同上このように Ruby 1.9 以降では非常に多くのエスケープ文字がサポートされる。
Ruby にはクラス、モジュール、オブジェクトにより、組み込みライブラリが標準でサポートされる。
ここで主な組み込みライブラリのリファレンスを列挙しておく。
gets, chomp, print, rand, exit などはここで定義される。abs, ceil, floor, round などはここで定義される。times, downto, upto などはここで定義される。abs, ceil, floor, round, nan? などはここで定義される。sin, cos, exp, log などはここで定義される。each, push, pop, shift, unshift, sort, bsearch などはここで定義される。keys, values, each などはここで定義される。chomp, chomp!, tr, tr!, gsub, gsub! などはここで定義される。last_match などはここで定義される。post_match, pre_match などはここで定義される。open, read, seek, tell などはここで定義される。リンク先のメソッドや定数をよく参考にすること。
以下に '$_' が省略可能なメソッドや式を列挙する。
| メソッド | 推奨代替 | 説明 |
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 スクリプトを以下にあげる。
ruby -pe ''
このように、cat と同じ ruby スクリプトは「空」となるが、「ruby -ne 」でもよいし、「'print'ruby -ne 」でもよいし、「'print $_'ruby -e 」でもよいし、「'print while gets'perl -e 」でもよい。
'while gets do print end'
ruby -ne 'print if $. == 1'
このように、head -n 1 と同じ ruby スクリプトは以上のようになるが、「perl -ne 」の方が効率がよいだろう。
'if ($. == 1) { print; last }'
ruby -ne 'print if $<.eof'
このように、tail -n 1 と同じ ruby スクリプト以上のようになるが、Awk のように「END{ print }」では動作せず、この場合「perl -ne 」となる。
'$b = $_; END{ print $b }'
ruby -ne 'print if !($. > 8)'
このように、head -n 8 と同じ ruby スクリプトは以上のようになるが、「ruby -ne 」の方が効率がよいだろう。
'if (!($. > 8)) then print else break end'
さて、`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 スクリプトは以上のようになる。
ruby -ne 'END{ print "#{$.}" }'
このように、wc -l と同じ ruby スクリプトは以上のようになる。
sed では大変面倒になる `wc -c` は ruby だと算術演算があるので簡単である。
ruby -ne '
BEGIN{
$l = 0
}
$l += $_.length
END{
print "#{$l}\n"
}'
Perl だと 'BEGIN' ブロックで行なっている変数の初期化は不要なのだが、Ruby だと変数の型を決定する為に必要になる。
このように、wc -c と同じ ruby スクリプトは以上のようになる。
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 スクリプトは以上のようになる。
'^$'`
ruby -ne 'print if ~/^$/'
このように、基本正規表現の grep '^$' と同じ ruby スクリプトは以上のようになるが、Ruby では基本正規表現はサポートされないので、他のパターンではRuby正規表現に書き直す必要がある。
'^$'`
ruby -ne 'print unless ~/^$/'
このように、マッチの否定、基本正規表現の grep -v '^$' と同じ ruby スクリプトは以上のようになるが、Ruby では基本正規表現はサポートされないので、他のパターンではRuby正規表現に書き直す必要がある。
'^.+'`
ruby -ne 'print if ~/^.+/'
このように、拡張正規表現の grep -E '^.+' と同じ ruby スクリプトは以上のようになる。
'^.+'`
ruby -ne 'print unless ~/^.+/'
このように、拡張正規表現のマッチの否定、grep -E -v '^.+' と同じ ruby スクリプトは以上のようになる。
':' -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 スクリプトは以上のようになる。
':' -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 スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
ruby -ne 'BEGIN{ $f = open("filename.out", "w") }; begin print; $f.print end; END{ $f.close }'
このように、標準入力を標準出力とファイルに書き出し、tee と同じ ruby スクリプトは以上のようになる。
'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 ではいささか面倒になる `cat -n` は ruby だと極めて簡単である。
ruby -ne 'printf("%6d\t%s", $., $_)'
このように、cat -n と同じ ruby スクリプトは以上のようになる。
sed では大変面倒になる `cat -n` は ruby だと極めて簡単である。しかし、変数の型を決める為の初期化を要するので Perl よりも繁雑になる。
ruby -pe 'BEGIN{ $i = 0 }; if (!/^$/) then $i += 1; $_ = sprintf("%6d\t%s", $i, $_) end'
このように、cat -b と同じ ruby スクリプトは以上のようになる。
ところで、これらは GNU sed や 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 スクリプトは以上のようになる。
このタブを複数の空白に置換するコマンドを 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 スクリプトは以上のようになる。
この複数の空白をタブに置換するコマンドを 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 スクリプトは以上のようになる。
行毎に文字列を反転する BSD rev(1) コマンド。実用したことはないが、sed で実現するには秀逸な技法が必要であった。しかし、ruby では極めて単純に、文字列を逆順に取り出して出力すればよい。
ruby -ne 'chomp; ($_.length - 1).downto(0){|i| print $_[i, 1] }; print "\n"'
このように、BSD rev(1) と同じ ruby スクリプトは以上のようになる。
最終行から先頭行まで逆順で出力する 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バイトずつ戻す処理を行なっている。ファイル全体をオンメモリで処理しないので、先の例よりは安全である。
この例、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 スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
この例、ファイルの連続した印字可能な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 スクリプトは以上のようになる。
さて、ここまでは入力ファイル駆動型のプログラムばかりであったが、そうではなく自律型のプログラムを書くには、Ruby では普通に '-n' や '-p' オプションを指定しなければよい。
この例、環境変数をすべて表示する ruby スクリプトを示す。
ruby -l -e 'ENV.each {|k,v| print k, "=", v }'
ハッシュのキー値をすべて取り出すには、このように 'each' メソッドを使う。
このように、printenv と同じ ruby スクリプトは以上のようになる。
この例、プロセス停止まで永遠に "yes^J" を印字し続ける ruby スクリプトを示す。但しこのコマンド、第一引数を指定した場合には "yes" の代わりにそれを印字する仕様となっている。この例では yes.rb という実行権のついたファイルに記述するものとする。
#!/usr/bin/ruby y = ARGV.length > 0 ? ARGV[0] : "yes" print y, "\n" while (true)
このように、yes と同じ ruby スクリプトは以上のようになる。
この例、二つのファイルのバイトの差異を返す 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 スクリプトは以上のようになる。
この例、複数のファイルの内容を行毎に結合する 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 スクリプトは以上のようになる。
この例、複数のファイルの内容を行毎に比較する 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 スクリプトは以上のようになる。
この例、複数のファイルの内容を行のキー毎に結合する 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 スクリプトは以上のようになる。但し、正確には単一ファイル内の重複するキーにおける挙動には対応していない。
この例、単一のファイルの内容を行数で複数のファイルに分割する 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 スクリプトは以上のようになる。
この例、単一のファイルの内容を行のパターンや行番号で複数のファイルに分割する 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 スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
この例、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 スクリプトは以上のようになる。
この例、GNU coreutils shuf コマンドはオプションの有無によって以下の機能を持つ。
'-r' のとき無限回数または '-n times' で指定回数で生成。'-r' のとき無限回数または '-n times' で指定回数で生成。'-r' のとき無限回数または '-n times' で指定回数で生成。
ここでは、shuf.rb という実行権のついたファイルに記述するものとし、以下のように使用するものとする。
#!/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 スクリプトは以上のようになる。
POSIX sort コマンドは主に以下の3つの機能がある。
また、'-u' オプションで重複する行の出力の抑止、'-r' オプションで逆順、'-d|-f|-i' オプションで順に、blank と alnum のみの比較、小文字を大文字と見なして比較、印字可能文字のみの比較でソートする。
さらに、'-k key' オプションでソートのキーフィールド指定、'-t char' オプションでフィールド区切りを指定できる。'-k key', '-t char' オプションの実装は多少複雑になるので、行そのものをキーとするソートの実装を示す。
#!/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 つまり重複する行も異常終了となる。
#!/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
各ファイルはソート済みであることが前提としてあるので、すべてのファイルを並行して読み、それらの行を並び変えて先頭のファイルの行のみを出力して進めばよい。
#!/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 スクリプトは以上のようになる。