PythonユーザーのためのBash独自正規表現処理ガイド

# はじめに

Bashの`[[ ... =~ ... ]]`構文を使用した正規表現は、Pythonユーザーにとっては意外と知られていない便利なツールです。この記事では、Pythonの正規表現に慣れたユーザー向けに、Bash独自の正規表現処理の使い方を解説します。

## Bash正規表現の基本構文

Bashでは、`[[ ... =~ ... ]]`構文を使うことで、正規表現を用いた文字列のパターンマッチングが可能です。この構文を用いると、シェルスクリプト内で複雑な条件分岐をシンプルに記述できます。

:::note alert
**注意点**

1. **`[[ ... ]]`** **構文のみで利用可能**
   正規表現は、古い `[ ... ]` 構文では動作しません。

2. **正規表現部分のクォートに注意**
   クォートすると文字列として扱われるため、意図した動作になりません。

3. **外部コマンドとの使い分け**
   複雑な正規表現や高度なマッチングが必要な場合は、`python`や、`grep`や`awk`などを併用することが推奨されます。

:::


### 基本的な例

```bash: check_abc.sh
#!/bin/bash
input="123abc"
if [[ $input =~ ^[0-9]+abc$ ]]; then
  echo "Pattern matched!"
else
  echo "No match"
fi
```

- **`^`**: 文字列の先頭を示します。
- **`[0-9]+`**: 1つ以上の数字。
- **`$`**: 文字列の末尾を示します。

実行出力例:

``` bash
% chmod +x check_abs.sh
% ./check_abs.sh 
Pattern matched! 123abc
```

:::note info
`BASH_REMATCH` は、Bash の正規表現マッチング構文 `[[ ... =~ ... ]]` を使った場合に、自動的に設定される特殊な配列変数です。この変数は、正規表現によってマッチした文字列やキャプチャグループを格納します。詳細は後述します。
:::

### Pythonとの類似点

Bashの正規表現は、Pythonの正規表現モジュール（`re`）に似た書き方ができますが、一部制限があります（後述）。

---

## Bash正規表現の特徴

### 1. クォートは不要

`[[ ... =~ ... ]]`内の正規表現部分は**クォートしない**必要があります。クォートすると、文字列として解釈されてしまい、正規表現が機能しません。

```bash
# 正しい例
if [[ "123abc" =~ ^[0-9]+abc$ ]]; then
  echo "Matched"
fi

# 誤った例
if [[ "123abc" =~ "^[0-9]+abc$" ]]; then
  echo "Matched" # 動作しない
fi
```

### 2. キャプチャ結果の利用

マッチした部分文字列やキャプチャグループは、特殊変数`BASH_REMATCH`に格納されます。

```bash: check_pixel.sh
input="PIXEL=0:11,13:35"
if [[ $input =~ ^PIXEL=([0-9,:]+)$ ]]; then
  echo "Complete match: ${BASH_REMATCH[0]}"  # 全体の一致
  echo "Captured group: ${BASH_REMATCH[1]}" # 数値と記号部分
else
  echo "No match"
fi
```

- **`BASH_REMATCH[0]`**: 完全一致した文字列。
- **`BASH_REMATCH[1]`**: 最初のキャプチャグループに一致した部分。

出力:

``` m:
% chmod +x ./check_pixel.sh
% ./check_pixel.sh 
Complete match: PIXEL=0:11,13:35
Captured group: 0:11,13:35
```

<details><summary> 【詳細解説】正規表現 ^PIXEL=([0-9,:]+)$の意味</summary>

正規表現 `^PIXEL=([0-9,:]+)$` の各部分を詳しく分解し、その意味を解説します。

---

### 全体の構成
この正規表現は、文字列が以下の条件を満たすかどうかをチェックします：
- 文字列の先頭に `PIXEL=` がある。
- `PIXEL=` の後に続く部分が、数字（`0-9`）、カンマ（`,`）、およびコロン（`:`）で構成されている。
- 文字列全体がこの形式に一致する。

---

### 正規表現の各部分の解説

#### 1. **`^`**
- **意味**: 文字列の先頭を表します。
- **目的**: `PIXEL=` が文字列の先頭にあることを確認します。
- **例**:
  - `PIXEL=0:11,13:35` → マッチする。
  - `data PIXEL=0:11,13:35` → マッチしない（先頭が `PIXEL=` ではないため）。

---

#### 2. **`PIXEL=`**
- **意味**: リテラル文字列として解釈されます。
- **目的**: 文字列が `PIXEL=` で始まっていることを確認します。
- **例**:
  - `PIXEL=0:11` → マッチする。
  - `VALUE=0:11` → マッチしない。

---

#### 3. **`([0-9,:]+)`**
- **意味**: キャプチャグループを定義します。
  - 括弧 `()` の中の部分にマッチした文字列は、Bash の場合 `BASH_REMATCH[1]` に格納されます。
- **具体的な構成**:
  - **`[0-9,:]`**:
    - 文字クラスを定義します。
    - この文字クラスは、以下の文字のいずれか1文字にマッチします：
      - `0-9`: 数字
      - `,`: カンマ
      - `:`: コロン
  - **`+`**:
    - 直前の文字クラス `[0-9,:]` が「1回以上繰り返される」部分にマッチします。

- **例**:
  - `PIXEL=0` → マッチ（キャプチャされるのは `0`）。
  - `PIXEL=0:11` → マッチ（キャプチャされるのは `0:11`）。
  - `PIXEL=0:11,13:35` → マッチ（キャプチャされるのは `0:11,13:35`）。
  - `PIXEL=abc` → マッチしない（`abc` は文字クラスに含まれないため）。

---

#### 4. **`$`**
- **意味**: 文字列の末尾を表します。
- **目的**: 文字列が、`PIXEL=` から始まり、その後ろの文字列が完全に `([0-9,:]+)` に一致することを確認します。
- **例**:
  - `PIXEL=0:11` → マッチ（文字列全体が正規表現に一致）。
  - `PIXEL=0:11,13:35 extra` → マッチしない（末尾に余分な文字列 `extra` があるため）。

---

### 正規表現の動作例

#### 入力文字列とマッチ結果
| 入力文字列                | マッチ結果       | キャプチャされた部分 (`BASH_REMATCH[1]`) |
|---------------------------|------------------|------------------------------------------|
| `PIXEL=0`                | マッチする       | `0`                                      |
| `PIXEL=0:11`             | マッチする       | `0:11`                                   |
| `PIXEL=0:11,13:35`       | マッチする       | `0:11,13:35`                             |
| `PIXEL=abc`              | マッチしない    | -                                        |
| `data PIXEL=0:11`        | マッチしない    | -                                        |
| `PIXEL=0:11,13:35 extra` | マッチしない    | -                                        |

### 応用例
#### ファイル名に基づく条件分岐
以下のような `PIXEL` 情報を含むファイル名を処理するスクリプトに活用できます。

```bash
filename="output_PIXEL=0:11,13:35.log"
if [[ $filename =~ PIXEL=([0-9,:]+) ]]; then
  echo "PIXEL range: ${BASH_REMATCH[1]}"
fi
```

#### 出力:
```
PIXEL range: 0:11,13:35
```

---

### まとめ
この正規表現 `^PIXEL=([0-9,:]+)$` は、特定のフォーマット（`PIXEL=` で始まり、数字やカンマ、コロンが含まれる文字列）に一致するかどうかをチェックするために有用です。また、キャプチャグループを使用することで、数値範囲の情報を簡単に抽出できます。

</details>


### 3. 正規表現の書き方

Bashの正規表現は、POSIX拡張正規表現（ERE: Extended Regular Expression）に近い構文を採用しています。

利用可能な要素:

- `.`: 任意の1文字
- `[ ]`: 文字クラス
- `^` / `$`: 文字列の先頭/末尾
- `*` / `+`: 繰り返し（0回以上/1回以上）
- `( )`: グループ化（キャプチャ可能）
- `|`: OR条件

---

## Pythonとの違い

### 1. 高度な機能は非対応

Bashの正規表現は、Pythonの`re`モジュールほど強力ではありません。

| 機能          | Bash | Python |
| ----------- | ---- | ------ |
| 非貪欲マッチ | ❌    | ✅      |
| 先読み/後読み     | ❌    | ✅      |
| ネストしたグループ   | ✅    | ✅      |

<details><summary>【詳細解説】非貪欲マッチ・貪欲マッチとは何か？</summary>

### 非貪欲マッチ（Non-greedy Match）と貪欲マッチ（Greedy Match）の基礎解説

正規表現を扱う際、「貪欲マッチ」と「非貪欲マッチ」という概念は非常に重要です。これらの違いを理解することで、正規表現を効率的に使いこなすことができます。

---

## 1. 貪欲マッチ（Greedy Match）とは？

**貪欲マッチ**とは、正規表現において可能な限り多くの文字列にマッチさせようとする動作のことです。特に、繰り返しを表す演算子（`*`、`+`、`{}`）を使用した場合、マッチングが最大限になるように進みます。

### 例: 貪欲マッチ
```python
import re

# 正規表現
pattern = r"<.*>"
text = "<tag1>content<tag2>"

# 貪欲マッチ
result = re.search(pattern, text)
print(result.group())  # 出力: <tag1>content<tag2>
```

#### 解説:
- **`.*`** は任意の文字が0回以上繰り返される部分にマッチします。
- 貪欲マッチでは `<` と `>` の間に含まれる文字すべてがマッチ対象となるため、最初の `<` から最後の `>` までマッチします。

---

## 2. 非貪欲マッチ（Non-greedy Match）とは？

**非貪欲マッチ**とは、正規表現において可能な限り少ない文字列にマッチさせようとする動作のことです。これを実現するには、繰り返し演算子の後ろに `?` を付けます（例: `*?`、`+?`、`{m,n}?`）。

### 例: 非貪欲マッチ
```python
import re

# 正規表現
pattern = r"<.*?>"
text = "<tag1>content<tag2>"

# 非貪欲マッチ
result = re.search(pattern, text)
print(result.group())  # 出力: <tag1>
```

#### 解説:
- **`.*?`** は任意の文字が0回以上繰り返される部分に、必要最小限だけマッチします。
- この場合、最初の `<` から最初に見つかる `>` までがマッチします。

---

## 3. 貪欲マッチと非貪欲マッチの比較

| 項目                 | 貪欲マッチ（Greedy）           | 非貪欲マッチ（Non-greedy）      |
|----------------------|-----------------------------|-----------------------------|
| 動作                 | 可能な限り多くの文字列にマッチ | 必要最小限の文字列にマッチ     |
| 演算子               | `*`, `+`, `{}`             | `*?`, `+?`, `{m,n}?`        |
| 使用例               | `<.*>`                     | `<.*?>`                     |
| マッチの結果         | `<tag1>content<tag2>`       | `<tag1>`                    |

---

## 4. Lazy Match（怠惰マッチ）とは？

**Lazy Match** は **Non-greedy Match** の別名で、「必要最小限のマッチングを行う」動作を指します。多くの正規表現エンジンやツールで、`lazy` という言葉が使われることがあります。例えば、Python や JavaScript の正規表現ドキュメントでは、`*?` のような演算子を「lazy」として説明しています。

Lazy Match と Non-greedy Match は同じ概念であり、どちらの用語が使われても同じ動作を指します。

---

## 5. 実践例: HTMLタグの抽出

### 問題: HTMLタグごとに抽出したい場合
以下のような文字列があるとします：
```html
<tag1>content1</tag1><tag2>content2</tag2>
```

#### 貪欲マッチの場合
```python
pattern = r"<.*>"
text = "<tag1>content1</tag1><tag2>content2</tag2>"

result = re.search(pattern, text)
print(result.group())  # 出力: <tag1>content1</tag1><tag2>content2</tag2>
```

#### 非貪欲マッチの場合
```python
pattern = r"<.*?>"
text = "<tag1>content1</tag1><tag2>content2</tag2>"

results = re.findall(pattern, text)
print(results)  # 出力: ['<tag1>', '</tag1>', '<tag2>', '</tag2>']
```

#### 解説:
- 貪欲マッチではすべてのタグを1つのマッチとして扱います。
- 非貪欲マッチではタグ単位で最小限に分割されます。

---

## 6. どちらを使うべきか？

### 貪欲マッチを使うべき場合
- 文字列全体をまとめてマッチさせたいとき。
- マッチングの範囲を制御する他の方法がある場合。

### 非貪欲マッチを使うべき場合
- 必要最小限の文字列をマッチさせたいとき。
- 部分的なマッチが求められる場合（例: HTMLタグの抽出）。

---

## まとめ

- **貪欲マッチ（Greedy Match）** は可能な限り多くの文字列にマッチする動作。
- **非貪欲マッチ（Non-greedy Match）** は必要最小限の文字列にマッチする動作。
- **Lazy Match** は Non-greedy Match の別名。
- 使用する場面によってこれらを使い分けることで、正規表現をより効率的に活用できます。

貪欲マッチと非貪欲マッチの動作を理解した上で、必要に応じて自分のアプリケーションでは `python` がよいか、quick な `bash` を用いた解析がよいか、判断しましょう。

</details>



### 2. サポートされない例

以下のようなPythonの高度な正規表現はBashではサポートされていません。

#### Python例（先読み）:

```python
import re
pattern = r'foo(?=bar)'
print(re.search(pattern, 'foobar')) 
#<re.Match object; span=(0, 3), match='foo'>
```

#### Bashでの代替方法

先読みや後読みが必要な場合は、文字列操作や外部コマンド（`grep`や`awk`）で実現する必要があります。

---

## 実践例

以下は、Bashの正規表現を使って引数の形式を判別する例です。

```bash:./check_pixel.sh
#!/bin/bash

# 引数を取得
pixel_arg=$1

# 数値のみの場合
if [[ "$pixel_arg" =~ ^[0-9]+$ ]]; then
  echo "Single integer: $pixel_arg"

# 範囲や複数指定の場合
elif [[ "$pixel_arg" =~ ^[0-9,:]+$ ]]; then
  echo "Range or multiple integers: $pixel_arg"

# 無効な形式の場合
else
  echo "Invalid PIXEL argument: $pixel_arg"
  exit 1
fi
```

#### 実行例

```bash
$ ./check_pixel.sh 12
Single integer: 12

$ ./check_pixel.sh 0:11,13:35
Range or multiple integers: 0:11,13:35

$ ./check_pixel.sh abc
Invalid PIXEL argument: abc
```

## ファイル操作への応用

正規表現を使うことで、複数のファイルを効率的に処理できます。

### ファイル名フィルタリング

```bash:
for file in *.txt; do
  if [[ $file =~ ^data_[0-9]{4}\.txt$ ]]; then
    echo "Processing file: $file"
  fi
done
```

### コードの意味と動作

以下は、Bashスクリプトの `for` ループと正規表現によるファイル名フィルタリングの具体例について解説します。

#### 1. **`for file in *.txt`**
- `*.txt` は、現在のディレクトリにある拡張子が `.txt` のすべてのファイルを対象とします。
- `for` ループにより、これらのファイル名が1つずつ変数 `file` に格納され、処理されます。

---

#### 2. **`if [[ $file =~ ^data_[0-9]{4}\.txt$ ]]`**
この条件式では、`file` が特定のパターンに一致するかどうかをチェックします。

##### 正規表現の分解
- **`^`**: ファイル名の先頭を意味します。
- **`data_`**: ファイル名が必ず `data_` で始まる必要があります。
- **`[0-9]{4}`**: `data_` に続く部分が「4桁の数字」であることを指定します。
- **`\.txt`**: ドット（`.`）をエスケープして、「`.txt`」という拡張子で終わることを指定します。
- **`$`**: ファイル名の末尾を意味します。

#### 3. **`echo "Processing file: $file"`**
条件に一致したファイルを処理対象として表示します。

##### サンプルディレクトリ構造

ディレクトリ内に以下の `.txt` ファイルがあると仮定します：

```
data_0001.txt
data_1234.txt
data_9999.txt
report_2023.txt
summary.txt
data_invalid.txt
```

##### スクリプト実行の動作

#### 1. ファイルリストの展開
`for file in *.txt` の部分で、以下のファイルが順番に変数 `file` に格納されます：
```
data_0001.txt
data_1234.txt
data_9999.txt
report_2023.txt
summary.txt
data_invalid.txt
```

---

#### 2. 正規表現のマッチング
`if [[ $file =~ ^data_[0-9]{4}\.txt$ ]]` の部分で、ファイル名が以下のように判定されます：

| ファイル名            | マッチするか | 理由                                 |
|-----------------------|--------------|--------------------------------------|
| `data_0001.txt`       | ✅ マッチ   | `data_` + 4桁の数字 + `.txt` に一致 |
| `data_1234.txt`       | ✅ マッチ   | 同上                                 |
| `data_9999.txt`       | ✅ マッチ   | 同上                                 |
| `report_2023.txt`     | ❌ マッチしない | ファイル名が `data_` で始まらない    |
| `summary.txt`         | ❌ マッチしない | `data_` も 4桁の数字もない          |
| `data_invalid.txt`    | ❌ マッチしない | `data_` の後が4桁の数字でない        |

#### 3. 結果の出力

条件に一致するファイルのみが `echo` によって表示されます。

```
Processing file: data_0001.txt
Processing file: data_1234.txt
Processing file: data_9999.txt
```

## 実際の活用例

このスクリプトは、特定の形式のファイルのみを処理する場合に便利です。たとえば：

1. **ログファイルの処理**
   - ファイル名が `log_YYYY.txt` の形式になっているログを日付ごとに処理。

2. **データファイルの集計**
   - `data_0001.txt` から `data_9999.txt` までの範囲のファイルを集計。

3. **ファイルの自動変換**
   - 条件に合うファイルのみ別のフォーマットに変換するスクリプト。

#### 改良のアイデア

以下の点を追加すると、さらに柔軟なスクリプトが作れます：

1. **マッチしなかったファイルの出力**
   ```bash
   for file in *.txt; do
     if [[ $file =~ ^data_[0-9]{4}\.txt$ ]]; then
       echo "Processing file: $file"
     else
       echo "Skipping file: $file"
     fi
   done
   ```

2. **別ディレクトリに結果を保存**
   ```bash
   for file in *.txt; do
     if [[ $file =~ ^data_[0-9]{4}\.txt$ ]]; then
       cp "$file" processed_files/
     fi
   done
   ```

## まとめ

Bashの `[[ ... =~ ... ]]` を用いた正規表現は、Pythonユーザーにとっても十分に直感的に使える構文です。ただし、Pythonに比べて機能が制限されているため、制約事項を踏まえて、用途に応じて外部ツールを活用するのが良いでしょう。シェルスクリプトの柔軟性を活かしながら、簡潔かつ強力な文字列操作を実現してみてください。