シェルスクリプト (bash, ksh, yash, zsh) で正規表現を使う方法のまとめ

## はじめに

一部の POSIX シェルには、シェル自体に正規表現対応の機能が含まれており、外部コマンドに依存せずに正規表現による比較を行えます。すべての POSIX シェルで使えるわけではありませんが、シェルに含まれている機能であるため環境の違いを気にする必要はなくパフォーマンスも良いというメリットがあります。しかし正規表現に対応している bash、ksh、yash、zsh で、実装にそれぞれ違いがあります。この記事ではその違いをまとめました。現時点でのそれぞれの最新版である bash 5.2、ksh 93u+m/1.0.3、yash 2.53、zsh 5.9 で動作確認しています。

なお POSIX 正規表現の話や、コマンド（POSIX コマンド・UNIX コマンド）で正規表現を使用する場合の注意点などは「**[シェルスクリプトの正規表現の詳細解説（令和最新版）〜 基本正規表現(BRE)と拡張正規表現(ERE)](https://qiita.com/ko1nksm/items/53abc144558b9bb5629f)**」を参照してください。

## シェルの正規表現

どのシェルでも対応している正規表現は拡張正規表現 (ERE) です。今の `[[ 変数 =~ 正規表現 ]]` という書き方がシェルで使えるようになった時期は案外遅く bash と ksh では 2004〜2006 年頃です。一般的には先に ksh で実装された機能を bash が後から取り入れるという流れが多いのですが、どちらが先に実装したのかはよくわかりませんでした。bash の方が先にも思えるのですが ksh が今と違う形で実装されていた可能性がありますし、先に ksh で実装を始めていたものを bash がすぐに追尾してリリースされたのは bash が先などが考えられるので詳細に調べないとわかりません。zsh は少し遅れて 2012 年頃のようです。yash は一番最後で 2018 年の終わり頃です。Perl に正規表現の機能が追加されたのが 1988 年、jQuery 1.0 が 2006 年であることを考えると随分遅くにようやく正規表現に対応したんだなという印象です。したがって 1992 年の POSIX.2 の時点では当然標準化されるわけもなく、対応している POSIX シェルも限られ、bash、ksh、zsh 以外では外部コマンドに頼らなければ正規表現が使えないという残念な状況です。

### 正規表現の書き方

正規表現は `[[ 変数 =~ 正規表現 ]]` という書き方で、`=~` 演算子を使って比較・マッチさせます。例えば `[[ $var =~ ([0-9]+) ]]` のように書きます。左側の変数はダブルクォーテーションで括っても括らなくても意味は同じです。右側の**正規表現はダブルクォーテーションで括ってはいけません**。シェルやバージョンによっては括っても動くのですが動かない場合もあります。正規表現を変数に入れて使う場合は `[[ $var =~ $re ]]` のように書きます。この場合も、正規表現を入れた変数はダブルクォーテーションで括ってはいけません。もちろん変数に正規表現文字列を代入する時には（必要な場合は）クォートします。これらのシェルに正規表現リテラルというものはありませんが `=~`の右側に書いた時には正規表現リテラルのような感じで少し特殊な文法の解釈が行われます。変数に代入する時はただの文字列です。

- GOOD: `[[ $var =~ ([0-9]+) ]]`
- GOOD: `[[ "$var" =~ ([0-9]+) ]]`
- GOOD: `[[ $var =~ $re ]]`
- BAD: `[[ $var =~ "([0-9]+)" ]]`
- BAD: `[[ $var =~ "$re" ]]`

**補足** 一般的にコマンドにわたす引数の変数はダブルクォーテーションで括らねばなりません。例えば `[ ]` の場合は `[ "$var1" = "$var2" ]` としなければいけません。そうしなければ `var1="a b"` かつ `[ $var1 = "$var2" ]`の時に `[ a b = "var2" ]` と解釈されてしまいます。一方 `[[ ]]` の場合はダブルクォートしてはいけません（`[ "$var" =~ $re ]]` または `[[ $var =~ $re ]]` と書く）。この違いは `[ ]` がコマンド（`[` コマンド）であるのに対して `[[ ... ]]` はシェルの文法だからです。コマンドの場合、変数に対して単語分割（スペースなどで引数を分割すること）などを行うことが仕様としてすでに決められていますが、シェルの文法として実装する場合はコマンドの仕様に必ずしも従う必要がなく、`[[ ]]` の場合は単語分割が行われません。

### キャプチャ変数

`( )` を使ってマッチした部分はシェルの特殊変数にキャプチャされますが、その変数名はそれぞれのシェルで異なります。なお yash にはマッチした部分を変数にキャプチャする機能は 2.53 の時点ではないようです。

|                        | bash                 | ksh               | zsh            |
| ---------------------- | -------------------- | ----------------- | -------------- |
| マッチした全体         | `${BASH_REMATCH[0]}` | `${.sh.match[0]}` | `$MATCH`       |
| キャプチャした部分     | `${BASH_REMATCH[@]}` | `${.sh.match[@]}` | `${match[@]}`  |
| マッチした開始位置     | -                    | -                 | `$MBEGIN`      |
| マッチした終了位置     | -                    | -                 | `$MEND`        |
| キャプチャした開始位置 | -                    | -                 | `${mbegin[@]}` |
| キャプチャした終了位置 | -                    | -                 | `${mend[@]}`   |

ちなみに bash と ksh には zsh の `MBEGIN` と `MEND` 相当のものがありませんが、これは自分で計算することができます。以下は bash での実装です。（`mbegin` と `mend` も計算で求められるはずだが、面倒なので省略）

```sh
str="ab123cd456"
if [[ $str =~ ([0-9]+)[^0-9]+([0-9]+) ]]; then
    p="${str%%"${BASH_REMATCH[0]}"*}"
    MBEGIN=$((${#p} + 1)) MEND=$((${#p} + ${#BASH_REMATCH[0]}))
    # 備考 zsh のデフォルトに合わせて最初の文字位置を 1 と計算しているが
    # BEGIN, MEND は KSH_ARRAYS 設定に依存するので -1 した方が良いかもしれない
fi
```

## bash の正規表現

bash で正規表現がサポートされたのはバージョン 3.0 (2004-08-03) です。3.2 (2006-10-11) で正規表現パターンの解釈が変更され `[[ ]]` 内で正規表現をダブルクォートした時の意味が代わりました。現在の bash の実装では正規表現をダブルクォートすると正規表現ではなく文字列として解釈されます。つまり現在は `[[ "1[0-9]" =~ [0-9]"[0-9]" ]]` がマッチするような形で動作します。一応[互換モード](https://www.gnu.org/software/bash/manual/html_node/Shell-Compatibility-Mode.html)を `BASH_COMPAT=31` にすれば bash 3.1 の動作に戻すことができますが、macOS でさえ bash 3.2.57 なので新しいシェルスクリプトで使う必要はなく、必要があるとしたら古い bash スクリプトを最新の bash で動作するように修正する時に最小限の修正ですませたいときぐらいでしょう。

正規表現にマッチした文字列は配列変数 `BASH_REMATCH` に代入されます。正規表現全体にマッチした部分は `BASH_REMATCH[0]` に、グループ `( )` にマッチした部分は `BASH_REMATCH[1]` から順番に代入されていきます。以下はその例です。

```sh
if [[ "ab123cd456" =~ ([0-9]+)[^0-9]+([0-9]+) ]]; then
    echo "${BASH_REMATCH[0]}" # => 123cd456
    echo "${BASH_REMATCH[1]}" # => 123
    echo "${BASH_REMATCH[2]}" # => 456
fi
```

## ksh の正規表現

ksh で正規表現がサポートされたのは ksh93 からです。`[[ string ~= ERE ]]` の構文がサポートされたのは、2006-05-10 ですが、それ以前から `[[ string == ~(E)ERE ]]` という構文が使用可能で、この構文に対応した年がいつかはよくわかりませんでした。ドキュメントをよく見てもわからなかったのですが、正規表現をダブルクォートすると正規表現ではなくて前方一致、後方一致、中間一致として扱われるように思えます。正規表現と同じ先頭 (`^`) と 末尾 (`$`) だけが使用可能で、その他の正規表現のメタ文字は使用できません。

正規表現にマッチした文字列は配列変数 `.sh.match` に代入されます。bash と変数名が異なっているだけです。

```sh
if [[ "ab123cd456" =~ ([0-9]+)[^0-9]+([0-9]+) ]]; then
    echo "${.sh.match[0]}" # => 123cd456
    echo "${.sh.match[1]}" # => 123
    echo "${.sh.match[2]}" # => 456
fi
```

余談ですが変数名に `.` が含まれている変わった名前ですが、これは ksh では正しい名前です。`.sh.match` の `sh` は ksh の名前空間 (namespace) と呼ばれる機能で、以下のように実行すると `.sh` に含まれた変数の一覧を得ることができます。

```console
$ ksh -c 'echo "${.sh[@]}"'
command dollar edchar edcol edmode edtext file fun level lineno
match math name pool stats subscript subshell type value version
```

ksh93u+m では `[ ]` でも正規表現が使えるようになりました。シェルのメタ文字（`(`, `)`、 `|` など）は　`\` でエスケープすることで同じような意味になるようです。ただし `[[ ]]` の方がシンプルに使えるため、あえて `[ ]` で正規表現を使う理由はないでしょう。

### パターンで正規表現を使う

ksh では `case` やファイル名のパターンとして正規表現を使うことができます。例えば以下のように書くことができます。

```sh
case "ab123" in
    ~(E:[^0-9]+[0-9]+) ) echo "matched" ;;
esac
```

最初の `E` は拡張正規表現 (ERE) の意味で、`man ksh` より以下の正規表現の方言に対応しているようです。

- `E` The remainder of the pattern uses extended regular expression syntax like the `egrep(1)` command.
- `F` The remainder of the pattern uses `fgrep(1)` expression syntax.
- `G` The remainder of the pattern uses basic regular expression syntax like the `grep(1)` command.
- `X` The remainder of the pattern uses augmented regular expression syntax like the `xgrep(1)` command.
- `P` The remainder of the pattern uses `perl(1)` regular expression syntax. Not all perl regular expression syntax is currently implemented.
- `V` The remainder of the pattern uses System V regular expression syntax.

**補足** 上記の `xgrep` は XML ファイルを検索するコマンドの方ではありません。「augmented regular expression」と呼ばれる正規表現を使用する `grep` コマンドのようです。AST - AT&T Software Technology に含まれる [grep.c](https://github.com/att/ast/blob/2016-01-10-beta/src/lib/libcmd/grep.c) を `xgrep` コマンドの名前で起動したものではないかと思っています。ソースコードの中には「Augmented regular expression」の他「Approximate regular expressions」という正規表現の名前を見つけたのですが、この 2 つってどのような正規表現なのでしょうか？

### 正規表現とシェルパターンの相互変換

`printf` コマンドを利用することで、正規表現とシェルパターンを相互に変換することができます。

```sh
# 正規表現からシェルパターンへの変換
printf '%P' '^[0-9]+$' # => +([0-9])

# シェルパターンから正規表現への変換
printf '%R' '+([0-9])' # => ^([0-9])+$
```

便利な機能に思えなくもないですが、いまいちどういう時に使うことを想定して実装したのかはよくわかりません。

ちなみに ksh 93u+m/1.0.3 から（ksh 93u+ までは使えない）`printf` コマンドに `-v` オプションが追加されたので、変換結果を変数に入れる時は以下のようにするとコマンド置換を使うよりもパフォーマンスが高く（約 8 倍ぐらい？）なります。

```sh
# ksh 93u+ まで
ret=$(printf '%P' '^[0-9]+$')
ret=$(printf '%R' '+([0-9])')

# ksh 93u+m/1.0.3 以降
printf -v ret '%P' '^[0-9]+$'
printf -v ret '%R' '+([0-9])'
```

## yash の正規表現

yash で正規表現がサポートされたのは 2018-12-22 にリリースされた 2.48 です。この時に `[[ ]]` 自体がサポートされています。それまでは `[[ ]]` は使えませんでした。正規表現のサポートは最も遅いですが、そもそも yash 自体が他のシェルに比べると比較的新しいシェルです。詳細は把握していませんが 2.51 (2020-12-12）で `[[ 変数 =~ 正規表現 ]]` の正規表現の解釈が bash とほぼ同じになり、`( )` と `|` が使用できるようになったようです。

bash との互換性を高めており bash と同等の動きを目指していると思われます。正規表現をダブルクォートした場合、bash と同じように文字列として扱われます。つまり `[[ "1[0-9]" =~ [0-9]"[0-9]" ]]` がマッチします。

現時点での最新版である 2.53 (2022-08-23) で正規表現にコマンド置換が含まれているバグが修正されたりと少し注意が必要かもしれません（もうそろそろ十分安定していそうですが）。また現時点では正規表現にマッチした文字列を変数に代入する機能は実装されてなさそうです。

yash では `[ ]` でも正規表現が使えます。シェルのメタ文字（`(`, `)`、 `|` など）は　`\` でエスケープすることで同じような意味になるようです。ただし `[[ ]]` の方がシンプルに使えるため、あえて `[ ]` で正規表現を使う理由はないでしょう。

## zsh の正規表現

zsh で正規表現がサポートされたのは、おそらく 2012-07-24 にリリースされた 5.0 からです。正規表現をダブルクォートしても正規表現として使えるようですが、bash との互換性を考慮してそのまま書くか変数に入れて使用することを推奨します。

正規表現にマッチした文字列は、全体は `MATCH` 変数に、キャプチャした文字列は配列変数 `match` に代入されます。

```sh
if [[ "ab123cd456" =~ ([0-9]+)[^0-9]+([0-9]+) ]]; then
    echo "${MATCH}" # => 123cd456
    echo "${match[1]}" # => 123
    echo "${match[2]}" # => 456
fi
```

重要な注意点は、zsh ではデフォルトでは配列のインデックス番号は 1 から始まるということです。上記の例では `${match[0]}` は存在しません。そのため bash と zsh では配列のインデックス番号 0 に代入されているマッチした全体の文字列が、異なる変数 `MATCH` に格納されています。なお `setopt KSH_ARRAYS` を実行すると zsh でも ksh や bash と同じようにインデックス番号 0 から開始します。

### bash 互換機能

zsh には bash の正規表現機能をエミュレートする機能があります。具体的に言えば、マッチした文字列を代入する変数が `BASH_REMATCH` に変わり最初の要素にマッチした全体が代入されるようになります。つまり `setopt BASH_REMATCH KSH_ARRAYS` を実行するだけで、bash と同じコードが使えるということです。bash と同じようにインデックス番号を 0 から始める必要があるため `KSH_ARRAYS` も有効にする必要があることに注意してください。

```sh
setopt BASH_REMATCH KSH_ARRAYS
if [[ "ab123cd456" =~ ([0-9]+)[^0-9]+([0-9]+) ]]; then
    echo "${BASH_REMATCH[0]}" # => 123cd456
    echo "${BASH_REMATCH[1]}" # => 123
    echo "${BASH_REMATCH[2]}" # => 456
fi
```

### Perl 正規表現の対応

`setopt REMATCH_PCRE` を実行することで、Perl の正規表現が使えるようになります。どこまで互換性があるかは調べていません。また [zsh/pcre](https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-zsh_002fpcre-Module) モジュールを使うことで `pcre_compile`, `pcre_study`, `pcre_match` ビルトインコマンドが使えるようになりますが、使ったことがなくあまり興味がないので省略します。詳細はドキュメントを参照してください。

## bash, ksh, yash, zsh の正規表現の非互換性

それぞれのシェルの間で `[[ ]]` の中で正規表現を書いた時に非互換性があります。以下はその例です。

**正規表現にスペースが含まれる場合は `\` でエスケープすべし**

```sh
bash -c '[[ "a b" =~ ^a[ ]b$ ]]; echo $?' # => 文法エラー
ksh  -c '[[ "a b" =~ ^a[ ]b$ ]]; echo $?' # => 0
yash -c '[[ "a b" =~ ^a[ ]b$ ]]; echo $?' # => 文法エラー 
zsh  -c '[[ "a b" =~ ^a[ ]b$ ]]; echo $?' # => 文法エラー

bash -c '[[ "a b" =~ ^a\ b$ ]]; echo $?' # => 0
ksh  -c '[[ "a b" =~ ^a\ b$ ]]; echo $?' # => 0
yash -c '[[ "a b" =~ ^a\ b$ ]]; echo $?' # => 0
zsh  -c '[[ "a b" =~ ^a\ b$ ]]; echo $?' # => 0
```

**正規表現のメタ文字は `[]` の中に書くべし**

```sh
bash -c '[[ "a*" =~ ^a[*]$ ]]; echo $?' # => 0
ksh  -c '[[ "a*" =~ ^a[*]$ ]]; echo $?' # => 0
yash -c '[[ "a*" =~ ^a[*]$ ]]; echo $?' # => 0
zsh  -c '[[ "a*" =~ ^a[*]$ ]]; echo $?' # => 0

bash -c '[[ "a*" =~ ^a\*$ ]]; echo $?' # => 0
ksh  -c '[[ "a*" =~ ^a\*$ ]]; echo $?' # => 0
yash -c '[[ "a*" =~ ^a\*$ ]]; echo $?' # => 0
zsh  -c '[[ "a*" =~ ^a\*$ ]]; echo $?' # => 1

bash -c '[[ "a*" =~ ^a\\*$ ]]; echo $?' # => 1
ksh  -c '[[ "a*" =~ ^a\\*$ ]]; echo $?' # => 1
yash -c '[[ "a*" =~ ^a\\*$ ]]; echo $?' # => 1
zsh  -c '[[ "a*" =~ ^a\\*$ ]]; echo $?' # => 0
```

**`[` にマッチさせる時は `[ ]` の中で `\[` にエスケープし、`]` にマッチさせる時は `[ ]` の中でエスケープしない**

```sh
bash -c '[[ "[[a]]" =~ ^[[][[]a[]][]]$ ]]; echo $?' # => 0
ksh  -c '[[ "[[a]]" =~ ^[[][[]a[]][]]$ ]]; echo $?' # => 文法エラー
yash -c '[[ "[[a]]" =~ ^[[][[]a[]][]]$ ]]; echo $?' # => 0
zsh  -c '[[ "[[a]]" =~ ^[[][[]a[]][]]$ ]]; echo $?' # => 0

bash -c '[[ "[[a]]" =~ ^[\[][\[]a[]][]]$ ]]; echo $?' # => 0
ksh  -c '[[ "[[a]]" =~ ^[\[][\[]a[]][]]$ ]]; echo $?' # => 0
yash -c '[[ "[[a]]" =~ ^[\[][\[]a[]][]]$ ]]; echo $?' # => 0
zsh  -c '[[ "[[a]]" =~ ^[\[][\[]a[]][]]$ ]]; echo $?' # => 0

bash -c '[[ "[[a]]" =~ ^[[][[]a[\]][\]]$ ]]; echo $?' # => 0
ksh  -c '[[ "[[a]]" =~ ^[[][[]a[\]][\]]$ ]]; echo $?' # => 文法エラー
zsh  -c '[[ "[[a]]" =~ ^[[][[]a[\]][\]]$ ]]; echo $?' # => 0
```

このように、`[[ ]]` の中に特殊な文字が含まれる場合、どのように書くのが良いかを正しく判断するのは困難です。

### 正規表現は変数に入れて使用せよ❗

各シェルの正規表現の非互換性問題を解決したい場合は**正規表現を変数に入れて使いましょう**。正規表現を変数に入れて取り扱うことでコードはわかりやすくなり、これらの非互換性も解消されます。

**全てのシェルで同じ動きになる**

```sh
re='^a b$';           [[ "a b" =~ $re ]];   echo $? # => 0
re='^a[*]$';          [[ "a*" =~ $re ]];    echo $? # => 0
re='^a\*$';           [[ "a*" =~ $re ]];    echo $? # => 0
re='^[[][[]a[]][]]$'; [[ "[[a]]" =~ $re ]]; echo $? # => 0
re='^\[\[a\]\]$';     [[ "[[a]]" =~ $re ]]; echo $? # => 0
```

正規表現を変数に入れて使うという方法は、実は bash 3.1 でも動作するため、昔から推奨されている方法です（参照 [BashGuide/Patterns - Greg's Wiki - Regular Expressions](http://mywiki.wooledge.org/BashGuide/Patterns#Regular_Expressions-1)）。面倒な場合、このような関数を定義すると使いやすくなるでしょう。

```sh
match() { [[ $1 =~ $2 ]]; } # $2 はダブルクォートしてはいけない

if match "a b" '^a b$'; then # 両方とも変数はクォートする必要がある
    ...
fi
```

しかし、`[[ ]]` で変数のクォートが不要になったはずなのに、わかりやすくするためにはクォートが必要だというのは皮肉なものです。

この延長で各シェルの正規表現の違いである、マッチした部分を代入する変数名の違いを吸収するコードがあったので紹介します。ただ元のコードに少し無駄があるなと少し手を加えています。

https://stackoverflow.com/questions/22537804/retrieve-a-word-after-a-regular-expression-in-shell-script/22539067#22539067

```sh
reMatch() {
  unset -v reMatch
  [[ $1 =~ $2 ]] || return $?
  [[ -n $BASH_VERSION ]] && reMatch=( "${BASH_REMATCH[@]}" )
  [[ -n $KSH_VERSION ]]  && reMatch=( "${.sh.match[@]}" )
  [[ -n $ZSH_VERSION ]]  && reMatch=( "$MATCH" "${match[@]}" )
  return 0
}
```

個人的には上記のコードのこの仕様はあまり気に入っていません。またもっと良いシェルスクリプト用の正規表現ライブラリが書けそうだなと思っています。そのうち書くかもしれません。

## さいごに

シェルスクリプトで正規表現の機能は殆ど使ってない（普段はシェルパターンを使っています）ので、なにか漏れがあるかもしれません。もしシェルの正規表現関連でこれが抜けてるというのがありましたら是非コメントください。m(_ _)m