Bash에서 구분자로 문자열 나누기 — IFS read, readarray, 파라미터 확장 실측 비교

CSV 한 줄, PATH 같은 콜론 목록, 설정 파일의 key=v1;v2;v3처럼 구분자로 이어진 문자열을 배열로 나누는 일은 셸 스크립트에서 매일 나온다. 그런데 흔히 쓰는 방법들은 공백이 든 필드, * 같은 glob 문자, 빈 필드, 끝 구분자, 줄바꿈을 만나면 각자 다른 결과를 낸다.

이 글에서는 대표적인 분리 방법 네 가지를 같은 입력 여섯 개로 Ubuntu 24.04(bash 5.2.21)에서 실행해 비교하고, 빈 필드 보존·여러 글자 구분자·필드 하나만 꺼내기까지 상황별 방법을 정리한다.

비교한 방법

#!/usr/bin/env bash
show() { printf '%-10s n=%d ' "$1" "$2"; shift 2; printf '[%s]' "$@"; echo; }

split_all() {
    local in=$1 arr
    IFS=';' read -ra arr <<< "$in";            show "read -ra"  ${#arr[@]} "${arr[@]}"
    arr=(${in//;/ });                          show "(\${//})"  ${#arr[@]} "${arr[@]}"
    readarray -td';' arr <<< "$in";            show "readarray" ${#arr[@]} "${arr[@]}"
    readarray -td';' arr < <(printf '%s' "$in"); show "readarr+p" ${#arr[@]} "${arr[@]}"
}

for in in 'a;b;c' 'a b;c' 'a;*;c' 'a;;c' 'a;b;' $'a;b\nc;d'; do
    printf -- '--- in=%q\n' "$in"
    split_all "$in"
done

작업 디렉터리에는 glob 확인용으로 x.txt, y.txt를 만들어 두었다. readarr+p는 here-string 대신 printf로 넘겨 끝 줄바꿈을 없앤 변형이다.

입력별 실행 결과

$ touch x.txt y.txt && bash split.sh
--- in=a\;b\;c
read -ra   n=3 [a][b][c]
(${//})    n=3 [a][b][c]
readarray  n=3 [a][b][c
]
readarr+p  n=3 [a][b][c]
--- in=a\ b\;c
read -ra   n=2 [a b][c]
(${//})    n=3 [a][b][c]
readarray  n=2 [a b][c
]
readarr+p  n=2 [a b][c]
--- in=a\;\*\;c
read -ra   n=3 [a][*][c]
(${//})    n=5 [a][split.sh][x.txt][y.txt][c]
readarray  n=3 [a][*][c
]
readarr+p  n=3 [a][*][c]
--- in=a\;\;c
read -ra   n=3 [a][][c]
(${//})    n=2 [a][c]
readarray  n=3 [a][][c
]
readarr+p  n=3 [a][][c]
--- in=a\;b\;
read -ra   n=2 [a][b]
(${//})    n=2 [a][b]
readarray  n=3 [a][b][
]
readarr+p  n=2 [a][b]
--- in=$'a;b\nc;d'
read -ra   n=2 [a][b]
(${//})    n=4 [a][b][c][d]
readarray  n=3 [a][b
c][d
]
readarr+p  n=3 [a][b
c][d]

${in//;/ }는 치환 결과를 따옴표 없이 다시 word splitting·glob 확장에 넘기므로 공백 필드가 쪼개지고 *가 파일 목록으로 바뀐다.

입력read -ra(${in//;/ })readarray -td';'readarray + printf
a b;c (공백 필드)✔✘ 3개로 쪼개짐✔✔
a;*;c (glob)✔✘ 파일명으로 확장✔✔
a;;c (빈 필드)✔✘ 빈 필드 사라짐✔✔
a;b; (끝 구분자)빈 필드 버림빈 필드 버림✘ \n 필드 추가빈 필드 버림
여러 줄첫 줄만✘ 줄 경계 무시✘ 줄바꿈 섞임줄바꿈이 필드에 포함
마지막 필드✔✔✘ 끝에 \n✔

한 줄 입력이면 IFS=';' read -ra가 가장 무난하고, readarray는 here-string이 붙이는 줄바꿈을 반드시 제거해야 한다.

상황별 분리 방법

#!/usr/bin/env bash
show() { printf '%-12s n=%d ' "$1" "$2"; shift 2; printf '[%s]' "$@"; echo; }

echo "--- 끝의 빈 필드 보존"
for in in 'a;b;c' 'a;b;' ';'; do
    readarray -td';' arr < <(printf '%s;' "$in")
    show "$in" ${#arr[@]} "${arr[@]}"
done

echo "--- IFS 공백 vs 비공백"
IFS=' ' read -ra arr <<< '  a   b  ';  show "IFS=' '" ${#arr[@]} "${arr[@]}"
IFS=',' read -ra arr <<< ',a,,,b,';    show "IFS=','" ${#arr[@]} "${arr[@]}"

echo "--- IFS는 문자 집합"
IFS=', ' read -ra arr <<< 'a, b,c d';   show "IFS=', '" ${#arr[@]} "${arr[@]}"

echo "--- 여러 글자 구분자 ::"
split_str() {   # split_str 문자열 구분자 → 전역 배열 parts
    local s=$1 d=$2
    parts=()
    while [[ $s == *"$d"* ]]; do
        parts+=("${s%%"$d"*}")
        s=${s#*"$d"}
    done
    parts+=("$s")
}
split_str 'usr::local:bin::' '::';     show "split_str" ${#parts[@]} "${parts[@]}"

echo "--- 필드 하나만: cut / awk / 파라미터 확장"
line='root:x:0:0:root:/root:/bin/bash'
echo "cut : $(cut -d: -f6 <<< "$line")"
echo "awk : $(awk -F: '{print $NF}' <<< "$line")"
echo "\${} : ${line%%:*}"
$ bash split2.sh
--- 끝의 빈 필드 보존
a;b;c        n=3 [a][b][c]
a;b;         n=3 [a][b][]
;            n=2 [][]
--- IFS 공백 vs 비공백
IFS=' '      n=2 [a][b]
IFS=','      n=5 [][a][][][b]
--- IFS는 문자 집합
IFS=', '     n=4 [a][b][c][d]
--- 여러 글자 구분자 ::
split_str    n=3 [usr][local:bin][]
--- 필드 하나만: cut / awk / 파라미터 확장
cut : /root
awk : /bin/bash
${} : root

구분자를 하나 덧붙여 readarray에 넘기면 끝의 빈 필드까지 개수가 정확히 맞는다. 공백 IFS는 연속 공백을 하나로 합치고 앞뒤를 잘라내지만, , 같은 비공백 IFS는 빈 필드를 그대로 남긴다.

필요한 것방법
한 줄을 배열로 (일반적인 경우)IFS=';' read -ra arr <<< "$in"
빈 필드·끝 빈 필드까지 정확히readarray -td';' arr < <(printf '%s;' "$in")
두 글자 이상 구분자${s%%"$d"*} / ${s#*"$d"} 루프
필드 하나만cut -d: -f6, awk -F: '{print $NF}', ${line%%:*}
여러 줄 파일을 줄 단위로while IFS=';' read -ra f; do ...; done < file

주의사항

  • IFS=';' read의 IFS는 그 read 명령에만 적용된다. IFS=';'를 단독 줄로 쓰면 이후 스크립트 전체의 word splitting이 바뀌므로, 꼭 필요하면 local IFS로 함수 안에 가둔다.
  • IFS의 각 문자가 개별 구분자다. IFS='::'로 두 글자 구분자를 만들 수 없고, : 하나짜리 구분자와 같다.
  • read에 -r이 없으면 역슬래시가 이스케이프로 해석되어 C:\dir 같은 값이 깨진다.
  • readarray -d는 bash 4.4 이상에서만 쓸 수 있다. macOS 기본 /bin/bash(3.2)에는 readarray 자체가 없다.
  • for x in $(echo $in | tr ';' '\n') 형태도 따옴표 없는 확장이라 (${in//;/ })와 같은 glob·공백 문제를 그대로 가진다.

마무리

구분자 분리에서 문제를 일으키는 것은 거의 항상 따옴표 없는 확장이다. 한 줄은 read -ra, 필드 개수가 정확해야 하면 구분자를 덧붙인 readarray -td, 여러 글자 구분자는 파라미터 확장 루프를 쓴다.

참고

답글 남기기