sedによるコメントアウト+空行破棄+継続行処理 (複数行処理の定石?)


sedによる複数行処理の定石?

データファイルや設定ファイルなどの可読性をよくするため、その書式に継続行を許す仕様としたいことが多々ある。その場合どうしても複数行の処理が必要となるが、いろいろ調べるとsedでの複数行処理に関しては定石のようなやり方があるようなのでメモしておくことにする。(以下はmacOS標準のsedで試した。拡張正規表現をつかうため-Eオプション付きでsedを呼ぶことを前提としている。)

複数行を処理する基本となる2つの要点

  • sed-nオプションをつけて呼ぶ。デフォルトでの標準出力への表示を止める。
  • N(次の行をパターンスペースに追加して、処理の行番号もインクリメント),P(パターンスペースの最初の行だけ標準出力に表示),D(パターンスペースの最初の行を削除して最初の処理からやり直す。)を用いてループ処理する。

「何も足さない、なにも引かない」: 基本的な構造

下記のファイルを% sed -nE -f sample1.sed input-fileと呼ぶと、単にファイルの内容を何も変えずに表示するだけであるが、これが定石的な処理の基本のようだ。つまり、パターンスペースには常に二行を保持し、パターンスペースの前の行を処理したのち、パターンスペースの前の行を捨ててから次の行の処理のループに行く。

sample1.sed
$!N
P
D

第1行で次の行をパターンスペースに追記する。この際行番号もインクリメントされるので、最終行で行うとここで処理が終わってしまうので、最終行では行わないようにアドレス指定が必要.第2行で先頭行だけ表示して、第3行で先頭行を処理して最初から処理を再開

継続行処理を追加

下記の例では、改行の前に\(バックスラッシュ)がある場合と、行頭に4つの空白文字がある場合には、継続行として連結して表示する。(% sed -nE -f sample2.sed input-file)

sample2.sed
:begin
$!N
/(\\\n|\n[[:blank:]]{4})/{
  s/[[:blank:]]*(\\\n|\n[[:blank:]]{4})[[:blank:]]*/ /g
  t begin
}
P
D

第3行目で継続行の条件に合う場合は、前後の空白文字とまとめて一つの空白に置き換えている。この処理でパターンスペースが2行→1行になってしまうため、処理の先頭に戻って次の行を読み込みパターンスペースの二行にしてから次の処理に進む。また、連続する継続行もおもったとおりに処理される。ラベルを設定してsedb,tコマンドを使うのはいわゆるスパゲッティプログラムになる恐れがあるので避けたいところだが、ここでは先頭に戻るcontinue/next的な処理として許容することにする。(追記: tコマンドの挙動を理解するのはなかなか難しい。直前の置換に成功したか否か?ではないようなので、置換コマンドととともにアドレス指定してグループ化した。)

空行破棄を追加

次の例では、さらに空行処理を追加する。この例では空白文字だけの行も空行として処理している。継続行をまとめてから処理するか、個別に処理するかはどちらの仕様にしたいかによるだろう。(% sed -nE -f sample3.sed input-file)

sample3.sed
:begin
$!N
/(\\\n|\n[[:blank:]]{4})/ {s/[[:blank:]]*(\\\n|\n[[:blank:]]{4})[[:blank:]]*/ /g
t begin
}
/^[[:blank:]]*\n/ D
/\n[[:blank:]]*$/ { s/\n[[:blank:]]*$//g
t begin
}
P
D

パターンスペースにある2行のうち、冗長だが前の行と後の行で分けて書いている。前者の処理ではDコマンドはパターンスペースの処理だけでなくcontinue/next的な動作を含んでいるのが肝である。

コメントアウト処理 (2019/9/21 19:00 ちょっと修正)

よくあるコメントアウト処理は、コメントアウト文字列(下記の例では#または;)から文末までをコメントとして無視、という処理であろう。コメントアウト文字列を含む行の継続行の扱いをどうするかは仕様によって修正が必要となるであろう。(% sed -nE -f sample4.sed input-file)

sample4.sed
:begin
$!N
s/[#;]([^[:space:]]|[[:blank:]])*(\\)(\n)/\2\3/
s/[#;]([^[:space:]]|[[:blank:]])*([^\\]*)(\n)/\3/
$s/[#;]([^[:space:]]|[[:blank:]])*$//
/(\\\n|\n[[:blank:]]{4})/ { s/[[:blank:]]*(\\\n|\n[[:blank:]]{4})[[:blank:]]*/ /g
t begin
}
/^[[:blank:]]*\n/ D
/\n[[:blank:]]*$/ { s/\n[[:blank:]]*$//g
t begin
}
P
D

第3行目と第4行目で継続行の場合とそうで無い場合の処理を分けている。第5行目が無いと、入力ファイルの最終行のコメントの行末に無駄な\があった場合にコメントアウト処理がされない。これらの処理で改行文字を取り除いてしまわない(パターンスペースの行数2を保つ)ことが以降の処理を複雑化させないために重要である。

このあと本文に処理を追加したい場合には最後の2行の前に追記していくことになる。その処理の記述や上記のコメントの処理などでハマってしまう落とし穴は,たとえば3~5行目を

s/[#;].*(\\)(\n)/\1\2/
s/[#;].*([^\\]*)(\n)/\2/
$ s/(\n.*)[#;].*$/\1/g

と書いた場合、.*の部分で改行文字もマッチしてしまうので意図せずに改行文字を消してしまい、パターンスペースが気づかないうちに2行ではなくなってしまい、以降で期待した動作とは違う処理になってしまう。sedでの複数行処理の場合には、パターンスペースにある文字の行数を一定の行数(できれば2行)に保つことが肝で、さもないと処理の記述がどんどん複雑になってしまう。

したがってパターンスペースの一行目のみを処理したいとき、.*の代わりに改行文字以外*というパターンを使う必要がある。ここでまた別の落とし穴が存在する。すくなくともBSD sedでは、sコマンドのパターンマッチでは\nで改行文字にマッチするが[^\n]を「改行文字以外」というパターンマッチには使えない。(単にn以外の文字にマッチする。) いろいろ試行錯誤したみた結果、「改行文字以外」にマッチさせるのに使えそうなのは([^[:space:]]|[[:blank::]])という正規表現である。(文字クラス[:space:]はspace+tab+改行で、[:blank:]はspace+tabなのでその差を利用。) macのマニュアルsed(1)によると\nはアドレス指定には使えないなどとか制約があるという記述がある。

短縮化

上記例では処理をわかりやすくするため複数行になっているが、セミコロンでつないで行数を少なくすることは可能である。ただし BSD sedの場合、:,b,tコマンド末尾の改行は省略できない。(セミコロンでつなごうとすると、アドレス引数と解釈されてしまう)。下記が最小限の改行数にした例。

sample5.sed
:begin
$!N;s/[#;]([^[:space:]]|[[:blank:]])*(\\)(\n)/\2\3/;s/[#;]([^[:space:]]|[[:blank:]])*([^\\]*)(\n)/\3/;$s/[#;]([^[:space:]]|[[:blank:]])*$//;/(\\\n|\n[[:blank:]]{4})/ {s/[[:blank:]]*(\\\n|\n[[:blank:]]{4})[[:blank:]]*/ /g;t begin
} ; /^[[:blank:]]*\n/ D;/\n[[:blank:]]*$/{s/\n[[:blank:]]*$//g;t begin
} ; P;D

前処理としてsedの活用の可能性

もちろん他のpython,perl,rubyといったスクリプト言語でもこのような処理は可能である。そのループ処理と同時に所望の処理と同時に行った方が実行速度としては早いはずである。が、このsedでフィルタ処理してから実際のプログラムに渡す、というのも所望の処理を行うコードの可読性の維持と、手軽さという意味では利用価値があるかな、と思う。

参考資料

  • "sed & awk プログラミング" (Dale Dougherty著 福崎俊博訳 ISBN-10: 4756100910)