【シェルスクリプト】IFSで区切り文字(デリミタ)を変更する方法

環境変数IFSについて

環境変数「IFS」(Internal Filed Separator)には、bashの場合「スペース」「タブ」「改行」($' \t\n')といった値が初期設定されていて、これらが文字の区切りとして認識されています。

ファイル等を読み込んだりする場合に、読み込む文の区切り文字を変更したい場合は、「IFS」に区切り文字としたい値を設定することで、区切りとさせる文字を好きに設定することが出来ます。

例えばcsvファイルを読み込む場合等は「IFS」に「,」を設定するといったようにです。

区切り文字を設定

区切り文字の設定例として、「/etc/passwd」ファイルから「ユーザー名」と「ログインシェル」の情報を抜き出して表示させてみます。

この場合、「/etc/passwd」の各フィールド区切り文字は「:」となっているため、「IFS」を設定せずに下記のようなスクリプトで内容を抜き出そうとしてみます。

#!/bin/bash
head -n 5 /etc/passwd | while read USER PASS USERID GROUPID COMMENT HOMEDIR LOGINSHELL
do
  echo "$USER $LOGINSHELL"
done

実際に実行してみると、初期設定では区切り文字に「:」は設定されていないため、一行まるまる「USER」変数に格納されてしまい、意図した結果が得られないことが確認できます。

$ sh ./IFS_1.sh
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

次に「:」をIFSに設定してみましょう。

最初に現在の「IFS」の設定を「OLDIFS=$IFS」でバックアップしてから「:」をIFSに設定し、「/etc/passwd」ファイルの各フィールドを、それぞれ「NAME」「PASS」「USERID」「GROUPID」「COMMENT」「HOMEDIR」「LOGINSHELL」といった変数に格納して、そこからユーザ名とログインシェルを表示させています。

最後にバックアップしてあった「IFS」の設定を元に戻しています。

#!/bin/bash

OLDIFS=$IFS
IFS=:

head -n 5 /etc/passwd | while read NAME PASS USERID GROUPID COMMENT HOMEDIR LOGINSHELL
do
  echo "$NAME $LOGINSHELL"
done

IFS=$OLDIFS

今回は「:」が区切り文字として設定されているため、「:」で区切られた各フィールドがそれぞれの変数に格納されて、ユーザー名とログインシェルの部分のみ無事抜き出すことが出来ました。

$ sh ./IFS_2.sh
root /bin/bash
bin /sbin/nologin
daemon /sbin/nologin
adm /sbin/nologin
lp /sbin/nologin

空白・タブを含んだ文を扱いたい場合

空白やタブなどを区切り文字にしたくない場合は、IFSの値を空にすることで「スペース(空白)」「タブ」で区切ることなく読み込むことが出来ます。

例として空白を含んだ下記のファイルを使って挙動を確認してみます。

$ cat list.txt
a b c
d e f
g i j

まずは「IFS」を初期値のままで実行してみます。

#!/bin/bash
for i in $(cat ./list.txt)
do
  echo $i
done

2023.07.24 ファイルを読み込む場合は$(cat ./list.txt)よりも、$(< ./list.txt)の方が効率的であると、コメントにて教えていただきました。

catとリダイレクト(<)どちらの場合も、ファイルの内容を変数iに代入していますが、下記のような違いがあるため、一度だけファイルの内容を読み込む$(< ./list.txt)の方が効率的になります。

  • cat ファイルの内容を読み込むたびにcatを実行する
  • リダイレクト(<) ファイルの内容を一度だけ読み込む

IFSの初期値は「スペース」「タブ」「改行」となっているため、スペースで区切られた文字が1文字ずつ変数iに格納されて、1文字ずつ表示されてしまします

$ sh ./IFS_3.sh
a
b
c
d
e
f
g
i
j

次にIFSの値を空に設定してみます。

#!/bin/bash

OLDIFS=$IFS
IFS=

for i in $(cat ./list.txt)
do
  echo $i
done

IFS=$OLDIFS

区切り文字として「スペース」が設定されていないため、一列ファイルに記述されている内容の全部が変数iに格納されて表示されるようになりました。

$ sh ./IFS_4.sh
a b c
d e f
g i j

IFSに改行を設定

bashで実行する場合

bashで実行する場合は「IFS=$'\n'」と指定することで、改行をIFSに指定することが出来ます。

このスクリプトでは、ファイルの内容が1行ごとに変数「i」に格納されていることがわかるように、変数「i」の内容を表示させる部分を「echo "echo $i"」としています。

#!/bin/bash
 
OLDIFS=$IFS
IFS=$'\n'

for i in $(cat ./list.txt)
do
  echo "echo $i"
done
 
IFS=$OLDIFS

「bash」でスクリプトを実行すると、意図したとおりに1行ごとに変数に格納されて、表示されていることがわかります。

$ bash IFS_5.sh 
echo a b c
echo d e f
echo g i j

ちなみにUbuntuなどは「sh」が「dash」へのリンクとなっているので「sh」でスクリプトを実行すると、「IFS=$'\n'」が意図したように反映されません。

$ sh ./IFS_5.sh 
echo a b c
d e f
g i j

bash以外で実行する場合

bash以外でIFSに改行を設定する場合は、下記のように改行を入力して設定します。

IFS='(実際に改行)
'
#!/bin/sh
 
OLDIFS=$IFS
IFS='
'
 
for i in $(cat ./list.txt)
do
  echo "echo $i"
done
 
IFS=$OLDIFS

ファイルの内容が1行ごと変数「i」に格納されて表示されています。

$ sh ./IFS_6.sh 
echo a b c
echo d e f
echo g i j

コメント

  1. けんじ より:

    ファイルを読み込む時は、
    $(cat ./list.txt)
    より、
    $(< ./list.txt)
    の方が効率的です。

    • tamohiko より:

      けんじさん

      ファイルを読み込む際は、catで標準出力に表示させるよりも<で標準入力にリダイレクトさせるほうが効率的なのですね。 勉強になりました、ありがとうございます。

  2. ぱぱおやじ より:

    IFS_4.sh ですが IFS を空ではなく IFS=$’\n’ にしないとiにlist.txtの中身が全て入って見た目は同じでも意図と違う動作をしていませんか?

    • tamohiko より:

      ぱぱおやじさん

      1列づつ変数iに読み込むのであれば、IFS=$’\n’というように改行を指定しなくてはいけませんね。

      ご指摘ありがとうございます。

タイトルとURLをコピーしました