sed basics - ストリームエディタの基礎

[sed における基本正規表現] [sed における拡張正規表現]
[2014/03/01新規] [2014/05/27更新]

Contents

主な形式

sed [-n] [options] [-e] 'script' [file...]

主なオプション

簡単な説明

ストリームエディタ sed は一行ずつ入力しフィルタする。スクリプトを指定することにより様々な編集を施すことができる。

まず、入力行は、末尾の改行コード '\n' が取り除かれパターンスペースに入る。特に、'\n' は行の追加の区切りに用いられる。

パターンスペース強制出力の抑止をしていなければ、自動的にパターンスペースが標準出力される。

sed スクリプトの概要

利用者は、パターンスペースと呼ばれるバッファに対して、コマンドにより、文字置換や正規表現による置換が出来る。 また、アドレスやコマンドリストのグループで柔軟な編集が可能となる。 さらに、パターンスペースに加えて、ホールドスペースと呼ばれるバッファを利用することが出来る。また、 指定したラベルへの遷移、正規表現による条件付き遷移、入力ファイルとは別の外部ファイルとの入出力などができる。

コマンド

sed スクリプトのコマンドは以下の通りである。

:label"b", "t" コマンドでの移動先を指すラベル
#commentコメント行
[address]=行番号の標準出力コマンド
[address]a\^J直下のテキストのパターンスペースへの追加コマンド。複数行の場合は行末の改行を '\' でエスケープ。追加は '\n' に続く
[address]i\^J直下のテキストの標準出力コマンド。複数行の場合は行末の改行を '\' でエスケープ
[address]q終了コマンド。パターンスペース強制出力の抑止がされてなければパターンスペースを標準出力、終了。
[address]r fileファイルを標準出力コマンド
[address][,address]{command;... }コマンドリストのグループ
[address][,address]b [label]ラベルへの遷移コマンド。ラベルを省略した場合はスクリプトの最後へ遷移
[address][,address]c\^J直下のテキストのパターンスペースへの書換コマンド。スクリプトの最初へ遷移
[address][,address]dパターンスペースの消去コマンド。スクリプトの最初へ遷移
[address][,address]Dパターンスペースの最初の行の消去コマンド。パターンスペースがまだ残っているならパターンスペースに新たな入力はしない。そして、スクリプトの最初へ遷移
[address][,address]gホールドスペースのパターンスペースへの書換コマンド
[address][,address]Gホールドスペースのパターンスペースへの追加コマンド。追加は '\n' に続く
[address][,address]hパターンスペースのホールドスペースへの書換コマンド
[address][,address]Hパターンスペースのホールドスペースへの追加コマンド。追加は '\n' に続く
[address][,address]n次の入力行のパターンスペースへの置換コマンド。パターンスペース強制出力の抑止がされてなければパターンスペースを標準出力。次の入力行がなければ、スクリプトの最後へ遷移、終了
[address][,address]N次の入力行のパターンスペースへの追加コマンド。行番号が変わることに注意。次の入力行がなければ、スクリプトの最後へ遷移、終了
[address][,address]pパターンスペースの標準出力コマンド
[address][,address]Pパターンスペースの最初の行の標準出力コマンド
[address][,address]s/BRE/replacement/flags正規表現による置換
[address][,address]t [label]直近の置換が成功した時のラベルへの遷移コマンド。ラベルを省略した場合はスクリプトの最後へ遷移
[address][,address]w fileパターンスペースのファイルへの追加コマンド
[address][,address]xパターンスペースとホールドスペースの交換コマンド
[address][,address]y/srcstr/dststr/文字置換

コマンドをセミコロン ';' または改行で繋げるとコマンドリストになる。

アドレス

前節の address は以下の通りである。

numberパターンスペースへの入力の行番号
/BRE/パターンスペースが基本正規表現にマッチした行
$最終行
address!アドレスの否定

BRE は基本正規表現(Basic Regular Expressions)であるが、オプションで ERE - 拡張正規表現(Extended Regular Expressions)を書くことも可能。

正規表現による置換先の主な特殊文字

正規表現による置換の主な flags

正規表現におけるエスケープ文字と GNU 拡張

POSIX sed におけるエスケープ文字は以下の通り。

GNU sed における拡張エスケープ文字は以下の通り。

例題

代表的な Unix のコマンドに相当する sed スクリプトを以下にあげる。

`cat`

	sed -e ''

このように、cat と同じ sed スクリプトは「空」となる。但し、パターンスペース強制出力の抑止をしない。

[sed] [Awk] [Perl] [Ruby] [Python]

`head -n 1`

	sed -ne '1p'

このように、head -n 1 と同じ sed スクリプトは「1p」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 1`

	sed -ne '$p'

このように、tail -n 1 と同じ sed スクリプトは「$p」となる。

[sed] [Awk] [Perl] [Ruby] [Python]
	sed -e '8q'

このように、head -n 8 と同じ sed スクリプトは「8q」となる。但し、パターンスペース強制出力の抑止をしない。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 8`

さて、`head -n 1`, `tail -n 1`, `head -n n` は以上のように大変簡単であるが、`tail -n n` は少々難しい。現に GNU sed では、二つの例を割いて実現方法を示している。しかし、一例目は POSIX sed では正しく動作しないし、二例目は少々冗長である。ここではより堅牢で単純な実現方法を示そう。

	sed -ne '
:b
$p
8,$!{
  N
  b b
}
N
D'

最終行のみパターンスペースを出力するが、8行目から最終行まで以外、つまり1行目から7行目までは、入力行をパターンスペースに追加してラベル "b" へ遷移し、それ以外の8行目から最終行までは、入力行をパターンスペースに追加して、パターンスペースの最初の行を除去している。

このように、tail -n 8 と同じ sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -l`

	sed -ne '$='

このように、wc -l と同じ sed スクリプトは「$=」となる。

ところで、`wc -l` は以上のように大変簡単であるが、`wc -c`, `wc -w` は面倒である。現に GNU sed では、wc -c の例wc -w の例が示されている。こんなトリッキーなことが必要となるなら perl 等の他の言語を採用すべきだ。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep '^$'`

	sed -ne '/^$/p'

このように、grep '^$' と同じ sed スクリプトは「/^$/p」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -v '^$'`

	sed -ne '/^$/!p'

このように、マッチの否定、grep -v '^$' と同じ sed スクリプトは「/^$/!p」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E '^.+'`

	sed -E -ne '/^.+/p'

このように、拡張正規表現の grep -E '^.+' と同じ拡張正規表現の sed スクリプトは「/^.+/p」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E -v '^.+'`

	sed -E -ne '/^.+/!p'

このように、拡張正規表現のマッチの否定、grep -E -v '^.+' と同じ拡張正規表現の sed スクリプトは「/^.+/!p」となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6`

	sed -E -e 's/^([^:]*):([^:]*):([^:]*):([^:]*):([^:]*):([^:]*):.*/\1:\6/'

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行はそのまま出力と同じ拡張正規表現の sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6 -s`

	sed -E -ne 's/^([^:]*):([^:]*):([^:]*):([^:]*):([^:]*):([^:]*):.*/\1:\6/p'

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行は出力しないと同じ拡張正規表現の sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold -b`

この例、1行あたりの文字数(既定値は80)を越えたら改行を挿入する sed スクリプトを示そう。これは 'P', 'D' コマンドの好例となっている。

	sed -E -e '
s/^(.{80})(.*)$/\1\
\2/
P
D'

行頭から任意の 80 文字とその残りを改行で区切り、行頭から改行までを 'P' コマンドで標準出力、そして 'D' コマンドでその残りを同様に処理する。'D' コマンドでは、その残りが無くならない限り、新たな入力がなされないので、このように実現できる。

このように、fold -b と同じ拡張正規表現の sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tee filename`

	sed -e 'w filename'

このように、標準入力を標準出力とファイルに書き出し、tee と同じ sed スクリプトは「w filename」となる。但し、パターンスペース強制出力の抑止をしない。

[sed] [Awk] [Perl] [Ruby] [Python]

`tr 'A-Za-z' 'N-ZA-Mn-za-m'`

この例、ROT13(と呼ばれる暗号化と言うより難読化)は tr コマンドを使うと表題のように簡単に実現できる。しかし、類似の sed'y' コマンドは文字コードの範囲を表せないので、非常に使い勝手が悪い。

	sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm/'

このように、文字置換、tr 'A-Za-z' 'N-ZA-Mn-za-m' と同じ sed スクリプトは以上のようになる。但し、パターンスペース強制出力の抑止をしない。

他の sed'y' コマンドの用例としては、GNU sed の cat -ncat -b にて、「'y/0123456789/1234567890/'」のように行番号をインクリメントする中途に用いられている。こんなトリッキーなことが必要となるなら perl 等の他の言語を採用すべきだ。

[sed] [Awk] [Perl] [Ruby] [Python]

`uniq`, `uniq -d`, `uniq -u`

これらは GNU sed で示される好例となっており、そちらに譲る。

  1. `uniq` - 重複する行を一行にする
  2. `uniq -d` - 重複する行のみを一行にして表示する
  3. `uniq -u` - 重複しない行のみを表示する

どの例も 'N' コマンドで入力行をパターンスペースの末尾に '\n' に続いて追加し、正規表現にて '^\(.*\)\n\1$' のように後方参照 '\1' を利用していることにある。

[sed] [Awk] [Perl] [Ruby] [Python]

`expand`

このタブを複数の空白に置換するコマンドを sed で実現するのは少々難しい。まず、拡張正規表現の GNU sed であれば多少簡単であり、以下のようになる。

	sed -r -ne '
h
:b
/^\n/{
  s/\n//g
  p
  b
}
/^$/p
/^([^\n\t]{0,7}\t|[^\n\t]{1,8})(.*)/{
  s/^([^\n\t]{0,7}\t|[^\n\t]{1,8})(.*)/\1/
  x
  s/^([^\n\t]{0,7}\t|[^\n\t]{1,8})(.*)/\2/
  x
  s/([^\t]{7})\t$/\1 /
  s/([^\t]{6})\t$/\1  /
  s/([^\t]{5})\t$/\1   /
  s/([^\t]{4})\t$/\1    /
  s/([^\t]{3})\t$/\1     /
  s/([^\t]{2})\t$/\1      /
  s/([^\t]{1})\t$/\1       /
  s/([^\t]{0})\t$/\1        /
  H
  g
  b b
}'

一方、POSIX sed だと '[^\n]' が使えないので、苦肉の策で、拡張正規表現の POSIX sed においては以下のようになる。

	sed -E -ne '
h
:b
/^\n/{
  s/\n//g
  p
  b
}
/^$/p
/^([[:print:]]{0,7}	|[[:print:]]{1,8})(.*)/{
  s/^([[:print:]]{0,7}	|[[:print:]]{1,8})(.*)/\1/
  x
  s/^([[:print:]]{0,7}	|[[:print:]]{1,8})(.*)/\2/
  x
  s/([^	]{7})	$/\1 /
  s/([^	]{6})	$/\1  /
  s/([^	]{5})	$/\1   /
  s/([^	]{4})	$/\1    /
  s/([^	]{3})	$/\1     /
  s/([^	]{2})	$/\1      /
  s/([^	]{1})	$/\1       /
  s/([^	]{0})	$/\1        /
  H
  g
  b b
}'

いずれも、行頭からタブストップまで分割して、タブ空白置換を施した上で、分割した余りが保持されているホールドスペースに追加、この処理を最初の追加が現れるまで行ない、改行を除いて出力している。

このように、expand と同じ拡張正規表現の sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`unexpand -a`

この複数の空白をタブに置換するコマンドを sed で実現するのは少々難しい。まず、拡張正規表現の GNU sed であれば多少簡単であり、以下のようになる。

	sed -r -ne '
h
:b
/^\n/{
  s/\n//g
  p
  b
}
/^$/p
/^([^\n\t]{0,7}\t|[^\n\t]{1,8})(.*)/{
  s/^([^\n\t]{0,7}\t|[^\n\t]{1,8})(.*)/\1/
  x
  s/^([^\n\t]{0,7}\t|[^\n\t]{1,8})(.*)/\2/
#  /^ /{
#    x
#    s/ {1,}$/\t/
#    b c
#  }
  x
  s/([^\t]{7})\t$/\1 /
  s/ {2,}$/\t/
#:c
  H
  g
  b b
}'

ちなみに、コメント行は本質的には不要であるが、完全に `unexpand -a` の挙動を再現するにはコメントを外す必要がある。

一方、POSIX sed だと '[^\n]' が使えないので、苦肉の策で、拡張正規表現の POSIX sed においては以下のようになる。

	sed -E -ne '
h
:b
/^\n/{
  s/\n//g
  p
  b
}
/^$/p
/^([[:print:]]{0,7}	|[[:print:]]{1,8})(.*)/{
  s/^([[:print:]]{0,7}	|[[:print:]]{1,8})(.*)/\1/
  x
  s/^([[:print:]]{0,7}	|[[:print:]]{1,8})(.*)/\2/
#  /^ /{
#    x
#    s/ {1,}$/	/
#    b c
#  }
  x
  s/([^	]{7})	$/\1 /
  s/ {2,}$/	/
#:c
  H
  g
  b b
}'

ちなみに、コメント行は本質的には不要であるが、完全に `unexpand -a` の挙動を再現するにはコメントを外す必要がある。

いずれも、行頭からタブストップまで分割して、空白タブ置換を施した上で、分割した余りが保持されているホールドスペースに追加、この処理を最初の追加が現れるまで行ない、改行を除いて出力している。

このように、unexpand -a と同じ拡張正規表現の sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`rev`

行毎に文字列を反転する BSD rev(1) コマンド。実用したことはないが、GNU sed における実現例が秀逸なので、ここでさらに単純にして紹介する。

	sed -e '
s/^.*$/\
&\
/
:x
s/\(\n.\)\(.*\)\(.\n\)/\3\2\1/
t x
s/\n//g'

行を入力したらまず行頭行末から改行コードで挟んで、改行コードを印に両端の '\n.''.\n' を交換していく。すると両端の一文字を交換しながら改行コードが中央に移動していき、中央が '\n.\n' もしくは '\n\n' となったら置換がなされないので、改行を除去しつつ次のサイクルに遷移する。但し、オリジナルにある不要な判定は除去した。

このように、BSD rev(1) と同じ拡張正規表現の sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tac`, `tail -r`

最終行から先頭行まで逆順で出力する GNU tac コマンド、`tail -r` に同じ。これも実用したことはないが、GNU sed における、好ましくない実現例は以下の通り。

	sed -ne '
1!G
h
$p'

ホールドスペースにファイルの内容すべて逆順で保持して最後にすべて出力するという処理なので、そこに書いてある通りメモリが足りなくなるか、仮想メモリで非常に遅くなるだろう。確かに他の方法も思いつかないが、そもそも 'fseek' 等がない sed にそぐわないことをさせるなら C や perl 等の他の言語を採用すべきだ。

[sed] [Awk] [Perl] [Ruby] [Python]

`strings -a [-n 4]`

この例、ファイルの連続した印字可能な4文字以上の文字列を表示する sed スクリプトを示す。この例では strings.sh という実行権のついたファイルに記述するものとする。まず、拡張正規表現の GNU sed であれば多少簡単であり、以下のようになる。

#!/bin/bash
n=4
while [ "$1" != "" ]; do
  case "$1" in
  -n)	shift; n="$1"	;;
  *)	break	;;
  esac
  shift
done
${SED:-/usr/local/bin/sed} -r -ne '
  /([\f[:print:]]{'"$n"',})/{
    s/([\f[:print:]]{'"$n"',})(.*)$/\
\1\
\2/
  s/^[^\n]*\n//
  P
  D
}
' "$@"

この strings.sh では、'-n 4' オプションが指定でき、このように、オプション解析はシェルに任せ、スクリプトにそれを埋め込めば柔軟な sed スクリプトが書ける。

一方、POSIX sed だと '[\f]' が使えなかったり '[^\n]' が使えないので、苦肉の策で、拡張正規表現の POSIX sed においては以下のようになる。

#!/bin/ksh
n=4
while [ "$1" != "" ]; do
  case "$1" in
  -n)	shift; n="$1"	;;
  *)	break	;;
  esac
  shift
done
${SED:-/usr/bin/sed} -E -ne '
  /([^L[:print:]]{'"$n"',})/{
    s/([^L[:print:]]{'"$n"',})(.*)$/\r\
\1\
\2/
  s/^[^\r]*\r\n//
  P
  D
}
' "$@"

但しここで、'^L' はフォームフィードそのものであることに注意。

このように、strings -a と同じ sed スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

参考文献

  1. sed, IEEE Std 1003.1.
  2. GNU sed.
Written by Taiji Yamada <taiji@aihara.co.jp>