sedで正規表現と置換


Macのターミナルでsedを使って置換する事はよくありますが、正規表現でパターンマッチした値を置換後の値に引き継いで使う方法をやったことがなかったのでメモ。

sedで正規表現

sedの正規表現についてはman sedの中で下記のような記述があります。

The regular expressions used in sed, by default, are basic regular expressions (BREs, see re_format(7) for more information), but extended (modern) regular expressions can be used instead if the -E flag is given.

デフォルトではBREが使われるが-Eオプションを使うことでEREを使うことができるようです。

EREの方が拡張というだけあって機能が豊富であり、記法も異なります。

Gnuのsedのマニュアルなので厳密には違いますが(MacはBSD)、下記のような説明があります。
https://www.gnu.org/software/sed/manual/html_node/BRE-vs-ERE.html

ざっくり言うと

BREでは\を付けない限り、文字列に特殊な意味を持たない
EREでは\を付けると特殊な意味を失う

と書いてます。

BREは特殊文字として扱いたい文字列に\を付ける必要がありますが、EREは文字列として扱いたいものに対して\を付ける必要があります。
BREとEREで\の意味が変わるので注意が必要です。

置換すること

さて、今回説明したかったことは正規表現でマッチした結果を使って置換をする方法です。

具体的なケースで考えます。
PostgreSQLで用いるクエリが書かれたsqlファイルがあるとします。
その中のWHERE句に下記のような記述がありました。

INTERVAL '-10days'
INTERVAL '-2weeks'
INTERVAL '+1month'

これを下記のように間にスペースを入れて書き換えたいとします。

INTERVAL '-10 days'
INTERVAL '-2 weeks'
INTERVAL '+1 month'

この時、変換したい文字列のパターンが複数あり、かつ数字の部分も可変です。
そのため正規表現を使った対象を見つけて、その値を引き継いで置換をする必要があるため、単純に文字列置換をすることは難しいです。

そこで正規表現を使ってパターンマッチさせて、うまく変換させることにしました。

方法

まず対象を見つける条件として下記のように考えます。

  1. 数字が1文字以上存在する
  2. その数字の直後に単位(day, week, month等)の文字列がくる
  3. 最後にシングルクオートがある

パターンだけ抜き出すと下記のようになります。

[0-9](day|days|week|weeks|hour|hours|month|months|year|years|DAY|DAYS|WEEK|WEEKS|HOUR|HOURS|MONTH|MONTHS|YEAR|YEARS)\'

これを置換するわけですが、結論を言うと下記のように記述することでうまくできます。

sed -Ei -e "s/([0-9])((day|days|week|weeks|hour|hours|month|months|year|years|DAY|DAYS|WEEK|WEEKS|HOUR|HOURS|MONTH|MONTHS|YEAR|YEARS))\'/\1\ \2\'/g" sample.sql

記法としては

sed -Ei -e "s/(数字の部分のパターンマッチ条件)(単位の部分のパターンマッチ条件)\'/1つめの()内の値\ 2つ目の()の値\'/g" 対象ファイル.sql

となっています。
前半の正規表現を()でくくることで、後半の置換内容に実際マッチした値を引き継ぐことができます。
正規表現で対象を探しつつ、その時の値を使って置換ができるので、grepで絞ってから単語単位で置換というステップを踏まなくても良いです。or条件で幅広く指定できるのでサクッと置換したい時には割と役に立つ方法かなと思いました。