Unixの基本ツールsedを便利に使う
Live With Linux::technique
- TOP
- Articles
- Live With Linux
- Unixの基本ツールsedを便利に使う
序
sed(1)
はUnixの古典的ツールである。
多くの人がLinuxというものに触れたときに初めて畏怖する要素でもある。
自らコマンドラインでsed
を叩くのは、初級Linuxerにとって最初の目標と言えるのではないだろうか。
しかしである。
なぜか人々はsed
でs
コマンドしか使わない。
確かにちょっとした機械的置換をしたいときに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
のようなコマンドは成立しない。sed
がfoo
を読む前に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
フラグをつけるとすべてに適用される。
ついでd
とp
コマンド。
d
はパターンスペースを消去し、p
はパターンスペースを出力する。
p
は-n
オプションと併用するのが普通。
a
とi
は行単位で追加する。
a
はパターンスペースの後ろに、i
はパターンスペースの前に行を追加する。
manpageではa
やi
はバックスラッシュをはさむように書いてあるけど、現行バージョンの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.conf
をserver
ディレクティブで読むようにしたかったので
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 = $1; #覚えておく
}
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
= nil
directive ARGF.each do |line|
= $1 if line =~ /^\[(.*)\]$/
directive .gsub!("foo", "bar") if directive[0,7] == "server_"
lineputs line
end
このため活躍するのは、主に裏で処理するようなタイプのシェルスクリプトの中に組み込みたいときになる。