クォーテーションの外側の文字列にマッチする正規表現

# 目的
プログラムソースコードの中の特定の文字列を検索したり置換したりする際に、引用符のペアに囲まれた文字列（文字列リテラル[^1]）やコメントに含まれる文字列を除外したい場合があります。例えば下記の様なケースです。

[^1]: 「リテラル（literal）」とは「文字どおりの」を意味する形容詞で、コンピュータ言語では「固定値」や「定数」を表す名詞として使われます（変数の対義語）。

- たとえば検索対象の変数名がありふれた単語と同じ綴りだったり、ごく短い長さ（2,3文字）だった場合に、通常の検索では文字列リテラルやコメントの中にある文字列が大量にヒットしてしまい、本当に探したい文字列が見つけにくい。
- そのような変数名を他の綴りの変数名に一括変換（置換）したい場合に、単純検索を利用した置換だと文字列リテラルやコメントの中の文字列も一緒に置き換わってしまう。

また、逆に引用符ペアで囲まれた文字列リテラルのみを検索したい場合もあります。

幸い .NET の正規表現エンジンでは任意長の後読みができるので[^2]、クォーテーションの外側（あるいは内側）にある文字列にマッチする正規表現を処理することができます。その複雑さゆえ、覚えておくことも、必要な時にすぐに作ることも困難なため、備忘録としてここにまとめておきます。

[^2]: 他の多くの正規表現エンジンの実装では固定長あるいは有限長の後読みしかできません。

# 正規表現（.NET）
比較的シンプルなものから徐々に複雑さの増すものを順に挙げていくことで、正規表現の内容が理解しやすくなると思います。
　通常モード、シングルラインモード、マルチラインモードの何れでも動作するように書いてあります。

:one: 引用符ペアの外側の `pattern` にマッチする正規表現（引用符のエスケープなし）：

単一引用符：`(?<=\A(?:[^']|'[^']*')*)pattern`
二重引用符：`(?<=\A(?:[^"]|"[^"]*")*)pattern`
両方　　　：`(?<=\A(?:[^'"]|'[^']*'|"[^"]*")*)pattern`
　　　　　　または
　　　　　　`(?<=\A(?:[^'"]|\k<q>(?:(?!\k<q>)[\S\s])*(?<q>['"]))*)pattern`
　　　　　　または（条件式を利用して）[^101]
           `(?<=\A(?>(?(q)(?:(?!\k<q>)[\S\s]|(?<-q>\k<q>))|(?:(?<q>['"])|[^'"]+))*))(?(q)(?!))pattern`

[^101]: ややこしいので以降のケースでは省略

:two: 引用符ペアの外側の `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`

:three: VB の文字列リテラルに含まれない `pattern` にマッチする正規表現[^3]（重ねること「`""`」により引用符がエスケープされる）：

[^3]: VB Ver.14で導入された文字列補間は考慮していません。

`(?<=\A(?:[^"]|"(?:[^"]|"")*"(?!"))*)pattern`

実は、クォーテーションの内外を判定する目的では引用符を重ねる形のエスケープは考慮する必要がなく、:one: の2番目の正規表現でも同じ結果が得られます。

:four: C# の文字列リテラル、逐語的文字列リテラル[^4]に含まれない `pattern` にマッチする正規表現[^5]（前者はバックスラッシュ「`\`」で、後者は重ねること「`""`」により引用符がエスケープされる[^6]）：

[^4]: `@"` と `"` で囲まれた文字列（エスケープシーケンスが無効）。
[^5]: C# Ver.6で導入された文字列補間は考慮していません。
[^6]: 逐語的文字列リテラル中での連続した二重引用符によるエスケープの処理は省略できません。

`(?<=\A(?:(?!@")[^"]|"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))*)pattern`

:five: VB の文字列リテラルおよびコメント（`REM...`、`'...`）に含まれない `pattern` にマッチする正規表現[^3] [^7]：

[^7]: クォーテーションの内外を判定する目的では引用符を重ねる形のエスケープは考慮する必要がないので処理を省略しています。

`(?<=\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)|"[^"]*")*)"(?:[^"]|"")*"(?!")`

:six: C# の文字列リテラル、逐語的文字列リテラル、およびコメント（`//...`、`/*...*/`）に含まれない `pattern` にマッチする正規表現[^5]：

`(?<=\A(?:(?!//|/\*|@")[^"]|//[^\n]*(?:\n|\z)|/\*(?:[^\*]|\*(?!/))*\*/|"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))*)pattern`

C# の文字列リテラルと逐語的文字列リテラルにマッチする正規表現：

`(?<=\A(?:(?!//|/\*|@")[^"]|//[^\n]*(?:\n|\z)|/\*(?:[^\*]|\*(?!/))*\*/|"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))*)(?:"(?:[^"\\]|\\[\S\s])*"|@"(?:[^"]|"")*"(?!"))`

:seven: PowerShell の文字列リテラル[^8]、ヒア文字列[^9]およびコメント（`#...`、`<#...#>`）に含まれない `pattern` にマッチする正規表現[^10]：

[^8]: PowerShellでは単一引用符、二重引用符の両方が使えます。引用符のエスケープはバッククォート(`` ` ``)と引用符を重ねる形の両方が可能ですが、クォーテーションの内外を判定する目的では引用符を重ねる形のエスケープは考慮する必要がないので処理を省略しています。

[^9]: `@"`（または`@'`） と 行頭の `"@` （または`'@`）で囲まれた文字列（エスケープシーケンスが無効）。

[^10]: 二重引用符を用いた文字列リテラルやヒア文字列の中の変数展開（`$(...)`） は考慮していません。

``(?<=\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)` を付加）では、:five: から :seven: の各正規表現の中の `(?:\n|\z)` を `$` に置き換えることができます。逆にマルチラインモードでない場合は、各正規表現の中の `\A` と `\Z` をそれぞれ `^` と `$` に置き換えることができます。
- 上記の正規表現を引用符で囲んでプログラム中で使用する際には、当該プログラムの仕様に従って引用符等の特殊記号をエスケープする必要があります。
- 正規表現が利用可能な[文字列検索ツール](https://qiita.com/earthdiver1/items/5eecd7016bb2ee24c507)や[文字列置換ツール](https://qiita.com/earthdiver1/items/688cd29784cf10d3cc91)を過去に投稿してきました。これらのツールや、.NET の正規表現に対応した他のツール（[TresGrep](http://hp.vector.co.jp/authors/VA055804/TresGrep/)、[Regex Hero](http://regexhero.net/tester/)、[Expresso](http://www.ultrapico.com/expresso.htm)、etc.）、[EditPad Lite](https://www.editpadlite.com/) などで試してみてください。

# .NET 以外の場合
.NET 以外の正規表現エンジンはごく一部を除いて任意長の後読みをサポートしていません。そのようなエンジンでクォーテーションの外側の文字列にマッチする正規表現を実現するために「検索対象文字列と正規表現の両方を逆順に並べ替えて、後読みの代わりに先読みを利用する」方法があります[^11]。置換を行なう場合は置換文字列も逆順に並べ替え、置換後に結果の文字列を逆順に並べ替えて元に戻します。:one: の2番目、:two: の2番目、:three:、:four: の正規表現は逆向きにするとそれぞれ、
`nrettap(?=(?:[^"]|"[^"]*")*$)`
`nrettap(?=(?:[^"]|"(?:[^"\\]|[\S\s]\\)*")*$)`
`nrettap(?=(?:[^"]|(?<!")"(?:[^"]|"")*")*$)`
`nrettap(?=(?:[^"](?<!"@)|"(?:[^"\\]|[\S\s]\\)*"|(?<!")"(?:[^"]|"")*"@)*$)`
のようになります。

[^11]: ついでに、後読みを全くサポートしていない正規表現エンジンで固定長の後読みと同様の効果を得る（別の）代替方法を紹介します。たとえば、`(?<=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` で置換します。別の[投稿](https://qiita.com/earthdiver1/items/4b3674ed496c500d6894)に掲示している VBA のソースコードでこれらの方法を実際に利用しています。
　
　
クリエイティブ・コモンズ 表示 - 継承 4.0 国際