Word Splitting

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

IFS 변수는 기본적으로 export 된 변수입니다.

$ AA="11X22X33Y44Y55"

$ echo $AA
11X22X33Y44Y55

$ IFS=XY

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

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

# 따라서 IFS 변수값을 출력하려면 quote 을 해야합니다.
$ ( IFS=XYZ; echo $IFS )

$ ( IFS=XYZ; echo "$IFS" )
XYZ
------------------------------------------------------

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

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

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

$ echo -en "11 22\t33\n44" 
11 22   33
44

$ echo -en "11 22\t33\n44" | od -a
0000000   1   1  sp   2   2  ht   3   3  nl   4   4

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

$ echo -n $( echo -e "11 22\t33\n44" ) | od -a
0000000   1   1  sp   2   2  sp   3   3  sp   4   4

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

$ echo -n "$( echo -e "11 22\t33\n44" )" | od -a
0000000   1   1  sp   2   2  ht   3   3  nl   4   4

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

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

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

# 다음과 같이 할 수도 있습니다.
# tab 문자 입력은 ctrl-v 한후 tab 키
sh$ tab='    '            
# 또는
sh$ tab=`printf '\011'`
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 로 변경하였지만 인수가 분리되지 않고 그대로 공백에 의해 분리가 됩니다.

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

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

foo 11Q22
foo 33 44
====== output ========

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

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

IFS=Q     
AA="11Q22"
BB="33 44"
foo $AA
foo $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

IFS 변수를 unset 할 경우

IFS 변수를 unset 하여 존재하지 않게 되면 기본값일 때와 동일하게 동작합니다. 따라서 IFS 값을 변경하여 사용한 후에 다시 기본값으로 복구하고자 할때 간단히 unset -v IFS 를 사용할 수 있습니다.

이것은 sh 에서도 동일하게 동작합니다.

$ AA='111 222
333 444
555 666'

$ set -- $AA

$ echo $2 : $#        # IFS 값이 기본값일 경우 전체 원소는 6 개가 된다.
222 : 6

$ IFS='               # IFS 값을 변경해 사용
'
$ set -- $AA

$ echo $2 : $#        # IFS 값이 newline 이므로 전체 원소는 3 개가 된다.
333 444 : 3

$ unset -v IFS        # IFS 변수를 unset 하면 기본값일 때와 동일하게 동작한다.

$ set -- $AA

$ echo $2 : $#
222 : 6

스크립트 작성시 주의할점

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

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


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

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

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

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

----------------------------------------------
                                                           # bash 일 경우
$ while read file; do echo "$file"; done <<< $(find .)     # here string 을 활용
.
./쉘 스크립트 테스팅.txt
./2013-03-19 154412.csv
./ReadObject.class
./ReadObject.java
./WriteObject.java
./WriteObject.class

다음은 find 명령으로 dir2 디렉토리를 검색해서 파일명을 awk 의 인수로 전달하고 awk 명령은 각각의 파일 내용을 검색해서 라인에 bar 스트링이 존재할 경우만 파일명을 출력해서 myarr 배열의 원소로 저장합니다. 실행전에 먼저 IFS 값을 newline 으로 설정하고, set -f 옵션을 설정하였으므로 find 명령으로 찾은 파일 이름에 공백이나 glob 문자가 포함되어 있어도 정상적으로 awk 명령에 전달이 되고, awk 명령에서는 파일 이름을 print 명령으로 출력하면 newline 에의해 분리되어 배열 원소로 저장되므로 결과적으로 올바르게 myarr 배열값이 설정됩니다.

set -f; IFS=$'\n'
myarr=( $(awk '/^bar /{ print FILENAME; nextfile }' $(find dir2 -name '*.txt')) )
set +f; unset -v IFS

Quiz

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

$ echo 1234 > 'foo bar'       # 공백으로 분리된 파일명

$ cat 'foo bar'               # quote 을 해주어야 한다.
1234

# {} 문자를 find 명령이 직접 처리하므로 quote 하지 않아도 된다.
$ find foo* -exec cat {} \;
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 가 직접 {} 문자를 처리하기 때문입니다. {} 값은 기본적으로 newline 에 의해 분리되어 할당됩니다 ( find 명령의 출력같이 ). 따라서 어떤 명령이 공백으로 값을 분리하여 출력한다면 {} 하나에 모든 값이 할당되어 버리게 됩니다.

$ 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
. . . .
. . . .