文字列からバージョン情報だけを抜き出す正規表現（bash/dash/ash/zsh で grep）

> 文字列「`v2.1.0-39-g4e1ede1`」から "`2.1.0`" もしくは "`v2.1.0`" <ruby>のみ<rt>・・</rt></ruby>を抜き出したい。
>
> 可能ならメジャー、マイナー、パッチのバージョンも個別に。プレリリースやビルドはいらない。そして `bash` `zsh` などでも使えるように POSIX 互換で。
>
> ... ... って `grep` だっけ？ `sed` だっけ？もしかして `awk`？

確かに [セマンティック・バージョニング 2.0.0](https://semver.org/lang/ja/) に完全に準拠している構文であれば、公式の正規表現を利用できます。

> ```bash
> ^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
> ```
> ```bash
> ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
> ```
> <sub><sup>（[SemVer文字列をチェックするために推奨される正規表現（RegEx）はありますか？](https://semver.org/lang/ja/#semver%E6%96%87%E5%AD%97%E5%88%97%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E6%8E%A8%E5%A5%A8%E3%81%95%E3%82%8C%E3%82%8B%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BEregex%E3%81%AF%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%81%8B) | FAQ @ semver.org より）</sup></sub>
> <sub><sup>[オンラインで動作を見る](https://paiza.io/projects/_qUqajn7p4WOorQEe5GalQ) @ paiza.IO</sup></sub>

しかし、これは「構文チェック用の正規表現」であるため、準拠していない `1.1` などはエラーになります。パッチのバージョンが抜けているからです。

つまり、バージョンを抜き出すシンプルな用途には向いていません。普通に文字列から `X.Y.Z` もしくは `vX.Y.Z` を抜き出したいだけなのに。

意外なことに[「bash 正規表現 バージョン情報 抜き出す」でググって](https://www.google.com/search?q=bash+%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE+%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E6%83%85%E5%A0%B1+%E6%8A%9C%E3%81%8D%E5%87%BA%E3%81%99)みても、**文字列からバージョン番号を抜き出す<ruby>だけ<rt>・・</rt></ruby>の記事がなかった**ので自分のググラビリティとして。

## TL; DR <sub><sup>（今北産業）</sup></sub>

> `grep` の `-o` オプションで正規表現にマッチした文字のみ抜き出し、`-E` オプションで細かい指定をし、`| head` で最初にマッチしたもののみに絞る。

```bash
grep -o -E "([0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1
```

```shell-session:例（vなし）
$ echo "v2.1.0-39-g4e1ede1" | grep -o -E "([0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1
2.1.0
```
```shell-session:例（vつき）
$ echo "v2.1.0-39-engine-1.2.3" | grep -o -E "(v[0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1
v2.1.0
```
```shell-session:応用（メジャー、マイナー、パッチバージョンを抜き出す）
$ MyVersion=$(echo "v2.1.0-39-engine-1.2.3" | grep -o -E "([0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1)
$ echo $MyVersion | awk -F. '{printf "Major:%d, Minor:%d, Patch:%d\n", $1,$2,$3}'
Major:2, Minor:1, Patch:0
```

- 動作確認OS: macOS Catalina 10.15.7, Alpine Linux v3.12, Ubuntu 20.04.1 LTS
- [オンラインで動作をみる](https://paiza.io/projects/OoK93Nk9Xf0wD75tph4QLA) @ paiza.IO

## TS; DR

```bash:sample.sh（色々なパターンをテスト）
#!/bin/bash

test() {
    input="$1"
    expect="$2"

    actual=$(echo "${input}" | grep -o -E "([0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1)

    [ "$expect" = "$actual" ] && {
        printf '%s' '[OK] '
    } || {
        printf '%s' '[ng] '
    }

    printf "Input: %s\t-> Output: %s\n" "$input" "$actual"
}

#    Input        Expect
test "2"          ""
test ".2"         ""
test "..2"        ""
test "2.1"        "2.1"
test "2..1"       ""
test "version2"   ""
test "version2.1" "2.1"
test "22.11"      "22.11"
test "2.1.0"      "2.1.0"
test "2.1.10"     "2.1.10"
test "2.10.0"     "2.10.0"
test "22.1.0"     "22.1.0"
test "33.22.11"   "33.22.11"

test "v1.2-3.4.5.6.7"     "1.2"
test "v1.2.3.4.5.6.7.8"   "1.2.3"
test "v1.2.3-4.5.6-7"     "1.2.3"
test "2.1.0-39-g4e1ede"   "2.1.0"
test "v2.1.0-39-g4e1ede1" "2.1.0"

grep --version | head -n1

```
```shell-session:テスト結果
$ ./sample.sh
[OK] Input: 2    -> Output: 
[OK] Input: .2  -> Output: 
[OK] Input: ..2 -> Output: 
[OK] Input: 2.1 -> Output: 2.1
[OK] Input: 2..1        -> Output: 
[OK] Input: version2    -> Output: 
[OK] Input: version2.1  -> Output: 2.1
[OK] Input: 22.11       -> Output: 22.11
[OK] Input: 2.1.0       -> Output: 2.1.0
[OK] Input: 2.1.10      -> Output: 2.1.10
[OK] Input: 2.10.0      -> Output: 2.10.0
[OK] Input: 22.1.0      -> Output: 22.1.0
[OK] Input: 33.22.11    -> Output: 33.22.11
[OK] Input: v1.2-3.4.5.6.7      -> Output: 1.2
[OK] Input: v1.2.3.4.5.6.7.8    -> Output: 1.2.3
[OK] Input: v1.2.3-4.5.6-7      -> Output: 1.2.3
[OK] Input: 2.1.0-39-g4e1ede    -> Output: 2.1.0
[OK] Input: v2.1.0-39-g4e1ede1  -> Output: 2.1.0
grep (GNU grep) 3.4
```

- [オンラインで動作確認する](https://paiza.io/projects/OoK93Nk9Xf0wD75tph4QLA) @ paiza.IO

上記のパターンに加えたいものがあれば遠慮なく[編集リク](https://qiita.com/KEINOS/items/b59badbc668480507707/edit)くださーい。また、検知して欲しいパターンの抜けがあれば、遠慮なくコメントくださーい。

### 応用技 <sub><sup>（メジャー、マイナー、パッチバージョンを抜き出す）</sup></sub>

`xx.yy.zz` の[セマンティック・バージョニング](https://semver.org/lang/ja/)なら `awk` で抜き出せます。

> `awk -F. '{printf "Major:%d, Minor:%d, Patch:%d\n", $1,$2,$3}'`

```shell-session:Golangのメジャー、マイナー、パッチバージョンを抜き出す例
$ go version
go version go1.16.3 linux/amd64

$ GoVersion=$(go version | grep -o -E "([0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1)
$ echo $GoVersion | awk -F. '{printf "Major:%d, Minor:%d, Patch:%d\n", $1,$2,$3}'
Major:1, Minor:16, Patch:3
```


## 参考文献／あわせて読みたい

- [どのUNIXコマンドでも使える正規表現](https://qiita.com/richmikan@github/items/b6fb641e5b2b9af3522e) @ Qiita
   - 「`たった3つの正規表現メタ文字セットだけ知ればいい`」<br>正規表現を抜本的に理解出来る、目から鱗の Qiita 記事です。Docker の Alpine でゴニョゴニョすることが多い人は POSIX 原理主義者になった方が良いと思いました