Sedの基本

注)この記事を書くにあたり、動作確認した環境は mac 。mac 標準の sed は BSD sed とか POSIX sed と言われているものだが、広く一般的に使用されている GNU sed を導入して適宜動作確認した。サンプルコード内の sed コマンドは POSIX sed (BSD sed) であり、gsed コマンドは GNU sed である。

sedの基本法則

  1. スクリプト中のコマンドは、入力ファイルのそれぞれの行に順に適用される。
  2. コマンドは、行アドレスを指定しなければ、全ての行にグローバルに適用される。
  3. コマンドは編集結果を標準出力に出力する。入力ファイルは変更されない。

sedの処理の流れとパターンスペース

  1. パターンスペースという作業領域に、入力ファイルの1レコードが読み込まれる。ただし末尾の改行コードは含まない。
  2. スクリプト内のコマンドリストが順にパターンスペースの行に適用される。 その結果、パターンスペースの行の内容が変更される。
  3. コマンドリストの最後まで実行したら、パターンスペースの内容を標準出力に出力する。このとき再度改行コードが追加される。
  4. パターンスペースがクリアされる。
  5. 次に読み込み可能な行があれば1.に戻って繰り返す。なければsedを終了する。

※ホールドスペースという特定コマンドで使用される作業領域もある。

sedの記述形式

1sed [-Enr] [-e 'command'] [-f scriptfile] file
  1. 「-E」または「-r」を指定すると、コマンド(command)のパターンを拡張正規表現として扱う。
  2. 「-n」を指定すると、デフォルト出力を抑制する。例えば、通常、コマンドの終了後にパターンスペース内容を標準出力に出力するが、このオプションを指定すると出力されなくなる。
  3. 「-e」は次の引数がコマンドであることを指定するもの。
  4. 「-e 'command1' -e 'command2'」のように複数のコマンドを指定できる。ただ1個のコマンドだけであれば、「-e」は省略可能。
  5. 「-f scriptfile」を指定すると、ファイル(scriptfile)に記載されたコマンドを実行する。
  6. 末尾の「file」はコマンドを適用するファイル。複数記載できる。
  7. command の囲み文字をダブルクォートにすると、シェル変数やコマンド置換結果を利用できる。
1$ echo 今日はyyyy年mm月dd日です | sed "s/yyyy年mm月dd日/$(date +'%Y年%m月%d日')/"
2今日は2023年04月22日です

コマンドの記述形式とアドレスによる適用範囲の制限

1[address]{command-list}  #[](ブラケット)は省略可能を示す
  1. アドレス(address)が指定されなければ、コマンドリスト(command-list)は全ての行に適用される。
  2. アドレスが1個だけ指定されると、コマンドリストはそのアドレスにマッチする全ての行に適用される。
  3. アドレスがカンマ区切りで2個指定されると、それぞれのアドレスにマッチした行の範囲内に対して、コマンドリストが適用される。
  4. アドレスは、行番号(行アドレス)・正規表現(パターンアドレス)のいずれかを指定する。 正規表現を使う場合は「/」で括る。
  5. アドレスを2個指定する場合、行番号と正規表現を混ぜて使うことができる。(もちろん混ぜなくても良い)
  6. アドレスの後に「!」を付けると、アドレスにマッチしない全ての行にコマンドリストが適用される。
  7. コマンドリストがただ1個のコマンドであれば、{}(ブレース)は不要。
  8. コマンドリストの各コマンドは「;」で区切って1行で記述することができる(a, i, cコマンドを除く)。 なお,最後に記述したコマンドと同じ行に「}」を記述する場合は,コマンドの後ろに「;」を記述する必要がある。
  9. コマンドリストの個々のコマンドに個々のアドレスを指定することができる。
  10. 「#」以降はコメントになる。処理系により1行目にしか書けなかったり、どこでも書けたりする。
  11. スクリプトの先頭レコードの行頭に「#n」を書いた場合、デフォルト出力を抑制する。(-nオプション指定と同じ)
  12. 後方参照は、一致した正規表現の前の部分を参照する正規表現コマンド。 バックスラッシュと 1 桁の数字 (\#) で指定する。 それらが参照する正規表現の部分は部分式 (subexpression) と呼ばれ、括弧で指定される。 後方参照と部分式は、s コマンドの置換部分で使用され、さらに GNU sed では (アドレスを含む) 正規表現検索パターンでも使用できる。 \# を指定すると、正規表現検索パターン内の()で指定した # 番目の部分式に一致する文字列に置き換わる。
 1$ # アドレスの部分式は s コマンドでは参照できないようだ
 2$ echo -e "bb\nbc\ncc" | gsed -E '/([bc])\1/s@\1\1@\1@'
 3gsed: -e expression #1, char 20: Invalid back reference
 4$
 5$ # s コマンドの検索パターンを無指定にするか、再記述する
 6$ echo -e "bb\nbc\ncc" | gsed -E '/([bc])\1/s@@\1@'
 7b
 8bc
 9c
10$ echo -e "bb\nbc\ncc" | gsed -E '/([bc])\1/s@([bc])\1@\1@'
11b
12bc
13c

コマンドの種類

置換(s)

1[address]s/pattern/replacement/flags
  1. 区切り文字の「/」は別文字でも可。
  2. パターン(pattern)が指定されなかった場合、パターンアドレス(address)にマッチしたのと同じパターンを置換する。
  3. フラグ(flags)に数字Nが指定された場合、パターンスペースでN回目にマッチしたパターンのみ置換する。
  4. フラグに「g」が指定された場合、パターンスペースでマッチするもの全てを置換する。この指定がなければ最初にマッチした物のみ置換する。
  5. フラグに「p」「w file」が指定された場合、置換が実行された時だけパターンスペースの内容を出力する。「p」の場合は標準出力へ、「w」の場合は指定のファイル(file)へ出力する。
  6. フラグに「i」または「I」が指定された場合、大文字と小文字を区別しない方法で正規表現をマッチングする。
  7. 異なるフラグを組み合わせて使える。
  8. replacementの部分に「&」を指定すると、パターンにマッチした文字列に置き換わる。
1# exsample
2echo -e 'abc' | sed 's@a@z@'  # zbc
3echo -e 'abc' | sed -n '/b./s//z/p'  # az
4echo -e 'aaa' | sed 's/a/z/2' # aza
5echo -e 'aba' | sed 's/a/z/g' # zbz
6echo -e 'aa\nbb' | sed -n 's/a/z/p'  # za
7echo -e 'aa\nbb' | sed -n 's/b/y/gp' # yy
8echo -e 'abc' | sed 's/b./(&)/' # a(bc)
9echo -e '2022/08' | sed -E 's/([0-9]{4})\/([0-9]{2})/\2-\1/' # 08-2022

削除(d)

1[address]d
  1. アドレスにマッチする行の場合、パターンスペースの内容を削除する。
  2. d コマンド以降にコマンドを記載しても、パターンスペースが空のため何も実行されない。
  3. 次の行がパターンスペースに読み込まれて、制御がスクリプトの先頭に戻る。
1# exsample
2echo -e 'aa\nbb' | sed '2d' # aa
3echo -e 'aa\nbb' | sed '/b/d' # aa

追加(a)、挿入(i)、変更(c)

 1#Posix sed
 2追加 [address]a\
 3     text
 4挿入 [address]i\
 5     text
 6変更 [address]c\
 7     text
 8#GNU sedの場合は以下も可だが、{}内では使えない。
 9追加 [address]a text
10挿入 [address]i text
11変更 [address]c text
  1. a コマンドはパターンスペースの現在行の下に指定のテキスト(text)を入れる。
  2. i コマンドはパターンスペースの現在行の上に指定のテキストを入れる。
  3. c コマンドはパターンスペースを指定のテキストで置き換える。
  4. a, i, c の各コマンドの後ろに「\」(バックスラッシュ)を入れて改行し、テキストを入力する。テキストが複数行になる場合は、各行末に「\」を入れる(但し最終行は不要)。
  5. a, i の各コマンドには、アドレスに単一の行を指定する。
  6. c コマンドのアドレスには、単一行、範囲、いずれも指定可能。但し、範囲指定されたコマンドリストで c コマンドを使用した場合、単一行指定されたものとして動作する。
  7. a, i の各コマンドは、パターンスペースを変更しない。よって、追加挿入されたテキストはその後のコマンドの影響を受けない。デフォルト出力抑制しても出力される。
  8. c コマンドはパターンスペースをクリアするので、d コマンドと同じ効果がある。すなわち、c コマンドの後続コマンドは実行されず、次行が読み込まれてスクリプトの先頭に戻る。
1# example
2echo -e 'aa\nbb' | gsed '2a cc' # aa\nbb\ncc
3echo -e 'aa\nbb' | gsed '2i cc' # aa\ncc\nbb
4echo -e 'aa\nbb' | gsed '2c cc' # aa\ncc
5echo -e 'aa\nbb' | gsed '1,2c cc' # cc
6echo -e 'abc' | gsed -e 'i 999' -e 's/9/3/' # 999\nabc パターンスペースに影響しないので置換は実行されない

リスト(l)

使用頻度低いので記載は未定

変換(y)

1[address]y/abc/xyz/
  1. 文字列abcの各文字を文字列xyzのそれと同じ位置にある文字に変換する。(a→x, b→y, c→z)
  2. グローバルに変換される。
1# example
2echo -e 'abcabc' | sed 'y/b/y/' # aycayc

表示(p)

1[address]p
  1. パターンスペースの内容を標準出力に出力する。
  2. デフォルト出力を抑制しない場合、重複して表示される。

行番号の表示(=)

1[address]=
  1. アドレス(address)の行番号を出力する。

次行読み込み(n)

1[address]n
  1. パターンスペースの内容を標準出力に出力して、スクリプトの先頭に戻らずに、次行をパターンスペースに読み込む。
1#example
2echo -e 'a\nb\nc' | gsed -n '2n;p' # a\nc#

ファイル読み込み(r)、書き出し(w)

1[address]r file
2[address]w file
  1. 読み込みコマンド(r)は、アドレス(address)で指定した行の次行にファイル(file)の内容を出力する。 N, n コマンド実行時やスクリプトの最後に到達して次行を読み込む前に、ファイル内容が標準出力に出力される。 デフォルト出力抑制されていない場合、パターンスペース内容を出力後にファイル内容が出力される。
  2. 書き出しコマンド(w)は、パターンスペースの内容をファイルに書き出す。
  3. r, w コマンドの直後の空白から改行までがファイル名と見做される。
  4. r コマンドはファイルが存在しなくてもエラーにならない。w コマンドはファイルがなければ作成し、あれば上書きする。但し一つのスクリプト中で同じファイルへの書き込みコマンドが複数あれば、追記される。

終了(q)

1[address]q
  1. スクリプトを終了する。スクリプトの途中に書かれていた場合、以降のコマンドは実行されないし、次の行の読み込みも行われない。

高度なコマンド

複数行のパターンスペースを扱うコマンド(N, D, P)

1[address]N # 次行を読み込んでパターンスペースの後ろに追加
2[address]D # パターンスペースの先頭から改行までを削除
3[address]P # パターンスペースの先頭から改行までを出力
  1. N コマンドはパターンスペースの後ろに次の入力行を追加する。元の内容と追加された行は改行(\n)で区切られる。
    ファイル末尾まで読み込み済みの状態で N コマンドを実行する場合、POSIX sed(BSE sed) と GNU sed で動作が異なる。
    ファイル末尾に達しているので sed コマンドは終了するが、GNU sed の場合はパターンスペースの内容を標準出力に出力して終了する一方、POSIX sed の場合は出力せずに終了する。
  2. D コマンドはパターンスペースの先頭から最初の改行までを削除する。実行後にスクリプトの先頭に戻るが、次の入力行の読み込みは行わない。
    スクリプトの先頭から処理を始めようとする時、パターンスペースの先頭行がパターンアドレス(address)にマッチしなければ、現在のパターンスペースに対するスクリプトは実行されず次行の処理に進む。
  3. P コマンドはパターンスペースの先頭から最初の改行までを標準出力に出力する。
1# example
2$ echo -e 'UNIX\nLINUX\nWIN' | gsed '/UNIX/{N;P;D}'
3UNIX
4LINUX
5WIN
6$ echo -e 'UNIX\nLINUX\nWIN' | gsed -n '/UNIX/{N;P;D}'
7UNIX

上の example の2つの sed の違いは、デフォルト出力抑制の有無だけである。
2つの sed の出力結果の比較により、「LINUX」「WIN」がデフォルト出力されていることがわかる。
N, P, D コマンドの理解を深めるため、下側の sed (上記 example の6行目) の処理を確認する。

  1. パターンスペースに1行目の「UNIX」が読み込まれる。
  2. N コマンドにより次行が読み込まれ、パターンスペースは「UNIX\nLINUX」となる。
  3. P コマンドによりパターンスペースの先頭から改行までの文字列「UNIX」が出力される。
  4. D コマンドによりパターンスペースは「LINUX」となる。さらにスクリプトの先頭に戻る(次行は読み込まれない!)。
  5. パターンスペースの「LINUX」はパターンアドレス /UNIX/ とマッチしないので、「LINUX」に対するスクリプトは実行されず、次行の処理に進む。・・・ここが理解の肝
  6. 「WIN」はパターンアドレス /UNIX/ とマッチしないので、「WIN」に対するスクリプトは実行されず、次行の処理に進む。但し、ファイル終端に達しているので sed コマンドを終了する。

ホールドスペースを扱うコマンド(h, H, g, G, x)

1[address]h # パターンスペースの内容をホールドスペースに上書き
2[address]H # パターンスペースの内容をホールドスペースの後ろに追加
3[address]g # ホールドスペースの内容をパターンスペースに上書き
4[address]G # ホールドスペースの内容をパターンスペースの後ろに追加
5[address]x # ホールドスペースの内容とパターンスペースの内容を交換
  1. H, G コマンドは追加前に改行を入れる。
1# example1 「1」を含む行と「2」を含む行の入れ替え
2$ echo -e '1\n2' | gsed '/1/{h;d};/2/G' # 2\n1
  1. 最初の入力行「1」がパターンスペースに格納される。
  2. 「1」はパターンアドレス /1/ にマッチするので、h コマンドによりホールドスペースに格納される。
  3. d コマンドによりパターンスペースがクリアされ、次の入力行の処理に進む。
  4. 次の入力行「2」がパターンスペースに格納される。
  5. 「2」はパターンアドレス /2/ にマッチするので、G コマンドにより現在のパターンスペースの内容「2」の後ろに改行を挟んでホールドスペースの内容「1」が追加される。
  6. ファイル末尾に達したので、パターンスペースの内容を出力して sed を終了する。
 1# example2 追記型logファイルで直近の(最後に追加された)Start〜Endを出力
 2$ cat sample.log
 3time 13時00分
 4Start
 5S1 正常
 6S2 正常
 7End
 8info
 9info
10time 13時10分
11Start
12S1 正常
13S2 異常
14End
15info
16info
17$ cat sample.log | gsed -n '/Start/,/End/H; /Start/h; ${x;p};'
18Start
19S1 正常
20S2 異常
21End
22$
  1. sed でファイルの先頭から順次読み込まれるが、-n を使っているのでデフォルト出力は抑制される。
  2. 「Start」の行は /Start/,/End/H のパターンアドレスにマッチするので、H コマンドによりホールドスペースの後ろに「Start」が追加される。
  3. さらに /Start/h のパターンアドレスにもマッチするので、h コマンドによりホールドスペースが上書きされて「Start」となる。
  4. ファイル終端を示すパターンアドレス「$」にはマッチしないので、{x;p} は今は実行されない。
  5. 次の入力行「S1 正常」がパターンスペースに読み込まれる。依然として /Start/,/End/Hのパターンアドレスにマッチするので、H コマンドによりホールドスペースの後ろに追加される。ホールドスペースの内容は「Start\nS1 正常」となる。/Start/h と ${x;p} の各パターンアドレスにはマッチしないので、次の入力行の処理に移る。
  6. 上記5.と同様の処理が入力行「End」まで繰り返され、ホールドスペースの内容はStart〜Endの内容となる。但しこの時点ではファイル末尾に達していないので、${x;p}のパターンアドレス「$」にマッチせず、出力は行われない。
  7. 入力ファイルの一番下の「Start」が読み込まれたときも同様に処理される。このとき、/Start/h により、もともとホールドスペースに入っていた内容は上書きされる。よって最後のStart〜Endがホールドスペースに残る。
  8. 入力行がファイル末尾まで達したら、${x;p} のパターンアドレス「$」にマッチするので、x コマンドによりパターンスペースの内容がホールドスペースの内容と交換され、p コマンドで出力される。なお、最後の x コマンドは g コマンドでも可。

高度なフロー制御コマンド(:, b, t)

1:label
2[address]b[label]
3[address]t[label]
  1. 「:」は、b および t コマンドに指定したジャンプ先のラベル(label)を定義する。「:」自体は何も処理を行わない。
  2. b コマンドは、制御をスクリプト内に定義したラベルにジャンプさせる。ラベル未指定時はスクリプト末尾にジャンプする。
    b コマンドの使用例はこちら → コメント行を削除する sed スクリプト
  3. t コマンドは、入力レコードが読み込まれてから or 直前に実行された t コマンド以降で、s コマンドによる置換が行われた場合に限り、指定のラベルにジャンプする。ラベル未指定時はスクリプト末尾にジャンプする。
1# example フロー制御によりカンマをパイプに置換する
2$ echo -e 'a,b,c' | gsed -n ':top; p; s/,/|/; ttop'
3a,b,c
4a|b,c
5a|b|c

※置換コマンドの g フラグを使ってs/,/|/gと書くのが簡単で同様の結果を得られるが、フロー制御の理解のためあえて上記exampleのような処理を記述した。

  1. 入力行「a,b,c」がパターンスペースに読み込まれる。
  2. :top では何も処理されない。
  3. p コマンドにより、現在のパターンスペースの内容「a,b,c」が出力される。
  4. s コマンドにより、パターンスペースの最初のカンマがパイプに置換される。パターンスペースの内容は「a|b,c」となる。
  5. ttop コマンドにより、ラベル top に制御がジャンプする。
  6. p コマンドにより、現在のパターンスペースの内容「a|b,c」が出力される。
  7. s コマンドにより、パターンスペースの最初のカンマがパイプに置換される。パターンスペースの内容は「a|b|c」となる。
  8. ttop コマンドにより、ラベル top に制御がジャンプする。
  9. p コマンドにより、現在のパターンスペースの内容「a|b|c」が出力される。
  10. s コマンドに指定されているパターンは、現在のパターンスペースにマッチしないので、置換は実行されない。
  11. 置換が実行されなかったので、ttop コマンドによるジャンプは行われず、スルーして次のコマンドに進む。
    しかしすでにスクリプト末尾に達しているので、sed 自体を終了する。

参考サイト

JP1/Advanced Shell sedコマンド(テキスト中の文字列を置換する)
Linux and UNIX Man Pages SED POSIX Programmer's Manual
FreeBSD Manual Pages SED

関連ページ