Word Splitting

Shell 은 변수의 값을 표시할 때 IFS( Internal Field Separator ) 변수에 설정되어있는 값을 이용해 단어를 분리해 표시합니다. 여기서 단어를 분리한다는 의미는 IFS 변수에 설정되어 있는 문자를 space 로 변경하여 표시한다는 것입니다. 변수를 quote 하게 되면 단어 분리가 발생하지 않습니다.

$ AA="11X22X33Y44Y55"

$ echo $AA
11X22X33Y44Y55

$ IFS=XY

# IFS 값인 X, Y 문자가 space 로 변경되어 표시됩니다.
$ echo $AA
11 22 33 44 55

# 변수를 quote 하게 되면 단어 분리가 발생하지 않습니다.
$ echo "$AA"
11X22X33Y44Y55
------------------------------------------------------

$ ( IFS=:; for v in $PATH; do echo "$v"; done )
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
. . . . 
. . . .

IFS 기본값은 공백문자인 space, tab, newline 입니다. IFS 변수가 unset 됐을 때도 동일한 값이 적용되며 IFS 값이 null 이면 단어분리가 일어나지 않습니다. read 명령으로 읽어들인 라인을 필드로 분리할 때, array 변수에 원소들을 분리하여 입력할 때도 IFS 값이 사용됩니다.

$ echo -n "$IFS" | od -a
0000000  sp  ht  nl

# space, tab, newline 이 모두 space 로 변경되어 표시됩니다.
$ echo $( echo -e "11 22\t33\n44" )
11 22 33 44

# quote 을 하면 단어 분리가 발생하지 않습니다.
$ echo "$( echo -e "11 22\t33\n44" )"
11 22   33
44

bash 와 sh 에서의 IFS 값 설정 방법

# bash 의 경우
bash$ IFS=$'\n'              # newline 으로 설정
bash$ IFS=$' \t\n'           # 기본값 으로 설정

# sh 의 경우
sh$ IFS='                    # newline 설정
'
sh$ IFS=$(echo " \n\t")      # 기본값 설정
                             # 여기서 \t 을 \n 뒤로 둔것은 $( ) 을 이용해 변수에 값을 대입할때는
                             # 마지막 newline 들이 제거되기 때문입니다.
                             # 첫번째 문자는 "$*" 값을 표시할때 구분자로 사용되므로 
                             # 위치가 바뀌면 안되겠습니다.

# 다음과 같이 할 수도 있습니다.
# tab 문자 입력은 ctrl-v 한후 tab 키
sh$ tab='    '            
sh$ nl='
'
sh$ IFS=" $tab$nl"
.........................................

# IFS 값을 변경하기 전에 백업하고 복구하기
sh$ oIFS=$IFS                # 기존 IFS 값 백업
sh$ IFS='                    # IFS 을 newline 으로 설정하여 사용
'
...
...
sh$ IFS=$oIFS                # 기존 IFS 값 복구

다음은 단어분리가 일어나는 예입니다.

$ dirname="쉘 스크립트 강좌"

# $dirname 변수에서 단어분리가 일어나 마지막 단어인 '강좌' 가 디렉토리 명이 됩니다.
$ cp *.txt $dirname                     
cp: target '강좌' is not a directory

# $dirname 변수를 quote 하여 정상적으로 실행됨.
$ cp *.txt "$dirname"
OK

--------------------------------------------------------------

$ AA="one two three"

# $AA 하나의 변수값이지만 단어분리에 의해 3개의 인수가 됩니다.
$ args.sh $AA
$1 : one
$2 : two
$3 : three

# $AA 하나의 변수값이지만 단어분리에 의해 ARR 원소 개수가 3개가 됩니다.
$ ARR=( $AA )      
$ echo ${#ARR[@]}
3

# $AA 하나의 변수값이지만 단어분리에 의해 3개의 값이 출력됩니다.
$ for num in $AA; do 
>    echo "$num"
>done
one
two
three

quote 을 하면 단어분리가 일어나지 않습니다.

AA="echo hello world"

# 단어분리가 일어나 echo 는 명령 hello world 는 인수가 됩니다.
$ $AA                 
hello world

# quote 을 하면 단어분리가 일어나지 않으므로 'echo hello world' 전체가 하나의 명령이 됩니다.
$ "$AA"               
echo hello world: command not found

단어분리는 변수확장, 명령치환 과 함께 일어나는 작업

단어분리는 변수확장, 명령치환 과 함께 일어나는 작업으로 다음과 같은 경우는 발생하지 않습니다.

$ set -f; IFS=:
$ ARR=(Arch Linux:Ubuntu Linux:Suse Linux:Fedora Linux)
$ set +f; IFS=$' \t\n'

$ echo ${#ARR[@]}    # 올바르게 분리 되지 않는다.   
5
$ echo ${ARR[1]}    
Linux:Ubuntu

$AA 변수확장에 대해서는 단어분리가 정상적으로 일어난다.

$ AA="Arch Linux:Ubuntu Linux:Suse Linux:Fedora Linux"

$ set -f; IFS=:               
$ ARR=( $AA )
$ set +f; IFS=$' \t\n'

$ echo ${#ARR[@]}    # 올바르게 분리 되었다.
4
$ echo ${ARR[1]}       
Ubuntu Linux

IFS 값을 'Q' 로 변경하였지만 인수가 분리되지 않고 그대로 공백에 의해 분리가 됩니다.

f1() {
    echo \$1 : "$1"
    echo \$2 : "$2"
}

IFS=Q          # IFS 값을 'Q' 로 설정

f1 11Q22
f1 33 44
====== output ========

$1 : 11Q22     # 'Q' 에 의해 인수가 분리되지 않는다.
$2 :
$1 : 33        # 그대로 공백에 의해 인수가 분리된다.
$2 : 44

다음과 같이 변수확장이 일어나야 'Q' 에 의해 인수가 분리됩니다.

IFS=Q     
AA="11Q22"
BB="33 44"
f1 $AA
f1 $BB
====== output ========

$1 : 11        # 'Q' 에 의해 인수가 분리된다.
$2 : 22
$1 : 33 44     # 공백 에서는 분리되지 않는다.
$2 :

IFS 값이 공백문자일 경우와 아닐 경우

다음은 IFS 값이 공백문자일( space, tab, newline ) 경우와 아닐 경우 차이를 비교한 것입니다.
연이어진 공백문자는 하나로 취급되며 IFS 값이 공백문자가 아닐 경우는 각각 분리됩니다.

$ AA="11          22"
$ IFS=' '              # IFS 값이 공백문자일 경우
$ echo $AA 
11 22                  # 연이어진 공백문자는 하나로 줄어든다.

# IFS 값이 기본값일 경우
$ echo $( echo -e "11       22\t\t\t\t33\n\n\n\n44" )
11 22 33 44

$ AA="11::::::::::22"
$ IFS=':'              # IFS 값이 공백문자가 아닐 경우
$ echo $AA         
11          22         # 줄어들지 않고 모두 공백으로 표시된다.
---------------------------

$ AA="Arch:Ubuntu:::Mint"

$ IFS=:                 # 공백이 아닌 문자를 사용하는 경우

$ ARR=( $AA )

$ echo ${#ARR[@]}       # 원소 개수가 빈 항목을 포함하여 5 개로 나온다.
5

$ echo ${ARR[1]}
Ubuntu
$ echo ${ARR[2]}

$ echo ${ARR[3]}

-----------------------------------

AA="Arch Ubuntu        Mint"

$ IFS=' '               # 공백문자를 사용하는 경우

$ ARR=( $AA )

$ echo ${#ARR[@]}       # IFS 값이 공백문자일 경우 연이어진 공백문자들은 하나로 취급됩니다.
3                       # 그러므로 원소 개수가 3 개로 나온다

$ echo ${ARR[1]}
Ubuntu
$ echo ${ARR[2]}
Mint

Script 작성시 주의할점

파일 이름에 space 가 포함되어 있을 경우 단어분리에 의해 파일 이름이 분리될 수 있습니다. 아래는 find 명령치환 값이 단어분리가 일어나는 예입니다. IFS 값을 newline 으로 변경하여 실행하면 문제를 해결할 수 있습니다.

$ ls
2013-03-19 154412.csv  ReadObject.java       WriteObject.class
ReadObject.class       쉘 스크립트 테스팅.txt    WriteObject.java


$ for file in $(find .)
do
        echo "$file"
done
.
./WriteObject.java
./WriteObject.class
./ReadObject.java
./2013-03-19            # 파일이름이 2개로 분리
154412.csv
./ReadObject.class
./쉘                    # 파일이름이 3개로 분리
스크립트
테스팅.txt

-----------------------------------------------

$ set -f; IFS=$'\n'           # IFS 값을 newline 으로 변경
                              # set -f 는 globbing 방지를 위한 옵션 설정
$ for file in $(find .)
do
        echo "$file"
done
.
./WriteObject.java
./WriteObject.class
./ReadObject.java
./2013-03-19 154412.csv
./ReadObject.class
./쉘 스크립트 테스팅.txt

$ set +f; IFS=$' \t\n'

find 명령의 -exec 옵션이나 xargs 명령 사용시 {} 에는 기본적으로 quote 을 할 필요가 없습니다.
하지만 sh -c 를 사용할 경우는 shell 환경이 되므로 단어분리와 globbing 이 발생합니다. 따라서 "{}" 와 같이 quote 을 해주어야 합니다.

$ echo 1234 > 'foo bar'

$ cat 'foo bar'
1234

$ find foo* -exec cat {} \;   # {} 를 quote 하지 않아도 된다.
1234

# sh -c 는 shell 환경이 되므로 단어분리가 일어난다.
$ find foo* -exec sh -c 'cat {}' \;
cat: foo: No such file or directory
cat: bar: No such file or directory

# 따라서 다음과 같이 quote 을 해주어야 합니다.
$ find foo* -exec sh -c 'cat "{}"' \;
1234
................................................

# xargs 명령 사용시도 마찬가지
$ find foo* | xargs -i cat {}
1234

$ find foo* | xargs -i sh -c 'cat {}'
cat: foo: No such file or directory
cat: bar: No such file or directory

$ find foo* | xargs -i sh -c 'cat "{}"'
1234

위에서 살펴본봐와 같이 {}sh -c 를 이용하지 않으면 단어분리와 globbing 의 영향을 받지 않는데요. 이것은 find 명령과 xargs 가 직접 {} 값을 처리하기 때문입니다. {} 값은 find 명령의 출력같이 newline 에 의해 분리되어 할당됩니다. 따라서 어떤 명령이 공백으로 값을 분리하여 출력한다면 {} 하나에 모든 값이 할당되어 버립니다.

$ pidof firefox
25226 18556 18347 14526 6962

# 5 개의 pid 가 {} 하나에 모두 할당된다.
$ pidof firefox | xargs -i echo XXX{}ZZZ
XXX25226 18556 18347 14526 6962ZZZ

# 따라서 다음 ps 명령은 ps '25226 18556 18347 14526 6962' 와 같이
# 모든 pid 를 single quote 한 것과 같기 때문에 오류가 발생합니다.
$ pidof firefox | xargs -i ps {}
error: process ID list syntax error
. . . . 
. . . .

# 이와 같은 경우 반대로 sh -c 에의한 단어 분리가 필요합니다.
$ pidof firefox | xargs -i sh -c 'ps {}'
  PID TTY      STAT   TIME COMMAND
 6962 ?        Sl    11:19 /home/mug896/Programs/firefox/firefox -contentproc ...
14526 ?        Sl     3:14 /home/mug896/Programs/firefox/firefox -contentproc ...
18347 ?        Sl   276:55 /home/mug896/Programs/firefox/firefox
18556 ?        Sl    31:49 /home/mug896/Programs/firefox/firefox -contentproc ...
25226 ?        Sl    64:19 /home/mug896/Programs/firefox/firefox 

# 만약에 각각의 pid 별로 ps 명령을 실행하려면 다음과 같이 합니다.
$ pidof firefox | xargs -n1 | xargs -i ps {}
  PID TTY      STAT   TIME COMMAND
25226 ?        Sl    77:08 /home/mug896/Programs/firefox/firefox -contentproc ...
  PID TTY      STAT   TIME COMMAND
18556 ?        Sl    32:02 /home/mug896/Programs/firefox/firefox -contentproc ...
  PID TTY      STAT   TIME COMMAND
18347 ?        Sl   290:12 /home/mug896/Programs/firefox/firefox
. . . .
. . . .