강력한 반복 전략: Bash 스크립트에서 파일과 리스트 순회하기

`for`와 `while`을 사용한 필수 Bash 반복 기법을 마스터하여 반복적인 시스템 작업을 효율적으로 자동화하세요. 이 포괄적인 가이드는 리스트 순회, 숫자 시퀀스 처리, 그리고 `while IFS= read -r`과 같은 모범 사례를 사용한 파일의 견고한 줄 단위 처리를 다룹니다. 기본 문법, 고급 루프 제어(`break`, `continue`), 그리고 강력하고 신뢰할 수 있는 셸 스크립팅 및 자동화를 위한 필수 기술을 실용적인 코드 예제와 함께 배우세요.

강력한 반복 전략: Bash 스크립트에서 파일과 리스트 순회하기

Bash 반복문은 작은 셸 명령어를 유용한 자동화 도구로 바꿔줍니다. 디렉토리의 모든 파일을 처리해야 하거나, 정해진 횟수만큼 작업을 수행해야 하거나, 설정 데이터를 한 줄씩 읽어야 하는 경우, 반복문은 명령어를 복사-붙여넣기 하지 않고도 작업을 반복할 수 있는 구조를 제공합니다.

가장 많이 사용하게 될 두 가지 반복문은 forwhile입니다. 배열이나 파일 글로브(glob)와 같이 이미 알려진 항목 집합이 있을 때는 for를 사용하세요. 반복문이 조건이나 입력 읽기에 의해 구동될 때는 while을 사용하세요. 이 간단한 구분만으로도 많은 스크립트를 더 쉽게 이해할 수 있습니다.


for 반복문: 고정된 집합 순회하기

for 반복문은 미리 처리해야 할 항목의 모음을 알고 있을 때 이상적입니다. 이 모음은 명시적인 값의 리스트, 명령어의 결과, 또는 글로빙(globbing)을 통해 찾은 파일 집합이 될 수 있습니다.

1. 표준 리스트 순회하기

가장 간단한 사용법은 스크립트에 직접 작성된 짧은 단어 리스트를 순회하는 것입니다.

문법

for 변수 in 항목_리스트; do
    # $변수를 사용하는 명령어들
done

예제: 사용자 리스트 처리하기

# 처리할 사용자 리스트
USERS="alice bob charlie"

for user in $USERS; do
  echo "$user의 홈 디렉토리를 확인하는 중..."
  if [ -d "/home/$user" ]; then
    echo "$user 사용자가 활성 상태입니다."
  else
    echo "경고: $user 사용자의 홈 디렉토리가 없습니다."
  fi
done

이 패턴은 간단한 이름에 적합합니다. 항목에 공백이 포함될 수 있다면 공백으로 구분된 문자열 대신 배열을 사용하세요:

USERS=("alice" "bob" "mary jane")

for user in "${USERS[@]}"; do
  echo "$user 확인 중"
done

2. C 스타일 숫자 반복

카운팅이나 특정 숫자 시퀀스가 필요한 작업의 경우, Bash는 C 스타일 for 반복문을 지원하며, 종종 중괄호 확장(brace expansion)이나 seq 명령어와 함께 사용됩니다.

문법 (C 스타일)

for (( 초기화; 조건; 증가 )); do
    # 명령어들
done

예제: 카운트다운 스크립트

# 5번 반복 (i는 1에서 시작, i가 5보다 작거나 같을 동안 계속)
for (( i=1; i<=5; i++ )); do
  echo "반복 횟수: $i"
  sleep 1
done
echo "완료!"

대안: 간단한 시퀀스를 위한 중괄호 확장 사용

중괄호 확장은 연속된 정수나 시퀀스를 생성할 때 seq를 사용하는 것보다 더 간단하고 빠릅니다.

# 10부터 1까지 숫자 생성
for num in {10..1}; do
  echo "카운트다운: $num"
done

3. 파일 및 디렉토리 순회하기 (글로빙)

for 반복문 내에서 와일드카드(*)를 사용하면 특정 패턴(예: 모든 로그 파일 또는 디렉토리의 모든 스크립트)과 일치하는 파일을 처리할 수 있습니다.

예제: 로그 파일 아카이빙

특히 공백이나 특수 문자가 포함된 파일 이름을 다룰 때는 변수("$file")를 인용 부호로 감싸세요.

TARGET_DIR="/var/log/application"

# 대상 디렉토리에서 .log로 끝나는 모든 파일을 순회
for logfile in "$TARGET_DIR"/*.log; do

  # 파일이 실제로 존재하는지 확인 (일치하는 파일이 없을 때 리터럴 "*.log"에서 실행되는 것을 방지)
  if [ -f "$logfile" ]; then
    echo "$logfile 압축 중..."
    gzip "$logfile"
  fi
done

while 반복문: 조건 기반 실행

while 반복문은 지정된 조건이 참인 동안 명령어 블록을 계속 실행합니다. 입력 스트림 읽기, 조건 모니터링, 또는 반복 횟수를 알 수 없는 작업을 처리하는 데 일반적으로 사용됩니다.

1. 기본 while 반복문

문법

while 조건; do
    # 명령어들
done

예제: 리소스 대기

이 반복문은 test 명령어([ ])를 사용하여 디렉토리가 존재하는지 확인한 후 진행합니다.

RESOURCE_PATH="/mnt/data/share"

while [ ! -d "$RESOURCE_PATH" ]; do
  echo "리소스 $RESOURCE_PATH가 마운트되기를 기다리는 중..."
  sleep 5
done

echo "리소스를 사용할 수 있습니다. 백업을 시작합니다."

2. 강력한 while read 패턴

while 반복문의 가장 강력한 응용은 파일 내용이나 출력 스트림을 한 줄씩 읽는 것입니다. 이 패턴은 cat의 출력에 for 반복문을 사용하는 것보다 훨씬 뛰어나며, 공백과 특수 문자를 안정적으로 처리합니다.

모범 사례: 줄 단위 읽기

최대한의 견고함을 보장하기 위해 세 가지 핵심 구성 요소를 활용합니다:

  1. IFS=: 내부 필드 구분자(Internal Field Separator)를 비워서 선행/후행 공백을 포함한 전체 줄이 변수로 읽히도록 합니다.
  2. read -r: -r 옵션은 백슬래시 해석을 방지(원시 읽기)하여 경로 및 복잡한 문자열에 중요합니다.
  3. 입력 재지정 (<): 파일 내용을 반복문 안으로 리디렉션하여 반복문이 현재 셸 컨텍스트에서 실행되도록 합니다(서브셸 문제 방지).
# 데이터를 포함한 파일, 한 줄에 하나의 항목
CONFIG_FILE="/etc/app/servers.txt"

while IFS= read -r server_name; do
  
  # 빈 줄이나 주석 줄 건너뛰기
  if [[ -z "$server_name" || "$server_name" =~ ^# ]]; then
    continue
  fi

  echo "서버 핑: $server_name"
  ping -c 1 "$server_name"

done < "$CONFIG_FILE"

팁: 반복문에서 cat 사용 피하기

파일을 읽을 때는 cat file | while ...보다 while ... done < file을 선호하세요. 대부분의 Bash 구성에서 파이프라인은 서브셸에서 반복문을 실행하므로, 반복문 내부에서 변경된 변수는 반복문이 종료되면 손실됩니다.

3. find에서 파일 이름 처리하기

재귀적 파일 처리를 위해 find의 일반 출력을 한 줄씩 파싱하는 것은 피하세요. 파일 이름에는 공백과 드물게 개행 문자가 포함될 수 있습니다. 널(null)로 구분된 출력을 사용하세요:

find /var/log/application -type f -name '*.log' -print0 |
while IFS= read -r -d '' logfile; do
  echo "로그 발견: $logfile"
  gzip -- "$logfile"
done

-print0read -d '' 쌍은 널 바이트를 구분자로 처리합니다. "$logfile" 앞의 --gzip에게 다음 값들이 피연산자(operand)이지 옵션이 아님을 알려주어, -로 시작하는 파일 이름으로부터 보호합니다.

고급 반복 제어 및 기술

효과적인 스크립트는 런타임 조건에 따라 반복 실행을 제어하는 기능을 필요로 합니다.

1. 흐름 제어: breakcontinue

  • break: 남은 반복 횟수나 조건에 관계없이 전체 반복문을 즉시 종료합니다.
  • continue: 현재 반복을 건너뛰고 즉시 다음 반복으로 이동합니다(또는 while 조건을 다시 평가합니다).

예제: 검색 및 중지

SEARCH_TARGET="target.conf"

for file in /etc/*; do
  if [ -f "$file" ] && [[ "$file" == *"$SEARCH_TARGET"* ]]; then
    echo "대상 설정 파일을 찾았습니다: $file"
    break  # 찾으면 처리를 중지
  elif [ -d "$file" ]; then
    continue # 디렉토리는 건너뛰고 파일만 확인
  fi
  echo "파일 확인 중: $file"
done

2. IFS를 사용한 복잡한 구분자 처리

파일을 줄 단위로 읽을 때는 IFS를 비워야 하지만, 다른 문자(예: 쉼표)로 구분된 리스트를 순회할 때는 일시적으로 IFS를 설정해야 합니다.

CSV_DATA="data1,data2,data3,data4"
OLD_IFS=$IFS # 원래 IFS 저장
IFS=','       # IFS를 쉼표 문자로 설정

for item in $CSV_DATA; do
  echo "항목 발견: $item"
done

IFS=$OLD_IFS # 반복문 직후 원래 IFS 복원

경고: 전역 IFS 변경

스크립트 내에서 $IFS를 수정하기 전에 항상 원래 값을 저장하세요(예: OLD_IFS=$IFS). 원래 값을 복원하지 않으면 이후 명령어에서 예측할 수 없는 동작이 발생할 수 있습니다.

강력한 Bash 반복문을 위한 모범 사례

사례 이유
항상 변수 인용 "$variable"을 사용하여 단어 분리(word splitting)와 글로브 확장(glob expansion)을 방지합니다. 특히 파일 순회 시 중요합니다.
while IFS= read -r 사용 파일을 줄 단위로 처리하는 가장 신뢰할 수 있는 방법으로, 공백과 특수 문자를 올바르게 처리합니다.
존재 여부 확인 글로빙(*.txt)을 사용할 때는 항상 확인(if [ -f "$file" ];)을 포함하여 일치하는 파일이 없을 경우 반복문이 리터럴 패턴 이름을 처리하지 않도록 합니다.
변수 지역화 함수 내에서 local 키워드를 사용하여 반복 변수가 실수로 전역 변수를 덮어쓰는 것을 방지합니다.
외부 명령어보다 내장 명령어 사용 성능을 위해 seq와 같은 외부 명령어를 생성하는 대신 중괄호 확장({1..10}) 또는 C 스타일 반복문을 사용하세요.

실용적인 경험 법칙

메모리 내 리스트에는 배열을, 간단한 파일 집합에는 글로브를, 줄 지향 입력에는 while IFS= read -r을, 재귀적 파일 이름 처리에는 널로 구분된 find 출력을 사용하세요. 기본적으로 확장을 인용하세요. 글로브 주변에 존재 여부 확인을 추가하세요. 반복문을 더 읽기 쉽게 만드는 경우에만 breakcontinue를 사용하고, 복잡한 제어 흐름을 숨기는 방법으로 사용하지 마세요.

대부분의 Bash 반복문 버그는 단어 분리, 예상치 못한 파일 이름, 또는 입력이 실제보다 더 깔끔하다고 가정하는 데서 발생합니다. 반복문이 공백, 빈 줄, 주석, 일치하지 않는 항목을 의도적으로 처리한다면 실제 자동화 작업에서도 잘 작동할 것입니다.