sedで改行を含む複数行の文字列を置換

sedは読み込んだ行の行末にある改行を削除してパターンスペースと呼ばれるバッファに格納したうえでテキスト処理をし、最後にパターンスペースの内容に改行をつけて出力する。。
だから改行を含む文字列にマッチさせる指定が非常にややこしい。

sedで改行を含む複数行を指定して一気に置換をやろうと一晩なんやかんややってみたけど、朝になったからあきらめてまったくスマートじゃない別な方法で強引に終わらせた。

:loop
N
$!b loop
s/\n//g

一度ファイル内の改行コードを全削除

そのあとに別スクリプトのsedで複数行じゃなくなったファイル内容を文字列指定で置換して、元通り改行コードを入れる置換する。
改行コードを入れる置換は
\の後に実際の改行を入れるでok

かなりダサい方法だけど一応出力したい結果は得られるから今はいいことにしておく。

sed と改行

(1) 入力にない改行の出力

入力にない改行の出力は、改行文字をバックスラッシュで隠せばよい。

s/xyz/&\
/

の如し。(例は xyz の後に改行を挿入する。& はマッチ全体を示すなり)

(2) 空行の削除

空行の削除は、改行だけ削除しようとせずに、行全体を d コマンドで削除せむと考えれば容易。

/^$/d

の如し。

(3) 改行にマッチさせる

改行にマッチさせるには、いくらか技が必要。sed は入力を一行ずつ読み込むくせに、その正規表現は行末にある改行にマッチしない仕様だからである。

sed は読み込んだ行の行末にある改行を削除してパターンスペースと呼ばれるバッファに格納したうえでテキスト処理をし、最後にパターンスペースの内容に改行をつけて出力する、とのことである

The \n symbol does not match the newline at an end-of-line because when sed reads each line into the pattern space for processing, it strips off the trailing newline, processes the line, and adds a newline back when printing the line to standard output. (http://www.student.northpark.edu/pemente/sed/sedfaq3.html)

N コマンドを使って、次行をパターンスペースの内容に追加すると、パターンスペースの途中にある改行が \n にマッチする。N コマンドについてのいくらかの説明はこちら

試みに abcの後に改行が続く場合、この改行を削除しようとしてみる。

abc
edf

なる入力に対し、

/abc$/N
s/\n//

というコマンドを実行すれば

agcdef

と出力される。

しかし、もし入力が

abc
abc
def

の如くあれば、N コマンド s コマンドはそれぞれ一度しか実行されぬので、出力は

abcabc
def

のようになり、不本意なもである。もし、

 

:a
/abc$/N
/abc$/b a
s/\n//g

のようにすれば、所期の目的を達せられるであろう。これは、N コマンドにより行を連結した後、あらためてパターンスペースの最後に abc があるかどうかを調べ、もしそうであれば b コマンドでラベル a すなわちスクリプトファイルの一行目に戻して再び連結作業を行い、これ以上連結すべき行がなくなった場合に改行を削除する。

こうした例においては、行末に abc という目印あったので、それを目安に後続行をパターンスペースに追加するかどうかの選択することができた。

しかし、「行頭に abc がある場合にそれに先行する改行を削除したい」としたらどうであろうか。ある行を読み込んだ時点では、パターンスペースに次の行追加すればいいかどうかは分からない。

こういう方法がある。

:a
N
s/abc\n/abc/
t a
P
D

これは、とりあえず次行を N コマンドで連結してしまい、s コマンドの置換を利用して abc に続く改行文字の削除を試みる。t コマンドで置換の成否を判別し、置換が失敗した場合、P コマンドでパターンスペースの中から埋め込まれた改行以前をプリントし、D コマンドでその部分をパターンスペースから削除する(D コマンドについてのいくらかの説明はこちら)。そして、また先頭に戻って次行を読み込む。置換が成功した場合、ラベル a を目印に先頭に戻り、処理を続ける。

ようするに、1 行ぶんずつ入れ替えながら常に 2 行ぶんをパターンスペースに保持するようにしているのである。

(補足 1) もし t コマンドがないとどうなるか。s コマンドが成功すると改行が削除されてしまうので、D コマンドがパターンスペース内容を全部削除してしまう。これでは s コマンドに見逃される改行がでてきてしまう。

(補足 2) D コマンドはいささかトリッキーな働きをする。埋め込まれた改行までをパターンスペースから削除したのち、まだパターンスペースに何か残っていたならば、新たな入力行を読み込むことなく次のサイクルを始めるのである。

このやり方は、汎用性が高く、「abc に先行する改行の削除」のみならず「abc に後続する改行を削除」という最初の課題も難無くこなしてくれる。さらに、「abc と def の間の改行を削除する」というような変換も簡単だ。ただし、これは改行を削除するような置換をするのであり、s コマンドでマッチするパターンに改行が含まれていない場合や、置換の結果改行の数が増加するような置換では予期せぬ動きをする。sed が行を単位として働くものである以上、置換により行を増減させる場合には慎重にやらなくてはいけない。

次に、「abc でおわる行と def ではじまる行の間に、空行を挿入する」という課題が考えてみる。これは、置換によってパターンスペース中の行数が増加してしまうとういケースである。

/\n/{P
D}
N
s/abc\ndef/abc\
\
def/
P
D

のようにやるとうまくいく。最初の 2 行は、パターンスペースの中に埋め込まれた改行がある場合に、先頭からいちばん後ろにある改行までを出力し、それを(パターンスペースから)削除するものである。D コマンドが実行されると、制御が先頭に移るので、この 2 行はループとして働く。最終行とその前で P, D コマンドを実行しているのが一見無駄に見えるかもしれないが、これがないと、制御が先頭行に移る前に、パターンスペースの内容が吐き出されてしまう。

さらに汎用性が高くしようとすると、乱暴なことをしなくてはならないだろう。ただやみくもに入力ファイル中のすべての行を連結してパターンスペースに詰め込み、一気に置換してしまうのだ。

:a
$!N
$!b a
s/\nabc/abc/g

はじめの 3 行によって、強引に入力ファイルの内容を一つにつなげてパターンスペースに押し込んでいる。(最終行だけは別あつかいしているが、これは最終行で N コマンドを実行すると、そこでパターンスペースの内容を出力して終了してしまい、以下の置換コマンドが実行されないからだ。)

このはじめの 3 行さえおまじないに書いておけば、多くの場合、「sed の正規表現は行末の改行にマッチしない」という制限が撤廃されたと同じような効果があるだろう(!)。ただし、場合によってはメモリが足りなくなる可能性がある。それが sed の制限なのか、sed が使えるメモリの制限なのかは、どういう sed をどういう環境で使っているかによるが、ともかくメモリが足りなくなるという危険は承知しておくべきである。

また、たとえメモリが十分使えるとしても、ごくつつましい環境で自在に sed を操っててきた強者に敬意を表するためには、もっと苦労してスクリプトを書くべきであろう 🙂

ところで、sed にはパターンスペースのほかに、ホールドスペースというバッファがある。たんに文字列を一時的に退避できるだけのバッファであるが、パターンスペースの内容をこちらに入れたり引き出したりすることによって、さらに難しいケースに対応するスクリプトを書くことができるし、場合によってはそのほうがより自然な書き方なこともある。「段落内改行の削除」では、これを用いている。

2 行にわたるパターンにマッチさせる

改行を無視して探索し、2 行にまたがるパターンに対してもマッチさせたいという場合がある。たとえば、

____abc_____a
bc__abc____ab
c___abc____ab

という入力に対して、abc を xyz に変換して出力したい。

____xyz_____xyz
__xyz____xyz
___xyz____ab

てなふうに。

やっかいなのは、abc の間に改行がはさまっている箇所がある点である。例によって sed は一行ずつの処理が原則だから、素朴にやると 2 行にわたるパターンにはマッチしないのだ。

(1) オシャレなやり方

これは、メモリをほんの少ししか使わない。

# 1 行におさまるパターンの置換
s/abc/xyz/g
N
# 2 行にわたるパターンの置換
s/ab\nc/xyz\ / s/a\nbc/xyz\ /
P D

では、ちょっと説明。

赤字(あるいは太字)にしたコマンドが、処理の流れ・バッファーへの読み込み・出力を制御するためのコマンドたち。緑字(あるいは斜体)の部分は、abc を xyz に置換するための s コマンドである。

流れは以下のごとし。一行におさまっているパターンを置換(さいしょの s コマンド)。次の行をパターンスペースに追加し(N コマンド)、2行にまたがるパターンを置換(2,3 番目の s コマンド)。パターンスペースに埋め込まれた改行以前をプリント(P コマンド)したうえで削除(D コマンド)。また、はじめにもどる。

上記の例で何が起こっているかを詳しく見てみむ。

コマンドパターンスペース内容出力内容
1行におさまるパターンの置換1 行目
N1 行目〈改行〉2 行目
2行にまたがるパターンの置換1 行目〈改行〉2 行目
P1 行目〈改行〉2 行目1 行目〈改行〉
D2 行目
1行におさまるパターンの置換2 行目
N2 行目〈改行〉3 行目
2行にまたがるパターンの置換2 行目〈改行〉3 行目
P2 行目〈改行〉3 行目2 行目〈改行〉
D3 行目
1行におさまるパターンの置換3 行目
N3 行目3 行目〈改行〉
終了

(補足1)sed では、ふつう、スクリプト最終行までいくと制御が先頭行にうつり、次の入力行が自動的に読み込まれる。しかし、D コマンドが使われると、制御が先頭に戻りはするが、パターンスペースが空でない限り自動的に行が読み込まれたりはしない。

(補足2)N コマンドは、読み込むべき次の行がないと、パターンスペースを吐き出して sed を終了させる。

(補足3)s コマンドが N コマンドの前後二箇所に分けて書かれいている理由。もし、最初の s コマンドを、2, 3 番目の s コマンドの直前に置くと、一行におさまっているパターンに対して二回置換コマンドが実行されてしまう。その結果、abc を aabc に置換したいような場合、aaabc になってしまう。

(補足4)これは基本的には、前に示した例と同じ考え方であるが、s コマンドが改行を消去したり増やしたりしないことを前提にしているために、コーディングが簡単になっている。

(2) 強引なやり方

以下のやり方は、入力の分量に比例してメモリ使用量が増え、メモリが足りないとヤバイことになる。

:a $!N $!b a
# 1 行におさまるパターンの置換
s/abc/xyz/g
# 2 行にわたるパターンの置換
s/a\nbc/xyz\ /g s/ab\nc/xyz\ /g

このスクリプトでは、入力をすべて一つにつなげてパターンスペースにぶち込み、それからやおら置換を行っている。

段落内改行の削除

一行20字の原稿を書くときに、20字ごとに改行を入れる人がいる。そして、こうした場合、行頭の全角スペースが改段落の記号となっていることが多い。ワープロの字数設定を知らない人に多いパターンである。

これを sed で「ふつうの原稿」になるよう処理してみよう。つまり、改行は段落の最後のみにつくように、段落内の改行を削除して行をつなげていくのである。じつのところ、これは sed 向きな作業ではない。なぜならば、sed は行指向のエディタである。そして、改行は行の終わりを示す。つまり、行内編集が得意な sed は、二つ以上の行をつなげるという作業が苦手なのだ。

しかし、苦手といえどもできないわけではない。ホールドスペースというものを使うと、なかなかスッキリやってくれる。(スペースのところが見えないが、コピー&ペーストして使えるように、そのままにしておいた。)。

スペースを含まない空行は、まったく削除されてしまうので注意。

#!/bin/sed -nf
# 最終行
${
  H
  x
  s/\n//g
  p
  b
}
# スペースで始まる最終行以外の行
/^[  ]/{
  x
  s/\n//g
  p
  b
}
# スペースで始まらない最終行以外の行
H

また、メールによくあるパターンに、空行を段落の区切りにして、改行は適宜読みやすいように入れるということがある。 LaTeX 用の原稿も、このような形式になっている。原稿がこの手のものであれば、やはり sed で処理できる。

同様のことを ed で行うこともできることをついでに言っておこう。

注意。以下 sed 用スクリプトは、空行じたいは削除する。また、スペース文字だけからなる行も空行として扱う。

#!/bin/sed -nf
# 最終行
${
  H
  x
  s/\n//g
  p
  b
}
# 空行
/^[  ]*$/{
  x
  s/\n//g
  /[^  ]/p
  b
}
# それ以外の行
H

引用元: sed メモ

コメント

タイトルとURLをコピーしました