フィルタとは, 連続的に入力されたデータに対して,規則的な加工を施し(ほどこし), その結果のデータを出力するようなプログラムのことである.
Unix コマンドの多くは,単純な機能しかもたないフィルタであるが, 次のように,複数のフィルタをパイプで連結することによって, 複雑な機能を実現できるようになっている:
$ コマンド1 < 入力ファイル | コマンド2 | コマンド3 | ... | コマンドn > 出力ファイル
今回は,テキストデータ中の文字列パターンに対して作用するタイプの フィルタコマンドについて実験する.
まず,文字列パターンを表現するための 正規表現(regular expression)について説明する.
シェルのファイル名置換では, 特殊記号(* など)を使って, 複数のファイルの集合を短い文字列(グロブパターン) で表現することができた:
$ ls
boo.c foo.c woo.h
$ echo *.c # グロブパターン *.c が boo.c と foo.c を表現
boo.c foo.c
正規表現では,これとよく似た方法 (同じ方法とは言っていない!! 別の方法)で, 文字列の集合を表現する. 正規表現の例を Table 1 に示しておく.
グロブパターンと正規表現は,一見とても似ているが,実際にはかなり違う. どちらにも共通の特殊記号(* や ? など)が利用されるが, 意味が異なるので混同しないよう注意せよ.
グロブパターンは,シェル独自の機能であって, 実在するファイル名の文字列しか取り扱えない. 一方,正規表現は,さまざまな Unix コマンドに共通の機能であって, 文字列なら何にでも利用できる. (すべてのコマンドで使えるとは言っていない!! 正規表現が使えないコマンドもある.)
パターンの例 | 意味 | マッチする文字列の集合 |
---|---|---|
1 文字の表現 | ||
a | 文字 a そのもの | "a" |
[abc] | 文字 a,b,c のうちのどれか 1 文字 | "a","b","c" |
[a-z] | 文字 a,b,c,...,z のうちのどれか 1 文字 | "a","b","c",...,"z" |
[^a-zA-Z] | アルファベット以外の 1 文字 | "3","=","あ" |
. | 任意の 1 文字 | "A","b","3","=","あ" |
文字列の表現 | ||
a+ | 文字 a の 1 個以上の連続 | "a","aa","aaa",... |
a* | 文字 a の 0 個以上の連続 | "","a","aa","aaa",... |
a? | 0 個または 1 個の文字 a | "","a" |
a{2,4} | 文字 a の 2 〜 4 個の連続 | "aa","aaa","aaaa" |
位置の表現 | ||
^a | 行の先頭の文字 a | 省略 |
a$ | 行の末尾の文字 a | 省略 |
文字列のグループ化・選択 | ||
(string) | 文字列 string のひとまとまり | 省略 |
\1 | 1 番目の文字列グループ | 省略 |
alpha|beta | 文字列 alpha または beta のどちらか | "alpha","beta" |
正規表現 "(pen)pine(apple)\2\1" → マッチする文字列 "penpineappleapplepen"
なお,正規表現には, 基本正規表現と拡張正規表現の2種類があり, これらの間で,パターンの記述方法が微妙に異なる. Table 1 では,拡張正規表現を使用している. また,Table 1 以外にも,さまざまなパターンがある. より詳しい説明については,次の資料を参照しよう:
$ man grep または man 1 grep
$ man 7 regex # man regex だと NG
では実際に,grep を利用して, 正規表現によるパターン検索を試してみよう. grep は,パターンにマッチした行だけを入力データから抽出する 「検索フィルタ」である. 基本的な使用方法は次の通り:
$ grep -E '拡張正規表現' ファイル ...
拡張正規表現で検索する場合,オプション -E を明示すること. grep 日本語マニュアル を参考にしながら実験を進めよう.
なお,以下の実行例では, テキストデータとして英単語データファイル /usr/share/dict/words を利用する:
$ less /usr/share/dict/words # サンプルデータを確認 ... $ grep -E 'regular' /usr/share/dict/words # "regular" を含む行を検索 contraregular contraregularity ... regularization ... unregular $ grep -E 'regular$' /usr/share/dict/words # "regular" で終わる行を検索 contraregular ... unregular $ grep -E 'ex.*sion' /usr/share/dict/words # "exなんとかsion" を検索 antiexpansionist coexplosion coextension ...
なお,grep の検索パターンについては, 特殊記号(*,?,!,等)が, grep へ渡される前に, シェルによって展開されてしまい, 期待した結果を得られない場合がある. この余計な展開を抑止するためには,いつでも上の例のように シングルクォートしておくのが安全だ. (「"」では NG,必ず「'」 を使うこと.)
正規表現の特殊記号は,コマンドライン内では, シェルのグロブパターンやヒストリ置換などの特殊記号と見分けがつかない.
ユーザが正規表現を指定したつもりであっても, シェルは正規表現をまったく理解できないので, そこにあるファイル名や前に使ったコマンドに置き換えてしまうことになる.
正規表現をクォートせずに使って,おかしな結果が出たとしても, シェルは自分にできることだけをバカ正直にやってくれているだけだ. 適切な指示を出さなかった人間が悪い. (こういう残念な状況は,人間対人間でもよくある... コミュニケーション能力の問題...)
$ grep -E '拡張正規表現' /usr/share/dict/words A AA ... ZIP ...
$ grep -E '拡張正規表現' /usr/share/dict/words aftertreatment # 4つ含むものや... anitinstitutionalism ... anticonstitutionalist # 5つ含むものも... ...
sed は, 指定された特定の文字列(検索パターン)を 別の文字列(置換パターン)へ変換する 「置換フィルタ」である. sed 日本語マニュアル を参考にしながら実験を進めよう.
$ man sed
sed の基本的な利用方法は次の通り:(拡張正規表現の場合)
$ sed -E 's/検索パターン/置換パターン/' 入力ファイル
このコマンドを使うと, テキストファイル内の多数のスペルミスの修正や名前の変更を一気に実行できる. たとえば,Cのソースコードとして, "printf" ではなく "pritnf" と打ってしまう病気は, 次のようにすると簡単に治療できる:
$ cat bug.c main() { int a, b; int kotae; pritnf("a > "); scanf("%d", &a); pritnf("b > "); scanf("%d", &b); kotae = a + b; pritnf("%d + %d = %d\n", a, b, kotae); } $ cc bug.c コンパイルエラー:pritnf って何よ? ... $ sed 's/pritnf/printf/' bug.c > ok.c $ cc ok.c
また,変数名を "kotae" から "answer" に変えたければ, 次のようにする:
$ sed 's/kotae/answer/' ok.c > int.c
ただし,調子に乗りすぎないこと. 次のようにはしない方がよい:
$ sed 's/int/double/' int.c > double.c
これだと,"printf" も置換されてしまう. 意味不明な文字列 "prdoublef" に...
次に,特殊記号の正規表現も使ってみよう:
$ grep -E '^ex.*sion$' /usr/share/dict/words excision exclusion excursion ... $ grep -E '^ex.*sion$' /usr/share/dict/words | sed -E 's/(ex)(.*)(sion)/\2/' ci clu cur ...
この例では,"exなんとかsion" を "なんとか" へ置換している. ("ex" と "sion" の部分を破棄して,"なんとか" の部分だけを抽出している.)
注意:行内にマッチする部分が複数個ある場合, デフォルトでは,最初の部分だけしか置換されない. すべての部分を置換するには 's/.../.../g' とすればよい.
エディタ vi(vim)の操作中にも,実は, sed と同じ置換機能を利用できる. vi のノーマルモードで:
:%s/検索パターン/置換パターン/
これはとても便利なので,vi ユーザは是非活用しよう.
sed コマンドで, printf に影響を与えずに int 型だけを置換する方法を考えよ.
ヒント:int を単語の一部ではなく, ひとつの単語として取り扱うということ.
大ヒント:単語としての int の場合, 次のような構成になっている.
これを正規表現で書けばよい. (括弧は \(,タブは \t,先頭は ^, 文字の or は [...], 文字列の or は ...|... である.)
実行例:
$ ls *.txt card.txt # この入力テキストファイル(テンプレートファイル)を用意しておく.. $ cat card.txt # ...テンプレの中身はこんな感じに Dear, #NAME#. A happy new year. $ ./mkcards.bash card.txt Amy Becky Chucky ... # 宛名は何人分でも OK に... $ ls *.txt Amy.txt Becky.txt Chucky.txt ... card.txt $ cat Amy.txt Dear, Amy. # "#NAME#" の部分が "Amy" に置換されている A happy new year.
やりたいことは,要するに,差し込み印刷だ.
この課題では sed のリダイレクト先を複数のデータファイルとしている. 実用上の問題点として,このままでは, メール送信したりハガキ印刷したりする手間が別途必要になってしまう.
しかし,リダイレクト先をプリンタ出力フィルタや メール送信フィルタに変えるだけで簡単に, 実用的なダイレクトメール印刷 or スパムメール送信のシステムへ改造できる. (ただし,気軽にやってはいけません.)
ヒント:
プログラミング言語 AWK を使うと, フィルタコマンドを簡単に自作できる. 次回の予習として AWK について調べておこう.