Chienomi

Unixの基本ツールsedを便利に使う

Live With Linux::technique

sed(1)はUnixの古典的ツールである。

多くの人がLinuxというものに触れたときに初めて畏怖する要素でもある。 自らコマンドラインでsedを叩くのは、初級Linuxerにとって最初の目標と言えるのではないだろうか。

しかしである。 なぜか人々はsedsコマンドしか使わない。

確かにちょっとした機械的置換をしたいときにsコマンドが便利なのは事実だし、sedでホールドスペースを使うのが不便なのも事実だが、sedにはもっといろいろと使いどころがある。

そこでここではsedの覚えておけば即使えるような実践的な基礎と、複雑化したときに代替として使えるperl(1)での表記について紹介しておこう。

なお、GNU sedとそうでないsedには機能的差異があり、私は日頃GNU sed以外を使うことがないので、ここでは前提としてGNU sedを使うものとする。 つまり、実質的にLinuxの話である。

sedの基本

コマンドライン

sedの書式は次のようになっている。

sed [-V] [--version] [--help] [-n] [--quiet] [--silent]
    [-l N] [--line-length=N] [-u] [--unbuffered]
    [-E] [-r] [--regexp-extended]
    [-e script] [--expression=script]
    [-f script-file] [--file=script-file]
    [script-if-no-other-script]
    [file...]

なんか妙に独特な書き方になっているが、つまりはこういうことだ

sed [options...] script [file...]
sed [options...] -f script-file [file...]
sed [options...] -e script... [file...]

基本的にはスクリプトが1つだけであれば

sed script [file...]

とし、複数あるなら

sed -e script -e script [file...]

のようにすることになる。

そして頻繁に使うオプションが-n, -iだ。

その前に前提としてsedは行単位で読んで行単位で処理するプログラムである。 ここは重要なので覚えておいてほしい。

sedは読み込んだ行をパターンスペースに格納し、パターンスペースに対してコマンドを適用し、パータンスペースの内容を出力するのがデフォルトである。 このため、行を出力したくない場合はdコマンドを使ってパターンスペースを消去することになる。

-nオプションはコマンド実行後、パターンスペースを出力しない。 このため、逆に出力したい場合にはpコマンドが必要になる。

-iコマンドは、ファイルに対して編集結果を書き込む。

リダイレクトはコマンドの実行より先に行われるため、例えば

sed 's/abc/def/' foo > foo

のようなコマンドは成立しない。sedfooを読む前にfooは書き込みモードでオープンされて空になってしまうからだ。 ファイルの編集を行いたい場合は-iオプションを使うことになる。

perlはsedとawkに毛が生えたようなものなので、同じように1行読んで処理するオプションに-n-pがある。 perl感覚で言えば-nは普通に1行読むだけで、-p$_を自動出力する。 $_がパターンスペースのようなものとして使われるわけだ。

ruby(1)にも同じようなオプションがあるのだが、こういう挙動をするのに適した機能がないためやや不便であり、なおかつRubyは対象を省略する書き方は順次消しているため、使う機会はあまりない。

sedの文法

sedはsedという言語であり、プログラミング言語としての文法を持っている。 ただし、プログラミングで使うような機能をコマンドラインで使うことは基本的にないし、sedで書くメリットも怪しいため、もっとシンプルな文法として使うことが多い。

sedの基本文法は

ADDRESS COMMAND

である。

これは「アドレスに一致する場合、コマンドを実行する」というものになる。 アドレスを省略するとすべての行に対して適用される。

addr1,addr2という書き方もでき、これはaddr1にマッチしたところからADDRESSは真となり、addr2にマッチすると偽になるというのを繰り返すスイッチとなる。 addr1にマッチした行にも、addr2にマッチした行にも適用される。 (つまり、終端を含む)

COMMANDがADDRESSに一致した場合に適用する内容。

よく見かける

s/abc/def/

というのはADDRESSなしのsコマンドである。

アドレス

一番使うのはregexp。 例えば/^#/と書けば、#で始まる行を対象にできる。

数値は行番号になる。単体では使わないが、レンジで書くときにはたまに使う。

first~stepの形式も、ほぼ1~2もしくは2~2ではあるが使う。 これは、firstが起点行で、stepおきに真になる。 長い行を見やすいように分割するために1~100とかすることもある。

!をつけることで反転も可能。

コマンド

一番使うのはもちろんsコマンド。 s/regexp/replacement/という書式になっていて、正規表現で一致した内容をreplacementに置き換える。 manpageに記載がないけど、通常はパターンスペースの最初マッチに適用され、最後にgフラグをつけるとすべてに適用される。

ついでdpコマンド。 dはパターンスペースを消去し、pはパターンスペースを出力する。 p-nオプションと併用するのが普通。

aiは行単位で追加する。 aはパターンスペースの後ろに、iはパターンスペースの前に行を追加する。

manpageではaiはバックスラッシュをはさむように書いてあるけど、現行バージョンのsedではバックスラッシュなしでも普通に動作する。

qは頻繁に使うわけではないけれど、これ以上の処理をしないため、ある特定の行までを出力したいという場合に使用する。 qだと今パターンスペースにある行は出力されるため、出力せずに打ち切りたいときはQを使う。

実用例

置き換え

まずは置き換え。 これはよく見かけるもの。

sed s/…/……/g foofile

三点リーダーを倍にする。perlでもほぼ同じ

perl -pe s/…/……/g foofile

エディタの一括置換でも正規表現が使えれば同じようなものなので、一番使うけれどあんまり使う必要性はないものだったりする。

セパレータ

sed '100~100 a ---'

100行目から100行ごとに---を入れて分割する。 sedで相当簡単に書ける部類で、perlだと無駄に長い

perl -pe 'if ((($. == 100)..(0)) && ($. % 100 == 0)) {$_ .= "---\n"; }'

だいぶ魔術なので、ワンライナーを分解して解説

if (   # 処理条件 (2つ)
    # 条件1: フリップフロップ
    # 行番号が100のときon
    # フロップ条件が偽なのでoffにはならない
    (( == 100)..(0))
    &&
    # 条件2
    # 100で割り切れるとき真
    # これが例えば105~100の場合は
    # $. % 100 == 5
    ( % 100 == 0)
   ) {
  # 現在の行にセパレータ行を追加
   .= "---\n";
}

行抽出

実例より。

sed -n '/^#EXTINF/ p' concept-playlist-ringtone.m3u

perlでもほとんど同じ。

perl -n '/^#EXTINF/ && print;' concept-playlist-ringtone.m3u

m3uプレイリストのEXTINF行を抽出している。 これはタイトルとアーティストを抜き出すのが目的なので、後でもっと楽をするなら

sed -n '/^#EXTINF/ { s/^#EXTINF:[0-9]*.[0-9]*,//; p }'

とすると、もっとちゃんと抜き出せる。 これは、ブロックを使って複数のコマンドをひとつのアドレスに対して適用している。

perlの場合

perl -ne 'if (/^#EXTINF/) { s/^#EXTINF:\d*.\d*,//; print; }'

行削除

実例より。

sed -i -e '/^\s*$/ d' -e '/^特性/ d' 特性.csv

^\s*$は(スペースを含んでも良い)空行である。 これで空行を削除する。

続いて、特性で始まる行も削除する。

これが一体なんだったのかは覚えていない。

perlの場合

perl -pie 'if (/\s*$/ || /^特性/) { $_ = ""; }'

行追加

これは実例から。 Nginxで新しくglobal/base.confserverディレクティブで読むようにしたかったので

sed -i '/server\s*{/ a \ \ include global/base.conf;' *

ファイルとして*を指定しており、カレントディレクトリのすべてのファイルに対して(-iオプションにより)書き換えを行う。

server\s*{serverディレクティブの行を対象にすることができる。 これでserverディレクティブの行がパターンスペースに入った状態。

aコマンドでパターンスペースの後ろにincludeを追加する。 serverディレクティブの直後の行に入るので、これで確実に入るはずである。 (serverディレクティブのブレースを分けて書いているということがない限り)

perlの場合

perl -pie '/server\s*{/ && $_ .= "include global/base.conf;\n"'

おまけ: perlが楽なケース

sedで短く書ける場合はPerlのほうがいいケースはほとんどない。 強いて言うなら、sedは異なるsed間での互換性問題があるため、シェルスクリプトに入れるならperlにしておいたほうが互換性は上がる。

perlのほうが楽に書けるのは、

  • 複合条件がある場合
  • 入力行の内容を数値的に判定する場合
  • その行にない情報を参照する必要がある場合

がほとんど。

#!/bin/perl -p
if (/^\[(.*)\]$/) { # [abc]のような行
  $directive = ; #覚えておく
}

if ($directive =~ /^server_/) {#server_で始まるディレクティブの中だけ
  s/foo/bar/g; # fooをbarにする
}

でもこれをワンライナーにするとかなり読みづらい。

perl -pe 'if (/^\[(.*)\]$/) {$directive = $1;}' -e 'if ($directive =~ /^server_/) {s/foo/bar/g;}'

「sedよりperlのほうがいい」となる場合はだいたいワンライナーではつらい内容のことが多くスクリプトファイルにしたいが、スクリプトファイルにするのなら別にPerlでなくてもいいじゃんになりやすい。

#!/bin/ruby
directive = nil
ARGF.each do |line|
  directive = $1 if line =~ /^\[(.*)\]$/
  line.gsub!("foo", "bar") if directive[0,7] == "server_"
  puts line
end

このため活躍するのは、主に裏で処理するようなタイプのシェルスクリプトの中に組み込みたいときになる。