細かくて伝わらないsedの使い方

LispをやるひとはLisper、RをやるひとはRおじさんというらしいですが、じゃあSedをやるひとは?

Sederというらしいです。

Seders's grab bag - Tutorials

一人前のSederになるために、Sedの細かい話をします。うそです。あんまり細かくないですけど、たぶんこれですら伝わらない予感…

Sedの基本文法

Sedの文法は単純明快です。

アドレス コマンド

という順番になっています。

コマンドはsとかpとかアルファベット一文字です。引数の形は様々です。

アドレスというのは、「この正規表現に引っかかる行」とか「何行目から何行目」とかいう条件です。アドレスにあてはまる行にだけコマンドが実行されます。アドレスは省略可能で、省略した場合はかならず実行されます(AWKと同じノリですね)。コマンドによって使うアドレスのタイプが違ったりするみたいですが、その辺には踏み込まないことにします(あんまりよく知らない)。

たとえばさっきの

s/hoge/fuga/

とかいうのは、アドレス→省略、コマンド→s、です。残りはコマンドの引数です。とか言うと「えっ、/hoge/がアドレスじゃないの??」とかいう反応がありそうですが、そこはただのコマンドの引数です。

アドレスを使ってみる

たとえば、こういうファイルがあるとします。

user:    yutani
admin:   true
expired: true

これのadminだけfalseにしたいときはどうしますか?

sed 's/^admin:   true$/admin:   false/' test.yml

とかやってませんか。これだと長いし、空白の数が変わったりすると動かなくなります。じゃあ空白は正規表現にするかーとか言い出すと、途端にややこしくなります。

sed 's/^admin:\( \+\)true$/admin:\1false/' test.yml

読みづらい。

こういうときはアドレスを使って書くとすっきりします。(感じ方に個人差はあるかもしれませんけど...)

sed '/admin/ s/true/false/' test.yml

adminがある行の、truefalseに置き換えます。(アドレスとコマンドの間の空白はあってもなくてもどちらでも)

あと、{}で括って複数のコマンドを実行することもできます。

sed '/admin/{s/admin/Administator/; s/true/false/}' test.yml

s以外のコマンドも使ってみる

個人的な感覚としては、世の中の9割の人はsコマンドしか知らないんじゃないかという気がしますが、Sedにはいろいろコマンドがあります。いくつか使ってみます。

d: 行を削除

これはアドレスと組み合わせて使います。たとえばadminの行だけ削除したいときは

sed '/admin/ d' test.yml

とかやります。これだけならgrep -vといっしょじゃん、という話ですが、アドレスは正規表現マッチ以外もいろいろあって、例えば行数を指定して操作したいときとかは便利です。以下は1行目を消す例です。

sed '1 d' test.yml

c: 行を置換

アドレスと組み合わせて使います。指定した文字列で行を丸ごと置き換えます。たとえば、adminが含まれる行を# (deleted)という文字列と置き換えるのはこんな感じです。

sed '/admin/c # (deleted)' test.yml

a,i: 行を追加

これもアドレスと組み合わせて使います。aはそのアドレスの後に指定した文字列の行を追加します。iはアドレスの前に追加します。

ちょっと分かりづらいと思うので実行結果も添えておくと、↓それぞれこんな感じになります。

$ sed '/admin/a I AM HERE' test.yml
user:    yutani
admin:   true
I AM HERE
expired: true

$ sed '/admin/i I AM HERE' test.yml
user:    yutani
I AM HERE
admin:   true
expired: true

n: 次の行に進む

これはちょっと説明が難しいです。正直、正しい使い方を理解できていません。。

これは例えば、以下のようなJSONファイルでparam2valueだけを書き換えたい、みたいな、処理が複数行にまたがるケースを考えましょう。(注:ここまでくるとたぶんSedでは荷が重いです。Pythonなりでスクリプトを書くことを検討しましょう)

{
  "param1": {
    "value":  1,
    "name":   "foo"
  },
  "param2": {
    "value":  23,
    "name":   "bar"
  }
}

そんなときは、「param2の次の行を読み込んで、数字を0に置き換える」という感じで書けます。

sed '/param2/{n; s/[0-9]\+/0/}' test.json

ほんとは/value/とかのアドレスを指定したいですが、{}の中にさらにアドレスを書いたりはできないのでこうなっています。正直あんまりきれいなやり方じゃないです。

あと、私のようなゆるふわSederではカラテが足りないんですが、ラベルという概念を使うとこんな風にも書けます。

sed '/param2/{n; b cr}; b; :cr; /value/ s/[0-9]\+/0/' test.json

が、ここまでくるともう誰にも(半年後の自分も含む)読めないので、こんなコマンドを書くのはよっぽどのことがない限り避けましょう。これがどういう意味か興味がある方は、以下のSOを読むとわかるかもしれません。

s正規表現を指定するときの豆知識

を書こうと思ったんですが、ひとつしか思いつきませんでした。しかもみんな知ってそう。

正規表現のパターンに/が含まれるとき

たとえばこういうファイルで、misc/workworkdirに置き換えたいとします。このとき、/正規表現の区切り文字とかぶるので少し対処が必要です。

[workspace]
dir    = /home/yutani/misc/work
mode   = 0755
create = yes

ひとつは\でエスケープするという方法があります。

sed 's/misc\/work/workdir/' test.ini

もうひとつは、区切り文字を変える方法です。sコマンドの後に指定した任意の文字が区切り文字になります。なんでもいいんですけど,とかをよく見る気がします。

sed 's,misc/work,workdir,' test.ini

アドレスも同じく区切り文字を変えることができます。\の後に指定した任意の文字が区切り文字になります。

sed '\,misc/work,d' test.ini

まとめ

sedは奥が深いのであんまり深入りしないようにしましょう!(あれっ...?)