UNIX 系のシステムを使っているなら、grep 使いますよね。 Ruby で書いた便利 grep です。
「便利さ」ってなんでしょう? 普段どんな検索しますか? Ruby の正規表現がそのまま使えるといいですよね。 私は、ASCII テキストとそれと同じくらいいろいろな漢字コードが含まれるテキストも扱います。 また、圧縮されているファイルも扱います。 こういうとき、使えるコマンド持っていますか?
基本的には、
になります。$ rg.rb [オプション] 検索パターン ファイル名
検索パターンには、Ruby の正規表現がそのまま使えます。
ファイル名は、対象となるファイルがしぼられていない場合は、シェル上で「*」で OK です。 バイナリファイルやディレクトリは対象からはずしますし、圧縮されている場合も適当に扱ってくれます。
オプションとしては、次のものがあります。
オプション 説明 -bバイナリファイルも検索対象とします -eE-mail の引用に使われる行頭の「>」や「|」を無視します -fファイル名を表示します -iアルファベットの大文字小文字を区別しません -k漢字を検索します -lファイル名だけを表示します -m行をまたいだパターンを検索します -n行番号を表示します -s連続したスペースを一つとして扱います -vパターンが一致しないと表示します -Ffgrep コマンドと同じくメタ文字もそのまま検索します -Vversion を表示します
検索結果によって終了コードが変わっています。 シェルスクリプトと組み合わせる場合などに使えます。
exit コード 説明 0 パターンがみつかった場合 1 なんらかのエラーか、パターンがみつからない場合
引数としてファイルを指定しますが、このときシェル上で「*」を指定してもほとんど問題はありません。 バイナリは対象外にしますし、ディレクトリもパスします。 圧縮されたファイルは、前処理プログラムを呼びだして検索の対象にしてしまいます。 便利でしょ? ということで、目的のものを見つけるために対象ファイルは「*」を指定可能です。
引数として圧縮されたファイルを指定した場合でも検索可能です。 このとき、ファイルの拡張子が圧縮フォーマットと一致していることが必要になります。 拡張子「.bz2」なら自動的に bzcat が展開してそれを検索しています。 このような作業を自動的に行うのでとても便利です。
拡張子 実行されるコマンド .lzh lha l .zip unzip -l .zoo zoo -list .tar.gz/tgz tar tzvf .tar.bz2/tbz tar tIvf .gz/.z/.Z zcat .bz2 bzcat
漢字検索オプション -k を加えると
でも「赤いコランダムの宝石」で検索可能です。赤い コランダム の 宝石
multi line オプション -m を加えると
でも「赤いコランダムの宝石」で検索可能です。赤いコラン ダムの宝石
でも「to Ruby」で検索可能です。Welcome to Ruby world
E-mail オプション -e を加えると
でも「赤いコランダムの宝石」で検索可能です。> 赤いコラン > ダムの宝石
space one オプション -s を加えると
でも「to Ruby」で検索可能です。 まあ、「\s+」使えばいいけどね。Welcome to Ruby world
漢字コードについて。 このスクリプトおよび処理の内部的には、EUC Japan を使用しています。 検索用パターンは、nkf モジュールで必ず EUC に変換されます。 検索対象となる入力文字列は、-k オプションが指定されている場合のみ nkf モジュールで変換されます。
-m オプションについて。 入力を空行区切りのブロックで扱います。 検索するキーワードは、ブロック内で改行で区切られていても、検索可能です。 その際は、改行は一つのスペースとして扱われます。 -k が指定されている場合は、スペースは入りません。
-k オプションについて。 「漢字」が「スペース」で区切られていた場合、「スペース」をとりのぞきます。 テキストの整形ツールなどで、「スペース」が入った場合でも、検索が可能にするためです。 日本語オンラインマニュアルなどに利用できると思います。
-e オプションについて。 E-mail で引用に使用される「|」や「>」が行頭にある場合、それをとりのぞき検索します。 引用中の改行で区切られた部分でもキーワードと一致しているか? 確認可能になっています。
Web ブラウザでの表示上は Ruby のスクリプトですが、HTML の特殊文字をエスケープしています。 テキストとしてセーブしてご利用ください。
スクリプト中に、漢字コードを直接指定している部分があります。 このスクリプトは、セーブ後 EUC-Japan に変換してください。
#! /usr/local/bin/ruby -Ke
# /home/tetsu/src/ruby/toolbox/rg.rb
# Created: July 09,2000 Sunday 18:26:03
# Author: tetsu(WATANABE Tetsuya)
RCS_ID = '$Id: rg.rb,v 1.26 2007/05/29 17:42:10 tetsu Exp $'
# usage:
class File
def File.zopen(filename)
suff2exe = {
'\.lzh' => 'lha l ', # lha pq
'\.zip' => 'unzip -l ', # unzip -p
'\.zoo' => 'zoo -list ', # zoo xpq
'\.tar.gz' => 'tar tzvf ',
'\.tgz' => 'tar tzvf ',
'\.tar.bz2' => 'tar tIvf ',
'\.tbz' => 'tar tIvf ',
'\.gz' => 'zcat ',
'\.z' => 'zcat ',
'\.Z' => 'zcat ',
'\.bz2' => 'bzcat '
}
suffix = ''
suff2exe.keys.sort do |a, b|
b.size <=> a.size
end.each do |s|
if filename =~ /#{s}$/
suffix = s
break
end
end
if suff2exe.key?(suffix)
f = File.popen(suff2exe[suffix] + filename)
eval "def f.path; '#{filename}' end"
f
else
File.open(filename)
end
end
end
def rg(re, f, opt)
opt_multi = opt['multi']
opt_lineno = opt['lineno']
opt_kanji = opt['kanji']
opt_binary = opt['binary']
opt_email = opt['email']
opt_nkf = opt['nkf']
opt_spaceone = opt['spaceone']
opt_revertmatch = opt['revertmatch']
opt_filenameonly = opt['filenameonly']
opt_filename = opt['filename']
match = false
sep = "\n"
if opt_multi
linecount = 0 if opt_lineno
sep.concat("\n")
lf2 = if opt_kanji then '' else ' ' end # 改行の扱い
end
while l = f.gets(sep)
break if ! opt_binary && l.count("\000") > 1
str = l.dup
str.gsub!(/^[|>\s]+/, '') if opt_email
str = str.gsub(/^\s+/, '').gsub(/\s+$/, '').gsub(/\n/, lf2) if opt_multi
str = NKF.nkf(opt_nkf, str).gsub(/([ -瑤])\s+([ -瑤])/, '\1\2') if opt_kanji
str.gsub!(/[ \t]+/, ' ') if opt_spaceone
m_flag = str =~ re
if (m_flag && opt_revertmatch == false) || (m_flag.nil? && opt_revertmatch)
match = true
if opt_filenameonly
puts f.path
break
else
if opt_multi
l.gsub!(/^/) do (linecount += 1).to_s + ':' end if opt_lineno
l.gsub!(/^/) do f.path + ':' end if opt_filename
else
print f.path, ':' if opt_filename
print f.lineno, ':' if opt_lineno
end
print NKF.nkf(opt_nkf, l)
end
else
linecount += l.count("\n") if opt_multi and opt_lineno
end
end
match
end
def usage
STDERR.print <<EOF
usage: #{$0} [-befiklmnsv] [files...]
b: binary file
e: E-mail format
f: filename
i: ignore_case
k: kanji
l: only filename
m: multi line
n: line number
s: space one
v: revert match
V: version
EOF
exit 1
end
opt = {
'binary' => false, # バイナリファイルも
'email' => false, # E-mail 形式の引用を扱う
'filename' => false, # ファイル名表示
'ignorecase' => false, # アルファベットの大文字小文字を区別しない
'kanji' => false, # 漢字を検索する場合
'filenameonly' => false, # ファイル名だけ表示
'multi' => false, # 行をまたいだパターンを指定したい場合
'lineno' => false, # lineno 表示
'revertmatch' => false, # マッチしなかったら
'spaceone' => false, # 連続スペースを一つとして扱う
'fgrep' => false, # fgrep と同じ
'nkf' => '-eZ1m' # 文字種統一のため
}
while ARGV[0] =~ /^-/
$_ = ARGV.shift
opt['binary'] = true if ~/b/
opt['email'] = true if ~/e/
opt['filename'] = true if ~/f/
opt['ignorecase'] = true if ~/i/
opt['kanji'] = true if ~/k/
opt['filenameonly'] = true if ~/l/
opt['multi'] = true if ~/m/
opt['lineno'] = true if ~/n/
opt['spaceone'] = true if ~/s/
opt['revertmatch'] = true if ~/v/
opt['fgrep'] = true if ~/F/
if ~/^-V/
print RCS_ID, "\n"
exit
end
usage if ~/[^\-befiklmnsvVF]/
end
usage if ARGV.size < 1
require 'nkf'
pattern = NKF.nkf(opt['nkf'], ARGV.shift)
pattern = Regexp.quote(pattern) if opt['fgrep']
re = Regexp.new(pattern, opt['ignorecase'])
match = 0
opt['filename'] = true if ARGV.size > 1
if ARGV.size == 0
class IO; def path; '-' end end
match |= if rg(re, STDIN, opt) then 1 else 0 end
else
while filename = ARGV.shift
next unless (st = File.stat(filename)) && st.file? && st.readable?
f = File.zopen(filename)
match |= if rg(re, f, opt) then 1 else 0 end
f.close
end
end
if match == 0
exit 1
end
grep コマンドなのですが、できるだけ手軽に使いたいということがあって次のような特徴を持ちます。 特徴というか、私の希望ですね。
「結果を表示する」ためと、「検索する」ことをわけて扱っています。 検索するためには、キーワードのマッチを考えて前処理をいくつか行っています。 特に漢字のドキュメントでは、普通はスペースや改行を無視して扱いたいので、前処理が必要になっています。 これは、オプションを指定した場合の動作です。 結果は、前処理後のものを表示するのではなく、オリジナルを表示する必要があるので「保存」しています。
デフォルトの動作として、引数で渡されてしまったテキスト以外のファイルを無視するようにしています。 これは、手軽にシェルのワイルドカード「*」を使えるようにするためです。 ですから、いきなり
とか、できてしまいます。$ cd /bin $ rg.rb sh * igawk:#! /bin/sh igawk: shift igawk: --) shift; break;; igawk: -W) shift igawk: shift;; igawk: shift;; igawk: shift;; igawk: shift;; igawk: shift igawk: shift remadmin:#!/bin/sh remadmin: shift
漢字のための前処理もデフォルトにしたいかなと思ったのですが、オーバーへッドが気になったので、オプション指定時だけにしています。
ちょい、扱いがめんどうだったのが、入力をブロックで扱ったときの行番号です。 最初どうしようか悩みました。 もっとスマートな方法があればいいのですが、目的は達成できているのでとりあえず?
rg.rb を Emacs 上で使うための Emacs-Lisp です。 もとは、mg.pl 用だったりしますけど。
;; rg.rb 2000/7/10
(defun rg (command)
"Run rg instead of grep."
(interactive "sRun rg (with args): ")
(require 'compile)
(compile-internal (concat "rg.rb -n " command " /dev/null")
"No more rg hits" "rg"))
漢字を使用する場合には、整形プログラム(roff 系)や w3m などでスペースが入ったりします。 漢字オプションを加えると、漢字と漢字の間のスペースを無視するようになっています。 文字種統一も考えていています。 漢字オプションを指定すると、「検索するパターン」と「検索対象」を同じ nkf モジュールのオプションで処理します。 このため、文字種の違いによる検索がはずれるということが起きにくくなります。 検索対象が改行によって二行に分断されている場合も対応しています。 E-mail の引用時のように、行頭に引用の「|」や「>」が含まれる場合も。 使っていくうちに調整が必要になると思いますが、なかなか便利です。
grep(1) コマンドの grep って「g/re/p」なのはご存じですよね? って、ed(1) はしりません?
「global」に「/regular expression(正規表現)/」を検索して「print」するという感じです。
こんな感じで使ったりするんです。
$ ed ChangeLog
303885
g/Ruby/p
* eval.c (rb_eval): the value from RTEST() is not valid Ruby
* regex.c (re_match): now understands interrupts under Ruby.
n
3927 * regex.c (re_match): now understands interrupts under Ruby.
ファイル名を指定すると、そのサイズが最初に表示されます(303885)。
コマンドとして「g/Ruby/p」を入力します。
「g」全体を「/Ruby/」Ruby で検索して、結果を「p」表示する指定です。
「p」がなければ、表示はありません。
「n」コマンドを使っていますが、これは行番号付で行を表示するということです。
終了は「q」です。
書き込むときは「w」をお忘れなく。
オンラインマニュアルにコマンドの説明などがありますので、ちょっとだけ試してみるのもおもしろいと思います。
vi(1) が使えないときに役立つかもしれませんよ?
ラインエディタ(ed)って、普通使わないですよね。 これでプログラム書いたんですよ。 すごいですね。 また、スクリプトとしてファイルの編集に ed(1) も使っていました。 いまは Ruby などの便利なスクリプト言語があるので、ほとんどみませんけど。
昔々は、ターミナルがテレタイプ(キーボードとプリンタ用紙)だったんですよ。 そういう環境では、スクリーンエディタなんて使えませんよね。 ed(1) なら、キーボードから入力して、プリンタに出力されるだけでも使えます。 ちなみにいまのプリンタと違って、これも一行一行文字を出力するくらいしかできないものですけど。 こういう環境だったので、ls(1) みたいなコマンド名使っていたりしたんですよ。 しばらくして、ターミナルがキャラクタベースのものになってきて、ようやく vi(1) のようなスクリーンエディタがでてきました。 BSD で Bill Joy さんが作ったものです。 このとき、遅いモデム(300bps とか)でも動くように表示領域を小さくするとかいろいろ工夫していたりするんです。
私が UNIX を使いはじめたときは vi(1) はありましたので、ed(1) だけを使うということはありませんでした。 でも、learn(1) という自己学習コマンド(CAI)があって、それで勉強しました。 ファイルを編集するようなスクリプトを作成する場合にも使えたことと、vi(1) が動かなくなったときのために勉強したものです。 vi(1) がいつでも動くとは限らないですから。
追加したいオプションとしては
というのがあります。 件数表示のほうは、そんなにややこしくなさそうですね。 前後の行については、保存しておいて必要なタイミングで表示すればできるかな。 「前」はいいですけど「後」はややこしい? 現状では、オプション
-C 数字前後の行を表示する-cヒットした件数だけ表示する
-m (行をまたいだ検索) を使うことで、「空行ブロック」単位に表示します。
これである程度代用できちゃいます。
みなさんは、grep のオプションでほしいなと思うのはなんでしょう?
私はほとんど困っていないのですが...
fgrep オプション -F サポート記念の追加です。
grep の仲間として egrep や fgrep があります。
egrep は、awk と同様に拡張正規表現がサポートされています。
fgrep は fixed grep ということで、正規表現に使用されるメタ文字をそのまま検索します。
けして fast grep ではありません(よく間違う?)。
開発された当時は、検索の処理速度が違っていて、通は?「普段から egrep を使っていた」なんて話もありました。
また、egrep の拡張正規表現がキーワードの区切り(Ruby での「\b」)などを指定できてかなり便利なものでした。
速度の話は、むかしむかしの話ですので、現在は違っているかな。
リファクタリングかな。
$_
依存の書きかたを変更。
オプションの渡しかたの変更。
文字種指定の
[]
に
-
を使うときに、たとえ先頭の
-
でも
\
が必要と警告が表示されるための対応。
fix 拡張子の定義の「.」をエスケープ
fgrep オプション -F のサポート
ruby-1.7.1 2001-08-06 対応です
一度 File::stat して、その後この情報を再利用します
オプション -l で、ファイル名だけの表示ができなかったので bug fix しました