sedで複数行にまたがった文字列を置換する方法

2020-02-06Bash

はじめに

sed コマンドで置換を行う場合、 複数行にまたがった文字列も置換対象としたい場合があります。

検証環境

$ uname -moi
x86_64 MacBookPro11,4 Darwin

$ bash -version | head -n 1
GNU bash, バージョン 5.0.11(1)-release (x86_64-apple-darwin18.6.0)

$ sed --version | head -n 1
sed (GNU sed) 4.2.2

置換対象の文字列が複数行にまたがる場合とは?

sed はラインエディタの思想を受け継いでいるため、 基本的に入力情報を1行ずつ受け取って処理を行 います。

例えば、 % で囲まれた部分を空文字に置換するといった場合、合致パターンが1行で収まるため容易に置換できます。

$ echo 'xx%aaa%yyy'
xx%aaa%yyy

$ echo 'xx%aaa%yyy' | sed 's/%.*%//'
xxyyy

ところが、 合致パターンが複数行にまたがったときにはうまく置換してくれません。

# 2つの%の間に改行が含まれている
$ echo 'xxx%aaa
bbb%yyy'
xxx%aaa
bbb%yyy

# 置換されない
$ echo 'xxx%aaa
bbb%yyy' | sed 's/%.*%//'
xxx%aaa
bbb%yyy

2つの対処法を取り上げます。

1.置換対象の文字列が複数行にまたがる場合は -z オプションを使う

sed-z オプションを利用することで、簡単に対処することができます。
これは sed のデフォルトの挙動である「行単位の処理」から「ヌル文字単位の処理」に挙動を切り替える、というオプションになります。

注意しなければならないのは、GNU版のsedでしか利用できないオプションである点です。Linuxの場合はGNU版のsedが導入されているディストリビューションが多いですが、Macの場合にはGNU版のsedを brew install しましょう。

どうしてもGNU版のsedを利用できない場合については後述します!

通常のテキストにヌル文字が入っていることはありませんので、これによりテキスト全体を読み込み、一度に置換するようになります。

$ echo 'xxx%aaa
bbb%yyy' | sed -z 's/%.*%//'
xxxyyy

2.GNU版sedが利用できない場合

GNU版のsedが利用できないケースも当然あります。
その場合には、以下のコマンドをお試しください。

# ちゃんと置換されていますね!
$ echo 'xxx%aaa
bbb%yyy' | sed ':l; N; s/%.*%//; b l;'
xxxyyy

sed のコマンド部分のみ簡単に解説します。

今回取り上げたコマンドは ; を区切り文字として4つの処理で構成されています。

  1. :l
  2. N
  3. s/%.*%//
  4. b l

それぞれのフェーズで行っている処理は以下のようになります。

  • :goto文におけるラベル のようなものを設定します。ここでは l というラベルを設定していることになります。
  • N は、1行データを読み込んで sed のバッファ領域に蓄えます。
  • sed のバッファ領域を s 内部コマンドで置換します。
    • 置換ができた場合はバッファの内容が出力されます。
    • 置換ができなかった場合はバッファの内容は据え置きされます。
  • b l は、ラベル l に戻る命令になります。

最終的に残ったバッファ領域の内容は、処理終了直前に出力されます。

ひとこと

sed -z はかなり強力で、例えば「連続して並んだ空白行は1つにする」といった処理も簡単に実現できます。

# 空白行が多い
$ cat test.txt
    android

    Linux
    windows

    windows

    Linux

# スッキリ!
$ sed -z 's/\n\{3,\}/\n\n/g' < test.txt
    android

    Linux
    windows

    windows

    Linux

注意しないといけない点があります。
通常は行単位に実行される sed を全文字列を一度に処理することになるため、メモリ負荷が増えます。

あまりに巨大なファイルを処理する場合にはメモリ不足の注意が必要です。

2020-02-06Bash