Bash 環境変数 IFS（区切り文字）の設定を変更する方法

# はじめに
Bash の環境変数 IFS（Internal Field Separator）の設定を変更することで、**単語分割の区切り文字**を**任意の文字**に指定することができます。シンプルな例を挙げて動作確認をしていきたいと思います。(実行環境は、WSL: Ubuntu-22.04)

# 1. デフォルトの動作確認と IFS の変更
Bash のマニュアルには以下のように記述されています。（`$ man bash` で確認できます）
> IFS
内部フィールド区切り文字 (Internal Field Separator) です。展開を行った後に単語を分割する場合や、組み込みコマンドの read を使ったときに行を単語に分割する場合に使われます。 デフォルト値は “<空白><タブ><改行>” です。

bash や zsh では、制御文字の入力に `$''` を使うことができるので、IFS のデフォルト値は下記のように記述できます。
`IFS=$' \t\n'`
IFS の上記三つのデフォルト値で単語分割されるか `for ... in 文`でテストしてみます。

``` bash:test.sh （IFS デフォルト値: "word1<空白>word2<タブ>word3<改行>word4”で確認）
#!/bin/bash
for str in $(printf 'word1 word2\tword3\nword4'); do
 echo "[$str]"
done
```
`echo "[$str]"` で、`[]`で囲んでいるのは、実行結果の空白や改行などをわかりやすくするためです。`[]` で単語分割のひと塊です。
以下のように想定通りの結果になります。
```:実行結果
$ ./test.sh
[word1]
[word2]
[word3]
[word4]
```
では、区切り文字を`,` と `:` に変更してみます。
```bash:test.sh （"word1<カンマ,>word2<コロン:>word3”で確認）
#!/bin/bash
IFS=",:"                # $'' でなくてもよい
for str in $(printf 'word1,word2:word3'); do
 echo "[$str]"
done
```
```:実行結果
$ ./test.sh
[word1]
[word2]
[word3]
```
想定通りです。続いて、ファイルを読み込んでテストしてみます。
区切り文字は、`,` と `\n` に変更します。
```bash:test.txt
$ cat test.txt 
w1,w2,w3
w4,w5,w6
w7,w8,w9

```
```bash:test.sh
#!/bin/bash
IFS=$',\n'
for str in $(< ./test.txt); do   # $(< file) の方が $(cat file) より高速
 echo "[$str]"
done
```
```:実行結果
$ ./test.sh
[w1]
[w2]
[w3]
[w4]
[w5]
[w6]
[w7]
[w8]
[w9]
```
区切り文字を `\n` のみにした場合は、下記のようになります。
```bash:test.sh
#!/bin/bash
IFS=$'\n'
for str in $(< ./test.txt); do
 echo "[$str]"
done
```
```:実行結果
$ ./test.sh
[w1,w2,w3]
[w4,w5,w6]
[w7,w8,w9]
```
区切り文字を空にしてみます。
```bash:test.sh
#!/bin/bash
IFS=          # 区切り文字削除
for str in $(< ./test.txt); do
 echo "[$str]"
done
```
```:実行結果
$ ./test.sh
[w1,w2,w3
w4,w5,w6
w7,w8,w9]
```
区切り文字を改行のみにした場合は、一行ずつ、分割され、区切り文字を空にした場合は、単語分割が行われずにひと塊になっているのがわかるかと思います。

:::note warn
注意
IFS の設定を変更する際、コマンド置換を使って変数に代入すると、末尾の改行が削除されます。以下 Bash マニュアル参照。
> コマンド置換
bash は command を実行し、 command の標準出力でコマンド置換の部分を置き換えます。  この際、末尾の改行文字は削除されます。  文字列の途中にある改行文字は削除されませんが、単語分割の際に削除されることがあります。 コマンド置換 `$(cat file)` は、同じ意味を持ち、 しかも高速な `$(< file)` に置き換え可能です。
<details><summary>コードで確認</summary>

```bash:test.sh
#!/bin/bash
IFS=$(printf ' \n')    # 区切り文字は空白と改行
for str in $(printf 'word1 word2\nword3'); do
 echo "[$str]"
done
```

```:実行結果
$ ./test.sh
[word1]
[word2
word3]
```

末尾の改行が削除されているので、改行では単語分割が行われていません。
:::
</details>

# 2. 前方一致する変数名や配列の表示
Bash のマニュアルから引用です。
> パラメータの展開 
`${!prefix*}`
`${!prefix@}`
前方一致する変数名。 prefix で始まる全ての変数の名前に展開して、 IFS 特殊変数の最初の文字によって区切ります。 ダブルクォートの中で @  が使われた場合、それぞれの変数の名前は 別々の単語に展開されます。

では試してみます。
```bash:test.sh
#!/bin/bash
color_blue='blue'
color_yellow='yellow'
color_red='red'
IFS=","
echo "${!color*}"    # ""ダブルクォートで囲み、@ ではなく * を使う
```
```bash:実行結果
$ ./test.sh
color_blue,color_red,color_yellow
```
区切り文字をデフォルト値に戻したい場合は、あらかじめ変数にバックアップしておきます。
```bash:test.sh
#!/bin/bash
color_blue='blue'
color_yellow='yellow'
color_red='red'
IFS_ORG="$IFS"        # バックアップ
IFS=","
echo "${!color*}"
IFS="$IFS_ORG"        # デフォルト値に戻す
echo "${!color*}"
```
```:実行結果
$ ./test.sh
color_blue,color_red,color_yellow
color_blue color_red color_yellow
```
一行目がカンマ、二行目が IFS デフォルト値の最初の文字である空白で区切られています。
配列も似たような感じです。以下は、Bash のマニュアルからの引用です。
> 配列
単語がダブルクォートされていれば、${name[*]} は 1 つの単語に展開されます。この単語は、配列の各メンバの値を特殊変数 IFS の最初の値で区切って並べたものです。
```bash:test.sh
#!/bin/bash
color=(blue yellow red)
IFS=","
echo "${color[*]}"    # ""ダブルクォートで囲み、@ ではなく * を使う
```
```:実行結果
$ ./test.sh
blue,yellow,red
```
# 3. while read 文での動作確認
`while read 文`の場合、読み込む行の前後に空白やタブがあると削除されてしまいます。
以下の例で確認してみます。
`<タブ>line1<空白><改行>`
`word1<タブ><空白>word2<改行>`
`<タブ><空白>line3<タブ><改行>`
```bash:test.sh
#!/bin/bash
while read line; do
  echo "[$line]"
done < <(printf '\tline1  \nword1\t  word2\n\t  line3\t\n')
```
```:実行結果
$ ./test.sh
[line1]
[word1    word2]
[line3]
```
これは IFS のデフォルト値に空白やタブがあるからで、IFS を空にすると、行の前後にある空白やタブを保持してくれます。
（ちなみに行の中の空白やタブは削除されずにそのまま保持されています）
```bash:test.sh
#!/bin/bash
while IFS= read line; do
  echo "[$line]"
done < <(printf '\tline1  \nword1\t  word2\n\t  line3\t\n')
```
```:実行結果
$ ./test.sh
[       line1  ]
[word1    word2]
[         line3 ]
```
:::note
補足１
空白とタブの区別をつけたい場合は、
`echo "[$line]"` の代わりに `printf "%q\n" "[$line]"` と記述すると、わかりやすく確認できるようになります。
<details><summary>コードで確認</summary>

```bash:test.sh
#!/bin/bash
while IFS= read line; do
  printf "%q\n" "[$line]"
done < <(printf '\tline1  \nword1\t  word2\n\t  line3\t\n')
```
```:実行結果
$ ./test.sh
$'[\tline1  ]'
$'[word1\t  word2]'
$'[\t  line3\t]'
```
:::
</details>

:::note
補足２
`while IFS= read ～` と記述すると、IFS の値は、シェルスクリプト全体ではなく、read コマンドを実行するときだけ限定的にセットされる値になります。こちらも覚えておくとよいと思います。
<details><summary>コードで確認</summary>

```bash:test.sh
#!/bin/bash
while IFS= read line; do
  echo "[$line]"
done < <(printf '  line1  \n  line2  \n')
printf "%q\n" "$IFS"       # デフォルト値に戻る
```
```:実行結果
$ ./test.sh
[  line1  ]
[  line2  ]
$' \t\n'
```
:::
</details>

以上です。お疲れ様でした。