目的
プログラムソースコードの中の特定の文字列を検索したり置換したりする際に、引用符のペアに囲まれた文字列(文字列リテラル1)やコメントに含まれる文字列を除外したい場合があります。例えば下記の様なケースです。
- たとえば検索対象の変数名がありふれた単語と同じ綴りだったり、ごく短い長さ(2,3文字)だった場合に、通常の検索では文字列リテラルやコメントの中にある文字列が大量にヒットしてしまい、本当に探したい文字列が見つけにくい。
- そのような変数名を他の綴りの変数名に一括変換(置換)したい場合に、単純検索を利用した置換だと文字列リテラルやコメントの中の文字列も一緒に置き換わってしまう。
また、逆に引用符ペアで囲まれた文字列リテラルのみを検索したい場合もあります。
幸い .NET の正規表現エンジンでは任意長の後読みができるので2、クォーテーションの外側(あるいは内側)にある文字列にマッチする正規表現を処理することができます。その複雑さゆえ、覚えておくことも、必要な時にすぐに作ることも困難なため、備忘録としてここにまとめておきます。
正規表現(.NET)
比較的シンプルなものから徐々に複雑さの増すものを順に挙げていくことで、正規表現の内容が理解しやすくなると思います。
通常モード、シングルラインモード、マルチラインモードの何れでも動作するように書いてあります。
引用符ペアの外側の pattern
にマッチする正規表現(引用符のエスケープなし):
単一引用符:(?<=\A(?:[^']|'[^']*')*)pattern
二重引用符:(?<=\A(?:[^"]|"[^"]*")*)pattern
両方 :(?<=\A(?:[^'"]|'[^']*'|"[^"]*")*)pattern
または
(?<=\A(?:[^'"]|\k<q>(?:(?!\k<q>)[\S\s])*(?<q>['"]))*)pattern
または(条件式を利用して)3
(?<=\A(?>(?(q)(?:(?!\k<q>)[\S\s]|(?<-q>\k<q>))|(?:(?<q>['"])|[^'"]+))*))(?(q)(?!))pattern
引用符ペアの外側の pattern
にマッチする正規表現(バックスラッシュ「\
」で引用符がエスケープされる場合):
単一引用符:(?<=\A(?:[^']|'(?:[^'\\]|\\[\S\s])*')*)pattern
二重引用符:(?<=\A(?:[^"]|"(?:[^"\\]|\\[\S\s])*")*)pattern
両方 :(?<=\A(?:[^'"]|'(?:[^'\\]|\\[\S\s])*'|"(?:[^"\\]|\\[\S\s])*")*)pattern
または
(?<=\A(?:[^'"]|\k<q>(?:(?!\k<q>|\\)[\S\s]|\\[\S\s])*(?<q>['"]))*)pattern
VB の文字列リテラルに含まれない pattern
にマッチする正規表現4(重ねること「""
」により引用符がエスケープされる):
(?<=\A(?:[^"]|"(?:[^"]|"")*"(?!"))*)pattern
実は、クォーテーションの内外を判定する目的では引用符を重ねる形のエスケープは考慮する必要がなく、 の2番目の正規表現でも同じ結果が得られます。
C# の文字列リテラル、逐語的文字列リテラル5に含まれない pattern
にマッチする正規表現6(前者はバックスラッシュ「\
」で、後者は重ねること「""
」により引用符がエスケープされる7):
(?<=\A(?:(?!@")[^"]|"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))*)pattern
VB の文字列リテラルおよびコメント(REM...
、'...
)に含まれない pattern
にマッチする正規表現4 8:
(?<=\A(?:(?!\b[Rr][Ee][Mm]\b)[^'"]|(?:\b[Rr][Ee][Mm]\b|')[^\n]*(?:\n|\z)|"[^"]*")*)pattern
pattern
を、例えば \d+
に置き換えるとプログラム中の数値リテラルのみを検索することができます。あるいは、pattern
を文字列リテラルにマッチするパターンに置き換えた下記の正規表現を用いて文字列リテラルのみを検索することもできます。
(?<=\A(?:(?!\b[Rr][Ee][Mm]\b)[^'"]|(?:\b[Rr][Ee][Mm]\b|')[^\n]*(?:\n|\z)|"[^"]*")*)"(?:[^"]|"")*"(?!")
C# の文字列リテラル、逐語的文字列リテラル、およびコメント(//...
、/*...*/
)に含まれない pattern
にマッチする正規表現6:
(?<=\A(?:(?!//|/\*|@")[^"]|//[^\n]*(?:\n|\z)|/\*(?:[^\*]|\*(?!/))*\*/|"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))*)pattern
C# の文字列リテラルと逐語的文字列リテラルにマッチする正規表現:
(?<=\A(?:(?!//|/\*|@")[^"]|//[^\n]*(?:\n|\z)|/\*(?:[^\*]|\*(?!/))*\*/|"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))*)(?:"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))
PowerShell の文字列リテラル9、ヒア文字列10およびコメント(#...
、<#...#>
)に含まれない pattern
にマッチする正規表現11:
(?<=\A(?:(?!<#|@['"])[^#'"]|#[^\n]*(?:\n|\z)|<#(?:[^#]|#(?!>))*#>|'(?:[^'`]|`[\S\s])*'|"(?:[^"`]|`[\S\s])*"|@'(?:(?!\n'@)[\S\s])*\n'@|@"(?:(?!\n"@)[\S\s])*\n"@)*)pattern
または
(?<=\A(?:(?!<#|@['"])[^#'"]|#[^\n]*(?:\n|\z)|<#(?:[^#]|#(?!>))*#>|\k<q1>(?:(?!\k<q1>|`)[\S\s]|`[\S\s])*(?<q1>['"])|@\k<q2>(?:(?!\n\k<q2>@)[\S\s])*\n(?<q2>['"])@)*)pattern
PowerShellの文字列リテラルとヒア文字列にマッチする正規表現:
(?<=\A(?:(?!<#|@['"])[^#'"]|#[^\n]*(?:\n|\z)|<#(?:[^#]|#(?!>))*#>|'(?:[^'`]|`[\S\s])*'|"(?:[^"`]|`[\S\s])*"|@'(?:(?!\n'@)[\S\s])*\n'@|@"(?:(?!\n"@)[\S\s])*\n"@)*)(?:'(?:[^'`]|''|`[\S\s])*'(?!')|"(?:[^"`]|""|`[\S\s])*"(?!")|@'(?:(?!\n'@)[\S\s])*\n'@|@"(?:(?!\n"@)[\S\s])*\n"@)
または
(?<=\A(?:(?!<#|@['"])[^#'"]|#[^\n]*(?:\n|\z)|<#(?:[^#]|#(?!>))*#>|\k<q1>(?:(?!\k<q1>|`)[\S\s]|`[\S\s])*(?<q1>['"])|@\k<q2>(?:(?!\n\k<q2>@)[\S\s])*\n(?<q2>['"])@)*)(?:(['"])(?:(?!\1|`)[\S\s]|\1{2}|`[\S\s])*\1(?!\1)|@(['"])(?:(?!\n\2@)[\S\s])*\n\2@)
#備考
- シングルラインモード(冒頭に
(?s)
を付加)では、上記の各正規表現の中の[\S\s]
を1個のピリオド(.
)に置き換えることができます。逆に、シングルラインモードでない場合は[^\n]
を1個のピリオド(.
)に置き換えることができます。 - マルチラインモード(冒頭に
(?m)
を付加)では、 から の各正規表現の中の(?:\n|\z)
を$
に置き換えることができます。逆にマルチラインモードでない場合は、各正規表現の中の\A
と\Z
をそれぞれ^
と$
に置き換えることができます。 - 上記の正規表現を引用符で囲んでプログラム中で使用する際には、当該プログラムの仕様に従って引用符等の特殊記号をエスケープする必要があります。
- 正規表現が利用可能な文字列検索ツールや文字列置換ツールを過去に投稿してきました。これらのツールや、.NET の正規表現に対応した他のツール(TresGrep、Regex Hero、Expresso、etc.)、EditPad Lite などで試してみてください。
.NET 以外の場合
.NET 以外の正規表現エンジンはごく一部を除いて任意長の後読みをサポートしていません。そのようなエンジンでクォーテーションの外側の文字列にマッチする正規表現を実現するために「検索対象文字列と正規表現の両方を逆順に並べ替えて、後読みの代わりに先読みを利用する」方法があります12。置換を行なう場合は置換文字列も逆順に並べ替え、置換後に結果の文字列を逆順に並べ替えて元に戻します。 の2番目、 の2番目、、 の正規表現は逆向きにするとそれぞれ、
nrettap(?=(?:[^"]|"[^"]*")*$)
nrettap(?=(?:[^"]|"(?:[^"\\]|[\S\s]\\)*")*$)
nrettap(?=(?:[^"]|(?<!")"(?:[^"]|"")*")*$)
nrettap(?=(?:[^"](?<!"@)|"(?:[^"\\]|[\S\s]\\)*"|(?<!")"(?:[^"]|"")*"@)*$)
のようになります。
-
「リテラル(literal)」とは「文字どおりの」を意味する形容詞で、コンピュータ言語では「固定値」や「定数」を表す名詞として使われます(変数の対義語)。 ↩
-
他の多くの正規表現エンジンの実装では固定長あるいは有限長の後読みしかできません。 ↩
-
ややこしいので以降のケースでは省略 ↩
-
@"
と"
で囲まれた文字列(エスケープシーケンスが無効)。 ↩ -
逐語的文字列リテラル中での連続した二重引用符によるエスケープの処理は省略できません。 ↩
-
クォーテーションの内外を判定する目的では引用符を重ねる形のエスケープは考慮する必要がないので処理を省略しています。 ↩
-
PowerShellでは単一引用符、二重引用符の両方が使えます。引用符のエスケープはバッククォート(
`
)と引用符を重ねる形の両方が可能ですが、クォーテーションの内外を判定する目的では引用符を重ねる形のエスケープは考慮する必要がないので処理を省略しています。 ↩ -
@"
(または@'
) と 行頭の"@
(または'@
)で囲まれた文字列(エスケープシーケンスが無効)。 ↩ -
二重引用符を用いた文字列リテラルやヒア文字列の中の変数展開(
$(...)
) は考慮していません。 ↩ -
ついでに、後読みを全くサポートしていない正規表現エンジンで固定長の後読みと同様の効果を得る(別の)代替方法を紹介します。たとえば、
(?<=look)behind
の検索はlook(behind)
の検索結果の 1番目のキャプチャグループで代替できますし、(?<=look)behind
にマッチした部分をahead
で置換する代わりに(look)behind
にマッチした部分を$1ahead
で置換することができます(1番目のキャプチャの内容が$1
で後方参照できる場合)。否定の後読みについても同様に、(?<!look)behind
の検索を(?:^[\S\s]{0,3}|(?!look)[\S\s]{4})(behind)
の検索に置き換えて 1番目のキャプチャグループを使ったり、(?<!look)behind
をahead
で置換する代わりに(^[\S\s]{0,3}|(?!look)[\S\s]{4})behind
を$1ahead
で置換したりすることができます。{4}
の 4 は後読みするパターンの文字数で、{0,3}
の 3 はその文字数から 1 を引いた数です。後読みするパターンの文字数が 1 の場合、たとえば(?<!a)wake
をwoke
に置換する場合は(^|[^a])wake
を$1woke
で置換します。別の投稿に掲示している VBA のソースコードでこれらの方法を実際に利用しています。
クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ↩