8
Help us understand the problem. What are the problem?

posted at

updated at

正しく理解できる!シェルスクリプトとPOSIXの正規表現(令和最新版)〜 基本正規表現BREと拡張正規表現EREについて

はじめに

この記事はシェルスクリプトと重要な関係がある POSIX の正規表現(基本正規表現 BRE と拡張正規表現 ERE)とその最新情報を正しく理解したい人のための記事です。正規表現とはなにか?みたいな基本的な話はしません。他のプログラミング言語で使ってるから正規表現自体は知ってるつもりだけど、シェルスクリプトだといつもの正規表現が動かずなんか苦手だという人のために、シェルスクリプトにおける正規表現を深く理解できるような内容にしています。基本的に POSIX に準拠した内容を中心に解説しており、どの環境でも当てはまる話になっていますが GNU や BSD や macOS の環境固有の拡張された正規表現、歴史的な UNIX の話や各コマンド毎の細かい違いなど、実際に使う上で必要な知識も解説しています。(この記事のおまけの正規表現の歴史的は「正規表現が UNIX コマンドに導入され POSIX で標準化するまでの歴史を紐解いてみた」に移動しました)

ちなみに「正しく理解できる!」とタイトルに入れたのは、他の解説記事が、知りたい情報が書かれておらず中途半端だったり、間違ってたり不正確な解説がされており、私が勉強しはじめの頃に混乱しまくったからです。この記事はそんな事にならないように信頼できる情報と実際の環境で検証を行って書いています。他の技術書や解説記事を読んだけどスッキリしない、説明の内容おかしくない?だとかモヤッとしている方には特にこの記事をおすすめします。「令和最新版」というのは現時点での最新情報をちゃんと反映させているという意味です。なお平成版や昭和版はありません。

分かり難いシェルスクリプトの正規表現

一般的にシェルスクリプトはいくつかのコマンドを使って何かを実現します。しかしそれらのコマンドは別々の開発者によって開発されたもので正規表現にも細かい違いがあります。同じコマンドでも GNU 版、BSD 版、macOS 版で動作が違うこともあります。またシェルスクリプトでよく使うコマンドの正規表現はおよそ 30 年前に作られたものであり、多くのプログラミング言語の高度な正規表現と比べると基本的な機能しか備えておらず一般的な機能が使えなくて戸惑うという問題もあります。こういったシェルスクリプト特有の事情があるため単に正規表現を覚えるだけでは十分ではありません。そこでこの記事では前半でシェルスクリプトにおける正規表現の基本を解説し後半で各コマンド毎の注意点を解説しています。

なお、念の為ですが、なんでもシェルスクリプトでやろうとはしないでください。シェルスクリプトで作る理由がなく文字列処理がきついと思ったら他のプログラミング言語(と最新の正規表現)を使用すべき時です。ほとんどの言語は POSIX に準拠して開発されており POSIX 環境でならどこでも動きます。移植性や互換性問題も少なく生産性も高いため、どこでも動くソフトウェアを作るのであれば他の言語の方がはるかに適しています。この記事はシェルスクリプトと正規表現を使わないといけない時にハマらないように知識をつけるのが目的であり、なんでもシェルスクリプトでやるための記事ではありません。

シェルスクリプトの正規表現

BRE(基本正規表現)とERE(拡張正規表現)

シェルスクリプト(正確にはコマンド)では一般的に POSIX が定義した BRE(基本正規表現) と ERE(拡張正規表現)のどちらか、または両方が採用されています。コマンド毎に採用している正規表現が違うのは歴史的な理由です。コマンドが先に作られ後から POSIX が登場し BRE と ERE を策定し標準化しました。名前から基本の BRE があってそれを拡張したのが ERE だと思われがちですがそうではありません。私の理解は次のとおりです。

  • BRE(基本正規表現) ・・・ 一貫性がなく分かりづらい
    • 「最初の正規表現」の基本構造を簡略化して後付で機能追加したものだから
  • ERE(拡張正規表現) ・・・ 一貫性があり分かりやすい
    • 「最初の正規表現」の基本構造を素直に拡張したものだから

実は BRE と ERE の両方の元となった「最初の正規表現」があります。「最初の正規表現」はまず簡略化され、それが UNIX コマンドに導入され普及しました。簡略化しただけなら良かったのですが、普及するにつれて機能が足りないことがわかり、互換性を保ちながら後付で拡張したため一貫性がなく分かりづらい文法になってしまいました。それが BRE として POSIX で標準化されます。その一方で「最初の正規表現」を拡張したものが登場します。こちらは純粋に文法を拡張したため一貫性がある分かりやすい文法になっています。これが ERE として POSIX で標準化さます。

このように BRE と ERE は異なる経緯で登場、発展してきたものであり、文法の基本構造に違いがあるため完全な互換性がありません。これら正規表現の歴史についての話は「正規表現が UNIX コマンドに導入され POSIX で標準化するまでの歴史を紐解いてみた」で詳しくまとめています。

補足 POSIX では SRE(単純正規表現)というのも策定していたようですが廃止されたものなので省略します。ただ SRE と BRE は私にはほとんど同じに見え大きな違いが分かりません。

覚えるのは ERE だけで良くなった

いきなり 2 種類の正規表現が登場して面倒に思うかもしれませんが、基本的に ERE(拡張正規表現)だけを覚えれば十分です。なぜかというとよく使われているコマンドは POSIX 準拠で ERE をサポートしているからです。ただしコマンド毎に細かい違いがあり実際には ERE ± α となっているので注意は必要です(プラスだけではなくマイナスもあります)。

正規表現はほぼ全てのプログラミング言語で直接またはライブラリとして間接的に採用されておりプログラマであれば必修といえる知識ですが、ERE はそれらの言語の正規表現のサブセット(エスケープシーケンスや(?...) 構文等を取り除いたもの)になっています。ERE を知ることで、すでにいずれかの言語の正規表現を知ってる人は別の言語でも共通して使える正規表現の範囲を知ることができますし、知らない人は ERE から高度な正規表現へと混乱することなく知識を拡張していくことができます。名前に反して正規表現の基本の文法は ERE の方なのです。

BRE ERE BRE ERE BRE ERE
[[ ]] yes ex posix lex posix
awk posix expr posix more posix
ed posix grep posix posix (-E) sed posix posix (-E)
egrep yes less yes vi (vim) posix (yes)

上記の一覧は正規表現を使う POSIX コマンド(太字)と、それ以外のよく使われるコマンドを一覧にしたものです。この中でシェルスクリプトで使うものは、おそらく [[ ]]awkgrepsed ぐらいでしょう(なお egrep は POSIX 準拠ではなくなりとっくにレガシーです)。人によっては lessvim を使っていると思いますが、これらはすべて ERE をサポートしています。

注意 sed -E (ERE 対応) は POSIX 準拠じゃない!と言いたい方へ↓
sedで拡張正規表現 (ERE) を使う時は -r ではなく -E オプション(POSIX Issue 8 準拠)を使いましょう

BRE を使わなければいけないのは次のような場合だけです。

  • expr で正規表現を使いたい(純粋な POSIX シェルでは [[ ]] が使えない)
  • sed-E オプションを使うのはまだ時期尚早だと思っている
  • lessvim ではなく POSIX 準拠の more や最小機能の vi で頑張っている

このような場合に当てはまる人は BRE も覚える必要がありますが、ほとんどの人は ERE だけで十分だと思います。とは言え BRE と ERE の違いはそんなに大きくはありません。違いを把握すれば簡単に覚えることが出来るでしょう。

BRE と ERE の文法の違い

BRE と ERE は正確には POSIX が定義しているものを指しますが GNU と macOS はそれらを拡張しているため異なる部分があります。なお比較表のタイトルの A - H の意味は次項の「特殊文字」を参照してください。

POSIX 版 BRE & ERE
A B C D E F G H 後方参照
BRE ^ $ . [ ] * \{ \} \( \) \ \1 ... \9
ERE ^ $ . [ ] * + ? { } ( ) | \

BRE では使えない + ?| が ERE では使えるようになっています。実の所 + ?{ } を使って代替可能なので BRE で使えないのは | のみです。逆に後方参照が使えるのは BRE だけです。(注意 後方参照とは sed などの置換文字列で使う参照のことではありません。置換文字列での参照は ERE でも対応しています。)

ERE ではここにあげた特殊文字を無効にする時にエスケープ文字(バックスラッシュ)でエスケープします。BRE でも大部分は同じなのですが { }( ) に限っては特殊な意味をもたせるためにエスケープ文字を使うため一貫性がありません。

GNU 版 BRE & ERE
A B C D E F G H 後方参照
BRE ^ $ . [ ] * \+ \? \{ \} \( \) \| \ \1 ... \9
ERE ^ $ . [ ] * + ? { } ( ) | \ \1 ... \9

GNU 版では BRE、ERE の両方で欠けている機能を補っているため、BRE と ERE に機能的な違いはありません。違う所は { }( )+ ? | のエスケープの意味が反対になっている所だけです。BRE はエスケープ文字の使い方に一貫性がないということがはっきりわかります。例えば * + ? は繰り返しの数が違うだけの似たような機能なのに * はエスケープ文字が必要なく + ? はエスケープ文字が必要になっています。こんな状態なので BRE を(最初に)覚えようとすると混乱してしまうことでしょう。もし必要なら BRE は ERE を覚えた後に差分として覚えることをお勧めします。

補足ですが GNU 版の BRE & ERE は「C 言語スタイルのエスケープシーケンス」を使用することが出来ます。

macOS 版 BRE & ERE

macOS では基本的に BSD 版のコマンドが使われているのですが、拡張された正規表現である enhanced BRE と enhanced ERE が使える場合があります。

A B C D E F G H 後方参照
BRE ^ $ . [ ] * \+ \? \{ \} \( \) \| \ \1 ... \9
ERE ^ $ . [ ] * + ? { } ( ) | \ \1 ... \9

比較表は特殊文字だけを書いているため GNU 版 と同じですが、文法は更に拡張されており、最短一致 .*?、キャプチャなしの部分式 (?:...)、インラインオプション (?i)、インラインリテラルモード \Q...\E、インラインコメント (?#...) 等の高度な正規表現に対応しています。詳細は man re_format の ENHANCED FEATURES を参照してください。ウェブでは re_format(7) [osx man page] で見ることが出来ます。

ただし全てのコマンドで ENHANCED FEATURES が有効になっているわけではないようで、macOS Big Sur 11.6 では grep では使えましたが sed では使えませんでした。

$ echo "foobarbaz" | grep 'foo\(bar\|baz\)\?'
foobarbaz

$ echo "foobarbaz" | sed -n '/foo\(bar\|baz\)\?/p'

補足ですが macOS 版の enhanced BRE & enhanced ERE は「C 言語スタイルのエスケープシーケンス」を使用することが出来ます。

BRE は「 \{ \} \( \) を使う」と覚える

(POSIX 版の) BRE を使う時に気にすることは \{ \} \( \) を使うということだけです。* は使えますが + ? は使えないので代わりに \{ \} を使うしかありません。マッチした部分のキャプチャには \( \) を使います。ただし | は使えません。

GNU 版 や macOS 版の拡張された BRE を使うぐらいなら ERE を使えばいいと思いますが、もし拡張された BRE を使う場合は \{ \} \( \) に加えて POSIX 版では使えなかった+ ? | が頭に \ をつければ使えるようになるというだけです。

ね?簡単でしょ?

特殊文字(メタ文字)^$ .[] *+? {} () | \

特殊文字 (Special Characters) は特殊な意味を持つ文字のことです。メタ文字とも言われますが POSIX ではブラケット表現の中でのみメタ文字という用語を使っているようなので、この記事では特殊文字と呼ぶことにします。なお"文字列"ではなく"文字"といった場合 1 文字を意味します。BRE の場合 + ? { } ( ) | はエスケープ文字(バックスラッシュ /)で始まるエスケープシーケンスとして実装されており特殊"文字"に含めないのが正しいのですが説明の都合上(BRE と ERE を分けて書きたくないので)この記事では特殊文字の記述方法の違いとして扱います。

特殊文字 意味
A ^ $ アンカー (anchor) 文字列の行頭(^) または 行末($) にマッチします。
B . ピリオド 任意の一文字にマッチします。
C [ ] ブラケット表現 (bracket expression) カッコ内のいずれかの文字にマッチします。詳細は「ブラケット表現」を参照。
D *+? 量指定子 (quantifier) 前の文字またはグループの「0 個以上の繰り返し *」「1 個以上の繰り返し +」「0 個または 1 個 ?」にマッチします。下記の量指定子の省略型とみなすことができます。なお BRE、ERE には最長一致しかありません(macOS 版の enhanced ERE を除く)。
E { } 量指定子のインターバル表現 (interval expression) {m,n} 前の文字またはグループの m 個以上 n 個以下の繰り返しにマッチします。「m 個 {m}」「m 個以上 {m,}」「n 個以下 {,n}(GNU 版のみ)」という指定の仕方もできます。
F ( ) BRE: 部分式 (sub expression) 後方参照で参照する部分式 / ERE: グループ (grouping) 選択子のためのグループ。POSIX の定義では BRE と ERE で機能が異なり名前も違います。POSIX 以外の定義では 2 つの機能をあわせ持っています。なお肯定先読み等の (?...) のようなものはありません。置換文字列で参照する範囲の指定にも使います。
G | 選択子 (alternation) (exp1|exp2) という指定で exp1 または exp2 という意味になります。トップレベルの () は省略可能です。POSIX 準拠では BRE では使えません。
H \ 特殊文字の特殊な意味を無効にするためのエスケープ文字です。特殊文字以外の前にバックスラッシュをおいた場合の挙動は(後方参照を除いて)POSIX では未定義です。GNU などの拡張ではエスケープシーケンスとして特別な意味を持ちます。

後方参照 (back reference) \1 ... \9 で部分式にマッチした文字列にマッチさせることができます。後方参照(置換文字列での参照のことではありません)は POSIX 準拠では BRE でのみ使えます。

部分式・グループ ( ) と後方参照 \1 ... \9

部分式とグループの違い

BRE の部分式と ERE のグループは厳密には異なる機能です。例えば選択子を使って「どちらかにマッチ」を書いたらキャプチャしたときの番号がずれて困ったことはないでしょうか?それは異なる機能を合わせ持っているからこのような問題が起きるわけです。他のプログラミング言語ではキャプチャなしのグループがありますが、シェルスクリプトの場合、キャプチャなしグループが使えるのは macOS の enhanced ERE だけです。

空の正規表現()(exp1|)は使用しない

部分式・グループでの空の正規表現 ()(exp1|) の結果は POSIX では未定義です。実際にエラーになる場合があるので注意してください。GNU の grep のドキュメントには「移植性が必要な場合は使用してはいけません」と書かれています(参考)。

Outside a bracket expression, a <left-parenthesis> immediately followed by a <right-parenthesis> produces undefined results.

訳 ブラケット表現の外で ( の直後に ) を置くと未定義の結果になる

A <vertical-line> appearing first or last in an ERE, or immediately following a <vertical-line> or a <left-parenthesis>, or immediately preceding a <right-parenthesis>, produces undefined results.

訳 ERE の最初または最後に | がある場合、または (||) は未定義の結果になる

# (exp1|) は macOS (BSD) 版 grep でエラーになる
$ echo foo10bar | grep -E 'foo(10|20|30|)bar'
grep: empty (sub)expression

# 代わりに ? を使えば良い
$ printf '%s\n' foobar foo10bar | grep -E 'foo(10|20|30)?bar'
foobar
foo10bar

後方参照と置換文字列の参照の違い

後方参照とは「() でマッチした文字列を正規表現の中で参照する機能」のことです。つまり sed 's/正規表現/置換文字列/g' の「正規表現」の中で使うものが後方参照です。勘違いされやすいですが「置換文字列」の中で使うものの事ではありません。

どちらも同じ \1() でキャプチャした文字列を参照しますが置換文字列が正規表現でないことは明らかです。後方参照が使えると POSIX で規定されているのは BRE だけで ERE では(GNU や macOS の拡張を除き)使えませんが、これは置換文字列の中で参照できないという意味ではありません。

別の言い方をすると、sed -E で正規表現に ERE を使った場合に、後方参照の \1 が使えなくなっても、置換文字列の \1 は使えます。(下記のコード参照)

# (下記の検証は macOS (BSD) 版 sed にて)

# BRE は後方参照が使える
$ echo "abcabc" | sed 's/\(abc\)\1/<\1>/g'
<abc>

# ERE では後方参照は使えない (GNU 版は拡張により使える)
$ echo "abcabc" | sed -E 's/(abc)\1/<\1>/g'
abcabc

# ただし、置換文字列での参照はできる
$ echo "abc" | sed -E 's/(abc)/<\1>/g'
<abc>

正規表現とロケール

文字列処理全般に当てはまる話ですが、正規表現はロケールに依存します。何を一文字とするかはロケールによって異なります。例えば日本語環境(文字コードは UTF-8)のデフォルトのロケール (ja_JP.UTF-8) であれば日本語の文字を一文字として扱いますが、 C (POSIX) ロケールの場合、1 バイトを一文字として扱ってしまいます。(ちなみにロケールの CPOSIX は全く同じ意味です。)

$ echo "あいうえお" | sed -E 's/^(.)/<\1>/g'
<あ>いうえお

# 「あ」の UTF-8 表現 `e3 81 82` の 1 バイト目だけを置換するので壊れる
$ echo "あいうえお" | LC_ALL=C sed -E 's/^(.)/<\1>/g'
<�>��いうえお

現在一般に使われている文字コードは UTF-8 ですし、同じ UTF-8 を使うロケール(例 en_US.UTF-8)であれば、一文字を間違うことはありませんが、文字の並び順や種類が異なります。次項のブラケット表現は文字の並び順や種類がとても重要になってきます。

ブラケット表現 [...] [^...]

正規表現(BRE と ERE の両方)で使うことができる [ ] の中の文字リストの任意の一文字にマッチさせるためのものです。例えば [abc] の場合 a b c のいずれかにマッチします。POSIX では「ブラケット表現」という名前ですが、その他の正規表現の話では [ ] 自体を「文字クラス」呼んでいる場合があるので注意が必要です。

メタ文字 意味
^ [^...] と書くことで否定リスト(... 以外の文字)の意味になります。^ 自体をリストに入れたい場合は、文字リストの 2 文字目以降に記述します。(例 [abc^]
- 範囲 [c-c] と 2 つの文字を - で繋ぐことにより、現在のロケールにおいてその文字の範囲にマッチします。^ 自体をリストに入れたい場合は、[ ] の先頭か末尾に記述します。(例 [abc-]
] [ を閉じます。] 自体をリストに入れたい場合は、[ ] の先頭に記述します。(例 []abc]

ブラケット表現は正しく動かない?

もちろんそんな事はありません。ブラケット表現は正しく動きます。記事の途中でも詳しく書いていますが、ブラケット表現が正しく動かないと勘違いしてる原因は以下の2つです。

範囲 a-z

「範囲」はロケールに依存することに注意してください。例えば [a-z] は現在のロケールが C (POSIX) の時には è にマッチしませんが en_US.UTF-8 の時にはおそらくマッチしますがマッチしないかもしれません。なぜ断定できないのかというと、ロケールが POSIX 以外の場合の範囲の動作は未定義 (unspecified) だからです。ロケールが POSIX の場合は範囲は文字の並び順(照合順序)に従って判定されます。しかしロケールが POSIX 以外の場合、単純に照合順序で判定するとは限りません。

なぜこのような仕様になっているかというと照合順序で判定するのは問題が有るからです、例えば POSIX ロケールではアルファベットは ABCabc という順番で並びますが en_US.UTF-8 の場合は aAbBcC という順番です。この時 [A-B] を単純に解釈してしまうと a は含まれないのに b は含まれるという結果になってしまいます。エストニアのアルファベットはアルファベットの順番が異なり z が最後のアルファベット文字ではありません。また èe の前にあるのか後にあるのか、つまり [a-e][e-g] のどちらにマッチするのでしょうか?これらは単純な方法で解決できる問題ではありません。

POSIX では当初 (POSIX.2-1992) は照合順序で範囲を判定するという仕様でした。のちに上記のような問題があると明らかになり POSIX ロケール以外では「未定義」と仕様が変更になりました。この仕様変更により各実装が考える自然と思われるアルゴリズムで範囲を判定することが POSIX で許可されました。そのため POSIX ロケール以外でも問題は無くなったような動作をするはずですが、どのように「範囲」が判定されるかは実装依存であるため、ロケールに注意する必要があることには変わりありません。これは必ずしも POSIX ロケールを使わなければいけないという意味ではありません。例えば検索ツールのようなものならロケールと実装依存で問題ない場合もあります。ロケールをどのように扱うかは要件次第であり、このような問題を踏まえて適切な方法を選択する必要があります。

この話についての詳細は以下のページで詳しく解説されています。

ブラケットシンボル

ブラケット表現の中では以下の特殊な表記で特殊な文字を表現することができます。これらを POSIX では「Collation-related bracket symbols」と呼んでいるようですが、長いのでここではブラケットシンボルと呼ぶことにします。(ただ厳密に言えば BSD 拡張の [:<:][:>:] は Collation じゃないと思うし、なんなら [: :] 自体が違う気もしますが)

もしこれらブラケットシンボルが全く使えなかったり正しく動いていないと思う場合、それは POSIX に準拠したコマンドではなく互換性のために用意されている歴史的なコマンドを使っているだけです。今どき POSIX に準拠してないコマンドを使う理由はないので「シェルスクリプト実行環境をPOSIX準拠モードに変更して互換性を上げる方法」を参考にして POSIX に準拠したコマンドを使用してください。

ブラケットシンボル POSIX BSD GNU macOS 意味
[:<:] yes yes 単語の先頭にマッチ
[:>:] yes yes 単語の末尾にマッチ
[:name:] yes yes yes yes 文字クラス 下記参照
[.string.] yes yes yes yes 照合シンボル 下記参照
[=char=] yes yes yes yes 等価クラス 下記参照

ブラケットシンボルは [[:alpha:]] のように書いて使うことが多いですが、ブラケット文字 [ を 2 重にして指定するものと勘違いしないようにしてください。これは [ ] の中に [:alpha:] が含まれているだけなので [0[:alpha:]9][[:lower:][:digit:]] のように書くこともできます。またブラケット [] を使わずに直接ブラケットシンボルを書いてしまうのも間違いです。

# これが正しい
$ echo abc | grep '[[:alpha:]]'
abc

# これは間違い
$ echo abc | grep '[:alpha:]'
abc

これは一見ブラケット一つで動いてるように見えますが、単に [] に囲まれた : a l p h a : という文字のいずれかにマッチしているだけです。勘違いしやすいので注意してください。

文字クラス、照合シンボル、等価クラスもロケールに依存することに注意してください。ロケールが正しくないと意図したとおりに動きません。これを知らずにブラケットシンボルが正しく動かないと勘違いしている人が多いです。(参考 「シェルスクリプトで正規表現の照合シンボル[.string.]と等価クラス[=char=]と文字クラス[:classname:]を正しく使う方法」)

文字クラス [:name:]

「文字クラス」は「POSIX 文字クラス」呼ばれることがあります。これは「ブラケット表現」を「文字クラス」と呼んでいる場合に紛らわしいからです。文字クラスを使うと指定した文字クラス名に定義されている文字にマッチさせることが出来ます。

マッチする文字は現在のロケールに依存します。例えば現在のロケール(環境変数 LC_CTYPE)が C の場合には [:alpha:]è にマッチしませんが、en_US.UTF-8 の場合にはマッチします。ちなみに日本語も [:alpha:] にマッチします。同様に [[:upper:]] は全角アルファベットにもマッチします。(en_US.UTF-8 でも ja_JP.UTF-8 でもマッチします)。

POSIX では以下の文字クラスが定義されています。詳しい意味は検索すればすぐに分かることなので省略します。

[:alnum:] [:cntrl:] [:lower:] [:space:] [:alpha:] [:digit:]
[:print:] [:upper:] [:blank:] [:graph:] [:punct:] [:xdigit:]

また [:name:] という書式でロケール依存の name が使用できるようなのですが、これに対応している実装はまだ見たことがありません。(本気で探したわけではありませんが)

ちなみに [:blank:] が GNU 拡張と言われることがあるようですが、おそらく SRE(単純正規表現) の話と混同されているのだと思います。SRE は BRE とほぼ同じ仕様に見えますが [:blank:] は含まれていません。

照合シンボル [.string.]

現在のロケールにおいて、指定した照合文字を 1 文字とみなしてマッチします。例えば現在のロケール(環境変数 LC_COLLATE)が cy_GB.utf8 の場合に [.ch.]ch を 1 文字とみなしてマッチします。

[.ch.]ch とマッチする文字の ch が同じなので勘違いしやすいですが、この照合文字はロケールの定義に書かれている名前で、自由な文字を指定できるものではないので注意してください。つまり [.abcde.] のように書いても abcde にマッチすることはなく、無効な照合文字としてエラーになります。

等価クラス [=char=]

現在のロケールにおいて、指定した文字と等価な文字と定義されている文字にマッチします。例えば現在のロケール(環境変数 LC_COLLATE)が en_US.UTF-8 の場合に [=e=]e è é ê ë にマッチします。

エスケープシーケンス

冗長なブラケット表現の略記法です。多くのプログラミング言語の正規表現で必ずと言っていいほど使っているものですが、残念ながら POSIX では定義されておらず POSIX に準拠したシェルスクリプトを書く場合には基本的に使えません。またブラケット表現と同様にロケールに依存するので注意してください。

POSIX には後方参照以外のエスケープシーケンス(エスケープ文字で始まる文字列)がないため、エスケープシーケンスという用語は使われていません。ここでは Perl の正規表現 の用語を採用しています。「エスケープシーケンス」の代わりに「メタ文字列」("文字"ではなく"文字列")という用語を採用している有名な例(詳説 正規表現)も見かけますが、どちらかと言えば一般的な用語ではないと思います。BRE の場合は {} () + ? | をエスケープシーケンスで表現するためここに含めるのが正確だとは思いますが、この記事では説明の都合上これらを特殊文字の例外的な表記として扱う事としここには含めていません。同様の理由で後方参照も省略します。

注意 以下のエスケープシーケンスは POSIX 準拠では使えません

旧 UNIX BSD GNU macOS マッチする文字 同等のブラケット表現
\< yes yes yes 単語の先頭 [[:<:]] (BSD・macOS のみ)
\> yes yes yes 単語の末尾 [[:>:]] (BSD・macOS のみ)
\b yes yes 単語の区切り
\B yes yes 単語の区切り以外
\d yes 数字 [[:digit:]]
\D yes 数字以外 [^[:digit:]]
\s yes yes 空白文字 [[:space:]]
\S yes yes 空白以外の文字 [^[:space:]]
\w yes yes 単語の文字 [_[:alnum:]]
\W yes yes 単語の文字以外 [^_[:alnum:]]

ここでいう「旧 UNIX」とは POSIX に準拠してない歴史的な UNIX で使われていた正規表現ことです。具体的には Solaris で確認しています。\<\> は GNU 拡張と説明されていることが多いですが、実際には歴史的な UNIX 時代から存在していたものです。POSIX で標準化されなかったのは BSD で実装されていなかった、もしくは [:<:] [:>:] として実装されたために、移植性がなかったからでしょう。

GNU 拡張は、BRE と ERE の両方で使用可能です。\s\S は 2000 年過ぎぐらいに導入された比較的新しいエスケープシーケンスです。すべてのコマンドに一気に導入されたわけではなくコマンド毎に徐々に対応していったようです。

macOS 拡張も BRE と ERE の両方で使用可能ですが、ENHANCED FEATURES が有効になっているコマンドのみです。grep は対応していそうですが、sed は対応してなさそうです。

C 言語スタイル

これらは(POSIX 準拠の) awk や GNU 版 や macOS 版のコマンドで使える可能性がある C 言語スタイルのエスケープシーケンスです。他のプログラミング言語の正規表現でも使えることが多いですが、元々は正規表現のエスケープシーケンスとは別のものです。

C 言語スタイルのエスケープシーケンスを避けて正規表現のエスケープシーケンスが定義された・・・と思いきや実は \b (単語の区切り)がかぶっています。どちらが採用されるかはコマンドや正規表現次第ですが、awk (gawk) ではバックスペースとして扱われ、他の言語の正規表現だと [\b] のように文字クラス(ブラケット表現)の中でのみバックスペースと解釈されるようになっていたりします。

注意 以下は C 言語スタイルのエスケープシーケンスのリストであり、コマンドで使えるエスケープシーケンスのリストではありません。実際に使えるエスケープシーケンスはコマンドにって異なります。awk に関しては POSIX で規定されているものを明記しています。

エスケープシーケンス 文字コード 意味 awk
\a 0x07 ベル (BEL) yes
\b 0x08 バックスペース (BS) yes
\f 0x0C 改ページ (FF) yes
\n 0x0A 改行 (LF) yes
\r 0x0D キャリッジリターン (CR) yes
\t 0x09 水平タブ (HT) yes
\v 0x0B 垂直タブ (VT) yes
\' 0x5C シングルクォート
\" 0x27 ダブルクォート yes
\\ 0x22 バックスラッシュ yes
\? 0x3F 疑問符
\ooo 8 進表記の ASCII 文字 yes
\xhh 16 進表記の ASCII 文字
\uhhhh 16 進表記の Unicode 文字
\Uhhhhhhhh 16 進表記の Unicode 文字

各コマンドと正規表現の注意点

よく使われていると思われるコマンドのみに絞って解説します。特に明記していない場合 POSIX で標準化されている範囲を解説しています。いずれのコマンドも BRE もしくは ERE を採用していることになっていますが、コマンドによってわずかに仕様が異なります。その理由はこれらのコマンドは元々 POSIX 以前に作成されたコマンドであり、バラバラに実装・拡張されてきたからです。POSIX という標準規格の登場で基本的に共通化されましたが全てが同じ仕様になったわけではありません。

補足ですが POSIX では、これらのコマンドでパス名を処理する時はロケールによっては無効な文字シーケンスが生成される可能性があるということで LC_ALL=C にすることを推奨しています。(個人的にはもう全部 UTF-8 系になったし UTF-8 以外は非対応とし、そんな変な名前をつける方が悪いということで気にしていません)

[[ ]] コマンド(シェルビルトイン)

POSIX シェルで標準化されているコマンドではないので詳細は省略します。標準規格がなく各シェルの実装を調べてまとめなきゃいけないので大変だからです。使い方は [[ "文字列" =~ 正規表現 ]] です。正規表現の部分にはダブルクォートは必要ありません。bash ではダブルクォートでくくった時に動作がバージョンによって違うとかいう問題があったはずです。詳細は別の機会にまとめるかもしれません。

ちなみに私は POSIX 準拠かつパフォーマンスのために外部コマンドをなるべく使用しないようにしてるので、ifcase、パラメータ展開を組み合わせて文字列判定を行う関数(例えば is_number 関数のようなもの)を作ってます。正規表現の方が楽ですが関数作るのは面倒くさいという程度の話です。使いまわしできますしね。

awk コマンド

固有の正規表現の拡張機能

awk は ERE を採用しています。POSIX の標準規格として awk では正規表現の中(ブラケット表現の外と中の両方)で「C 言語スタイルのエスケープシーケンス」が使えるように拡張されています。使える C 言語スタイルのエスケープシーケンスは \a, \b, \f, \n, \r, \t, \v, \", \\, \ooo (C 言語スタイルのエスケープシーケンスのすべてではない)で、加えて正規表現を囲うための \/ です。これ以外の文字に \ をつけた場合の意味は未定義となっています。

空の正規表現の意味

おそらく全ての awk の実装で、空の正規表現指定すると以下のように文字の境界にマッチしているようなのですが、これが POSIX で規定されているかどうかは不明です。(今の所書いてある場所を見つけていない。)

$ echo "abc" | gawk  '{ gsub(//, "@"); print}'
@a@b@c@

文字列リテラルの正規表現の解釈

awk は正規表現リテラルがサポートされていますが文字列で正規表現を指定することもできます。文字列で指定した場合、awk 言語としてのエスケープシーケンスの解釈と正規表現のエスケープシーケンスの解釈の、二回のエスケープシーケンスの解釈が行われるので注意してください。下記の例は正規表現の \s (空白文字 = スペースやタブ等)を置換する例です。

# これは正規表現リテラルで、正規表現のエスケープシーケンスの解釈だけが行われる
$ printf 'a \t\nb' | awk  '{ gsub(/\s/, "@"); print}'
a@@
b

# これは文字列リテラルで、awk 言語と正規表現のエスケープシーケンスの二回の
# 解釈が行われるため二重にエスケープが必要
$ printf 'a \t\nb' | awk  '{ gsub("\\s", "@"); print}' 
a@@
b

# gawk の場合、警告してくれるのでありがたい
$ printf 'a \t\nb' | gawk  '{ gsub("\s", "@"); print}'
awk: コマンドライン:1: 警告: エスケープシーケンス `\s' は `s' と同等に扱われます
a
b

この文字列リテラルの二回のエスケープシーケンスの解釈は、エスケープシーケンスによっては一回で正しく動く例が存在します。

$ printf 'a\tb\n' | awk  '{ gsub(/\t/, "@"); print}'
a@b

# 先程の例とは違い警告されず、正しく動く
$ printf 'a\tb\n' | gawk  '{ gsub("\t", "@"); print}'
a@b

なぜこうなるのかと言うと、文字列リテラルで \t を指定し、それが awk 言語のエスケープシーケンスとしてタブ文字に解釈されますが、正規表現の一部としてタブ文字を使うことは有効な正規表現だからです。最初の例では正規表現の \s を置換したかったため gsub 関数に \s を渡す必要がありましたが、\t の場合は gsub 関数にタブ文字を渡しても \t を渡しても同じ意味になるのでエスケープは一回でも良いのです。

この違いを理解してないと gsub 関数などに渡す正規表現をエスケープする場合に、バックスラッシュの数は一つで (\) でいいのか二つ (\\) 必要なのかと混乱してしまうことがあります。

置換文字列での参照と特殊記法

置換文字列は sub 関数や gsub 関数で正規表現にマッチした部分を置換する文字列です。置換文字列自体は awk の文字列であるため awk のエスケープシーケンスが使えます。置換文字列自体は正規表現ではありませんが、置換文字列の中で & を使うことでマッチした全体を参照することができます。& そのものに置換したい場合は \\& と記述します(awk の文字列としてエスケープシーケンスが解釈され、\&& と解釈されてしまうので \ は二重に記述する必要がある)

$ echo "foo bar baz" | awk '{ sub(/(bar)/, "<&>\n\\& \\\\ \\\\\\&"); print }'
foo <bar>
& \ \& baz

なお awk が採用している ERE には正規表現の後方参照はありません。ただし gawk の拡張機能の gensub を使用すると置換文字列の中でキャプチャした文字列を参照することができます。

$ echo "a b" | gawk '{ print gensub(/(.) (.)/, "\\2 \\1", "g")}'
b a

\\ への置換の互換性問題

正規表現と直接関係はないのですが関連情報として書いておきます。

二重バックスラッシュに置換する場合の書き方は、POSIX 準拠の方法はこのように書きます。

$ echo "@" | gawk --posix '{ sub(/@/, "A\\\\\\\\Z"); print }'
A\\Z
  1. awk の文字列として "A\\\\\\\\Z""A\\\\Z" と解釈される
  2. 置換文字列の特殊記法として "A\\\\Z"A\\Z と解釈される

しかし、nawk では 2. の解釈が行われないようです。

# macOS の /usr/bin/awk は nawk。また Debian では original-awk が nawk。
$ echo "@" | /usr/bin/awk '{ sub(/@/, "A\\\\\\\\Z"); print }'
A\\\\Z

# POSIX 非準拠
$ echo "@" | /usr/bin/awk '{ sub(/@/, "A\\\\Z"); print }'
A\\Z

# gawk のデフォルトは nawk との互換性が考慮されている?
$ echo "@" | gawk '{ sub(/@/, "A\\\\Z"); print }'
A\\Z
$ echo "@" | gawk '{ sub(/@/, "A\\\\\\\\Z"); print }'
A\\Z

回避策はありますが、もっとシンプルにしたい所・・・。

$ echo "@" | /usr/bin/awk '
  BEGIN { DB="\\"; gsub(/\\/, "\\\\", DB); if (DB != "\\\\") DB = "\\\\\\\\" }
  { sub(/@/, "A" DB "Z"); print }
'
A\\Z

バックスラッシュを二重バックスラッシュに置換する場合は簡単な書き方があります。

$ echo 'A\Z' | /usr/bin/awk '{ gsub(/\\/, "&&"); print }'
A\\Z

{m,n} 未実装による互換性問題

awk は歴史的に ERE に近い正規表現を実装していましたが {m,n} は実装されていませんでした。POSIX.2-1992 で awk も ERE を使うものとされ {m,n} の対応が POSIX で要求されましたが、POSIX に準拠してない awk の実装はまだ残っています。

  • nawk: 20180827 時点で未対応2019-03 に修正されたので次のリリースには入るはず
    • macOS 版: おそらく POSIX 準拠となった 10.5.0 (2007) から対応している
    • FreeBSD 版: 12.3 (2021-12), 13.0 (2021-04) から対応
    • NetBSD 版: 9.0 (2020-02) から対応
    • OpenBSD 版: 6.8 (2020-10) から対応(?) (2020-06 頃マージされている)
  • mawk: 1.3.4 20200120 時点で未対応プルリク はあるが 2016 年から放置状態
  • gawk: 3.0 (2010) より --re-interval オプションで対応。4.0 (2011) よりデフォルト
  • Busybox awk: 1.1.3 時点で対応してるのを確認。いつから対応しているかは不明
  • Solaris: Solaris 10 の /usr/xpg4/bin/awk で対応してるのを確認

nawk は新しい OS では対応されてきていますが、mawk を使用している Debian / Ubuntu がネックですね(Debian での対応状況)。mawk もいずれは対応すると思われますが、その時に問題が出ないように今から書き方に気をつけておく必要があります。具体的には {} という文字とマッチさせたい時はエスケープするかブラケット表現の中で使うようにする必要があります。

# 現在の mawk は {} はただの文字として扱われる
$ echo "a{3}" | mawk '/a{3}/{print "ok"}'
ok

# 他の awk の実装ではマッチしない(POSIX に準拠した動作)
$ echo "a{3}" | gawk '/a{3}/{print "ok"}'

# 以下の書き方なら、どちらも文字として扱われるし将来 mawk が {m,n} に対応しても問題ない
$ echo "a{3}" | mawk '/a\{3\}/{print "ok"}'
$ echo "a{3}" | gawk '/a\{3\}/{print "ok"}'

[GNU] \b 重複問題

awk では POSIX の仕様として正規表現で C 言語スタイルのエスケープシーケンスが使えます。さらに gawk では拡張された正規表現のエスケープシーケンスも使えます。この中で唯一 \b は両方で定義されています。当然 awk としての標準仕様の方が優先ですので \b と書けば C 言語スタイルのエスケープシーケンスとしてバックスペースと解釈されます。では正規表現の \b(単語の区切り)が使いたい場合はどうするか? \b の代わりに \y を使います。

$ printf "foo\nfoobar\nbar\n"
foo
foobar
baz

$ printf "foo\nfoobar\nbar\n" | sed -n '/\bbar/p'
bar

printf "foo\nfoobar\nbar\n" | awk '/\ybar/{print}'
bar

expr コマンド

固有の正規表現の拡張機能

expr は BRE を採用しています。expr STRING : REGEXP という表現で正規表現のマッチングができます。正規表現は ^ で始めなくても文字列の頭からマッチするという意味になるので注意してください。正規表現の最初の文字を ^ にした場合に、どのように解釈されるかは POSIX では指定されていません。(多くの実装は正規表現の ^ アンカーとして解釈されるようですが、^ で始まる文字列と比較した場合どうするのがいいのか?頭にダミーの文字をつけるのが一番かなぁ?)

空の正規表現の意味

正規表現が空文字の場合の挙動は POSIX では何も指定されてないようですが、GNU 版 expr は、一致せず 0 を出力し終了ステータス 1 になったのに対して、macOS(BSD 版)では、一致せずエラーメッセージを標準エラー出力に出力し、終了ステータス 2 (無効な式)と違いがでたので、POSIX 標準規格に書かれてないことを指摘したら「終了ステータスは 0 以外で、その出力は未定義である」とかになると思います。

部分式によるマッチ部分の出力

部分式 \( \) には後方参照の他にもう一つの役目があります。それは expr の出力です。通常はマッチした文字列の長さを返すのですが、部分式を使うとマッチした文字列を返すようになります。返す文字列は後方参照の \1 に相当する部分だけです。

expr "abc123" : "[a-z]*[1-3]*"
6

expr "abc123" :  "\([a-z]*\)\([1-3]*\)"
abc

grep コマンド

固有の正規表現の拡張機能

grep は BRE または ERE (-E オプション)を採用しています。

一般的に正規表現に改行文字を入れることは可能ですが、grep の場合は正規表現に改行文字を入れると OR 結合の複数の正規表現を意味するので注意してください。つまり以下のように機能します。

$ seq 100 | grep "10
^2" | xargs # 出力が縦に長いので xargs で 1 行にしています
2 10 20 21 22 23 24 25 26 27 28 29 100

上記の場合 10 または ^2 にマッチする文字列を検索でしており grep -e "10" -e "^2" と同じ意味です。

空の正規表現の意味

空文字の正規表現はすべての行にマッチします。

[GNU] Perl 互換の正規表現検索

GNU grep では -P オプションで Perl 互換の普段良く使っている(?)高度な正規表現 (PCRE) を使うことができます。この記事では詳細を省きますが GNU grep 以外との移植性が必要ない場合は -P オプションを使うのも良いでしょう。その場で実行するだけのものに移植性は必要ないですからね。移植性よりも効率です。まあ私は 普段 PCRE がデフォルトの ag - The Silver Searcher を使っているわけですが。

sed コマンド

固有の正規表現の拡張機能

sed は BRE または ERE (-E オプション) を採用しています。

POSIX の正規表現では \n は改行文字とはみなされません。しかし sed は例外で \n を改行文字とみなす(これは GNU 拡張ではありません)ため sed の置換命令を使って改行文字を削除することができます。ただし通常は 1 行ずつ読み込んで置換処理を行うため(この読み込んだメモリ領域のことをパターンスペースと言います)、置換対象のパターンスペースには改行文字が含まれていません。そこで全ての行をパターンスペースに読み込んでから改行文字を削除します。

# パターンスペースには改行文字が含まれている
$ seq 5 | sed -e ':loop' -e 'N; $!b loop'
1
2
3
4
5

# パターンスペースの改行文字を削除できる
$ seq 5 | sed -e ':loop' -e 'N; $!b loop' -e 's/\n//g'
12345

# 読みやすく(?)書き直すと
$ seq 5 | sed '
:loop
N
$!b loop
s/\n//g
'

コードの意味

  1. :loop 戻り先用のラベル
  2. N 次の行を(改行文字を追加して)パターンスペースに追加する
  3. $(最終行) !(でなかったら) b(分岐する) loop
  4. s/\n//g パターンスペースに読み込まれた全てのデータの改行文字を削除する

上のコードで複数の -e を利用しているのは、BSD 版の sed がラベル名と次の命令を ; で繋いで一行で書くことがことができないからです。

正規表現を囲む文字のエスケープ

POSIX の正規表現では規定された特殊文字以外を \ でエスケープした場合、その解釈は未定義となっていますが、sed では例外的に正規表現を囲った文字をエスケープすることができます。つまりこういう事です。

$ echo foobar | sed 's/foobar/baz/g'    # 一般的な書き方
$ echo foobar | sed 's|foobar|baz|g'    # / の代わりに任意の文字を使うことができる
$ echo foobar | sed 'sbfoo\barb\bazbg'  # b を使った場合
$ echo foo1bar | sed 's1foo\1bar1baz1g' # 1 を使った場合(\1 は後方参照としての意味を失う)

通常は \b の解釈は未定義なのですが b を正規表現を囲う文字として使った場合 foobarfoo\bar とエスケープして書かなければならなくなるので、この場合に限っては通常の文字として解釈されると POSIX で規定されています。また \1 は本来は後方参照を意味しますが 1 を区切り文字として使った場合は、正規表現の中の 1\1 と書くため後方参照ではなくなります。

ただ念の為ですがこんなややこしい書き方はしないでください!区切り文字には正規表現にも置換文字列にも使われてない記号文字を使うものです。

空の正規表現の意味

正規表現が空文字の場合、最後に使用した正規表現を意味します。これを利用すると簡単に m 番目と n 番目にマッチするものだけを変換するというようなことができます。

$ echo "a a a a a" | sed 's/a/B/2; s//C/3'
a B a C a

# 一度も正規表現を使ってなければエラーになる
$ echo "a a a a a" | sed 's//C/3'
sed: first RE may not be empty

置換文字列での参照と特殊記法

置換文字列とは s/正規表現/置換文字列/ の「置換文字列」の部分のことです。置換文字列自体は正規表現ではありませんが、マッチした全体またはキャプチャした文字列を参照することができます。その他置換文字列で使用可能なエスケープシーケンスは以下のとおりです、

置換文字列 意味
& マッチした文字列全体に置換
\& & そのものに置換
\1 ... \9 後方参照に対応するキャプチャした文字列に置換
\<改行> 改行文字に置換
\\ \ そのものに置換
\<上記以外> 未定義

[GNU] 固有の正規表現の拡張機能

さて、先程のコードを少し書き換えて、改行を削除する代わりに、行頭に @ を追加するコードに変更します。

$ seq 3 | sed -e ':loop' -e 'N; $!b loop' -e 's/^/@/g'
@1
2
3

見ての通り、このコードは最初の行の行頭だけに @ を追加します。GNU sed にはマルチラインモードというのがあり、これを使うと 1 行毎に処理を行うことができます。

$ seq 3 | sed -e ':loop' -e 'N; $!b loop' -e 's/^/@/gM'
@1
@2
@3

そしてこのマルチラインモードで、行頭ではなく全体の最初にマッチするエスケープシーケンスが \` です。(一周しただけの気がしますが・・・)

seq 3 | sed -e ':loop' -e 'N; $!b loop' -e 's/\`/@/gM'
@1
2
3

ということで GNU sed だけで使える追加の正規表現のエスケープシーケンスです。

意味
\` マルチラインモードで最初の行の行頭にマッチ
\' マルチラインモードで最後の行の行末にマッチ

tr コマンド

このコマンドは正規表現と全く関係ありません。

正規表現のブラケット表現と似ている表記で、範囲、POSIX 文字クラス、等価クラスが使えるため、勘違されそうなので念のために書いておきます。このコマンドも使い方を勘違いしている人が多いため、以下の参考リンクを紹介しておきます。

trの範囲指定は POSIX準拠のシェルスクリプトなら 0-9 で動きます。だから [0-9] や 0123456789 と書く必要はなかったんですね

vi / ex コマンド

固有の正規表現の拡張機能

viex と同じ正規表現が利用されています。vi / ex では BRE が使われていますが、以下の拡張機能が POSIX で要求されています。

意味
\< 単語の先頭にマッチ
\> 単語の末尾にマッチ
~ 前回置換した時の置換文字列

空の正規表現の意味

最後に使用した正規表現です。

[vim] ERE 対応

多くの人は vi の代わりに拡張された vim を使っているのではないかと思いますが、vim では検索で使うパターンが以下の 4 種類あります。

  • very magic ・・・ ERE 相当
  • magic ・・・ デフォルト BRE 相当
  • nomagic
  • very magic

検索する時のパターンに \v と書くと、それ以降は very magic (ERE) として扱われます。デフォルトを very magic にする直接の方法はないため :nnoremap / /\v でキーの割当を変更する方法がよく紹介されています。

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
8
Help us understand the problem. What are the problem?