コメント行を無視して文字列置換を行う

8496 ワード

はじめに

突然ですが以下のようなシミュレーションに使うパラメータファイルがあったとします[1].

calcmod.def
#CalcType = 0:Lanczos, 1:TPQCalc, 2:FullDiag, 3:CG, 4:Time-evolution
CalcType   0

これはコメント行にもあるように, CalcTypeの値に0から4までの整数値を指定することでシミュレーションに用いる手法を切り替えるためのものです.

今, 値はLanczosの0に指定されていますが, CGの3に置換したいという場合にどのようなコマンドを叩けばよいのかという話をします.

答え

まず最初にコマンドを示して後から説明を書きます.
なぜ4種類のバリエーションがあるかは後で補足します.
なおオプション-iは直接ファイルを書き換えるためのオプションです.

GNU版

sed -i -e '/^#/!{/CalcType/s/0/3/}' calcmod.def
#もしくは
sed -i -e "/^#/! {/CalcType/s/0/3/}" calcmod.def

BSD版

sed -i -e '/^#/!{;/CalcType/s/0/3/;}' calcmod.def
#もしくは
sed -i -e "/^#/! {;/CalcType/s/0/3/;}" calcmod.def

最も汎用的であろう最後の例で書式を示します.

sed -i -e "/^コメント記号/! {;[アドレス]コマンド;}" ファイル名

アドレスとコマンドが連続するため便宜上アドレスを[アドレス]と書いていますが, 角括弧[]はsedでは意味のある記号ですので余計な[]を書かないよう注意してください.

説明

順を追ってコマンドの意味を説明していきます.
微妙にややこしい点についても補足します.

単純な置換→コメント行に引っかかってしまう

まずsedの基本的な置換コマンドを復習します.

# sed -e アドレス+コマンド ファイル名
sed -e "/CalcType/s/0/3/" calcmod.def

このコマンドは/CalcType/までがアドレスでCalcTypeを含む行を指定しています.
その後のs/0/3/がアドレスで指定された行の0を3で置換せよというコマンドになります.

結果は

calcmod.def
#CalcType = 3:Lanczos, 1:TPQCalc, 2:FullDiag, 3:CG, 4:Time-evolution
CalcType   3

となります.
コメントの方まで0が3に置換されています.
コメントは結果に影響しないと考えるとこのままでもシミュレーションに支障はありませんが, 他の人が当該ファイルを見たときにパラメータ値の意味を勘違いさせる可能性があり, できれば避けたいところです.

コメント行は置換しない

サーカムフレックス^を文字列の前につけると行頭に指定文字列がある行を指定することができます.
また, エクスクラメーションマーク!をアドレスの後ろにつけると指定文字列を含まない行を指定できます.
すなわち/^#/!をアドレスとして指定すれば, 行頭に#がない行を指定することができます.

アドレスの組み合わせ

次にコメント行ではない条件/^#/!CalcTypeがある条件/CalcType/を組み合わせて指定する方法について述べます.
これは波括弧{}を用いることで実現できます.
アドレスの後ろを{}で括ると{}の中のコマンドが最初に指定したアドレス中でのみ実行されます.
今回用いるための書式は

sed -e "アドレス1{アドレス2+コマンド}" ファイル名

の形になります.
こうすることでアドレス1で指定される行に対してアドレス2を満たす行を指定してコマンドを実行することができます.
したがって, コメント行ではない行のうちCalcTypeのある行で0を3に置換するコマンドは

sed -e "/^#/!{/CalcType/s/0/3/}" calcmod.def

となります.

しかしこのコマンドは動きません.

その原因は

  • エクスクラメーションマーク!がシェルのコマンドであること
  • コマンドのグループ化に用いる波括弧{}の仕様(BSD版のみ)

の2種類あります.

エクスクラメーションマーク!がシェルのコマンドであること

上述のコマンドを実行すると恐らく

terminal
-bash: !{/CalcType/s/0/3/}": event not found

というエラーが出ると思います.
これは!はシェルでは過去に使ったコマンドを実行するためのもので, このエラーは該当する過去のコマンドがないために出ています.

!でエスケープ→うまくいかない

!をシェルのコマンドとして解釈されないようエスケープしなければならないのですが, 単純にバックスラッシュ\をつけて\!とすると, 文字として解釈されはするのですが\も出力されてしまい動きません.

単一引用符の利用

一番手っ取り早い方法はアドレスとコマンドを二重引用符""の代わりに単一引用符''で括ることです.

#GNU版1
sed -e '/^#/!{/CalcType/s/0/3/}' calcmod.def

bashでは単一引用符''で括ると変数などが展開されませんので先ほどの問題は起こりません.
ただ, 上記のコマンドは置換に変数を使おうとすると使えませんので二重引用符でエスケープする方法を考えます.
なお, 逆に言えば変数などを使わないのであれば上記のコマンドで十分です.

二重引用符中のエクスクラメーションマーク!のエスケープ

二重引用符のままエスケープするやり方は2通りあります.
まず他のwebsiteに載っていた方法として, 二重引用符""と単一引用符''の連結を利用してエクスクラメーションマーク!のみ単一引用符''で括る方法を紹介します.

sed -e "/^#/"'!'"{/CalcType/s/0/3/}" calcmod.def

もう一つはエクスクラメーションマーク!の後ろにスペースが空いてればコマンドとして認識されないということを利用したものです.
つまりエクスクラメーションマーク!の後ろにスペースを入れるだけでよいです.

#GNU版2
sed -e "/^#/! {/CalcType/s/0/3/}" calcmod.def

Linuxなどに入っているGNU版sedを使っている場合は以上になります.
おつかれさまでした.

macOSなどに入っているBSD版sedを使っている場合はまだ動きません.
コマンドのグループ化に用いた波括弧{}の仕様によるもので, 上記のコマンドはBSD版sedでの制約を満たしていません.
以下ではその制約について説明し, 修正を行います.

コマンドのグループ化に用いる波括弧{}の仕様(BSD版のみ)

波括弧{}の使い方をもう一度書くと, アドレス{コマンド}とすると最初に書いたアドレスに括弧内のコマンドの適用範囲を絞ることができる, と言うものでした.

実は, 正確な書式は

アドレス{
	コマンド
}

です.
つまり,

  • {の直後は改行しなければならない
  • }の直前は改行しなければならない

という制約があります.
GNU版ではこの制約は緩和されたようで改行しなくても動きました.

それでは, この制約を満たすようにセミコロン;で改行を入れてあげましょう.

#BSD版1
sed -e '/^#/!{;/CalcType/s/0/3/;}' calcmod.def
#BSD版2
sed -e "/^#/! {;/CalcType/s/0/3/;}" calcmod.def

終わりに

慣れている人には大したことない話かもしれませんが, 色んな落とし穴があり長くなってしまいました.

参考文献

sed & awkプログラミング 改訂版, オライリージャパン

脚注
  1. これは\mathcal{H}\Phiのparameter fileです. ↩︎