sed - Stream EDitor (文字列置換)
Table of Contents |
sed 
sed とは Stream EDitor の略で基本的に文字列置換のために使用するコマンドです(他にもいくつかの機能があります)。 例えば、
% sed 's/ika/tako/' [filename]
とすると、[filename] で指定したファイル中の最初に見つかった ika という文字列を tako に変えて、標準出力に出力します。 出力をファイルに保存したい場合はリダイレクトを利用して、
% sed 's/ika/tako/' [in_file] > [out_file]
のようにします。ファイルそのものを書き換えてしまいたい場合は、
% sed -i 's/ika/tako/g' [filename]
のように -i オプションを使用します。-i[SUFFIX]、例えば -i.bak のようにすると、[filename].bak のようなバックアップをとっておいてくれます。
標準入力も受け付けるのでパイプからの入力を受けることもできます。
% cat [filename] | sed 's/ika/tako/'
その行で最初に見つかったもののみではなく、すべて置換したい場合は、
% sed 's/ika/tako/g' [filename]
のように g フラグを使用します。基本的に常につけておいて良いぐらいだと思います。
詳しくは man か sed, a stream editor が参考になります。
sed の正規表現 
s/[regexp]/[replacement]/[flags]
のようになっています。[regexp] には正規表現を使用できます。正規表現 を学んでおきましょう。
文字列置換時に [replacement] でマッチしたパターンを参照したい場合は \[数字] を使用します。
% sed 's/.*\(apple|orange\)/\1/g'
この場合 \1 は apple か orange のいずれかになります。 \(\) をもう1つ後ろに追加した場合、そのパターンを参照するには \2 を使用することになります。 \0 はマッチ全体「.*\(apple|orange\)」になります。 [regexp] で前方でマッチしたパターンを参照したいすることもできます。
% sed 's/\(apple|orange\)\1/hogehoge/g'
appleapple か orangeorange にマッチしたら置換されます。
sed ではグループ化を意味する () は \(\) のようにエスケープする必要があります。 perl, php などではそのまま () です。emacs は sed 同様 \(\) です。
コマンドラインで実行時には、正規表現の * などがシェルにワイルドカードとして展開されないように、's/[regexp]/[replacement]/[flags]' のようにクォーテーションをつけるくせをつけておくとよいです。
複数ファイル内の文字列置換 
複数ファイル内の文字列置換を行いたい場合は
% ls | xargs sed -i.bak 's/[regexp]/[replacement]/g'
のように xargs コマンドと併用します。しかし、これだと特に置換対象文字列がない場合でもファイルの変更時間が更新されてしまうので、
% grep -l '[regexp]' *.txt | xargs sed -i.bak 's/[regexp]/[replacement]/g'
のようにしたほうが良さそうです。grep の -l はマッチ行ではなく、マッチ行のあったファイル名を出力するオプションです。 ディレクトリ階層を再帰的に辿って置換したい場合は
% grep -rl '[regexp]' *.txt | xargs sed -i.bak 's/[regexp]/[replacement]/g'
とするか、より柔軟には、
% find . -name '*.txt' | xargs grep -l '[regexp]' | xargs sed -i.bak 's/[regexp]/[replacement]/g'
とします。find には多くのオプションがあるのできめ細かい制御ができそうです。
文章単位での置換 
sed では行ごとに文章を読み込み、行単位で処理をします。 文章全体で文字列置換をしたい場合は、sed では苦しいので、ここでは perl を使用してみます。
% perl -e '$_=join("",<>);s/[regexp]/[replacement]/g;print;' < [filename]
全行読み込み、すべてくっ付けてから文字列置換し、プリントアウトしています。*1
例をあげます。file という名の以下の内容のファイルがあるとします。
<div> This is div. </div>
次のようにすると、文章先頭に HEAD を追加できます。文章全体に対して処理しているので、^ は行頭ではなく文章頭のみにマッチしているわけです。
% perl -e '$_=join("",<>);s/^/HEAD/g;print;' < file
HEAD<div> This is div. </div>
次のようにすると、 <div></div> 内部の文字は変えずに <div></div> を <p></p> に変えることができます。
% perl -e '$_=join("",<>);s!<div>([^<>]*)</div>!<p>$1</p>!gs;print;' < file
<p> This is div </p>
s// の区切り文字を !! にしています。</div> の / を <\/div> のようにエスケープせずにすむようになるので楽です。 !gs の s は . を改行文字にもマッチさせる修飾子です( [^<>]* の部分で改行文字にもマッチするように)。
閑話休題: perl には、バグというか変な仕様がありまして、
% perl -e '$_=join("",<>);s/$/TAIL/g;print;' < file
と $ を使用して文章末を置換しようとすると
<div> This is div </div>TAIL TAIL
のように2度置換されてしまったりします。
php の perl コンパチブルな正規表現関数 preg_*** では、このおかしな挙動をわざわざ忠実に再現していたりします。PHP - パターン修飾子 - m (PCRE_MULTILINE)。おかしな話ですね。
*1 これは perl -e '$_=join("",<IN>);$_=~s/[regexp]/[replacement]/g;print $_;' < [filename] と同等です。