10.1. 루프

루프란 루프 제어 조건이 참인 동안에 여러 명령어들을 반복적으로 수행하는 코드 블럭입니다.

for 루프

for (in)

다음은 기본적인 루프문인데 C의 루프문과는 상당한 차이를 보입니다.

for arg in [list]
do
command...
done

참고: 루프의 각 단계마다 list의 값들이 arg에 들어갑니다.

for arg in "$var1" "$var2" "$var3" ... "$varN"  
# 루프 1 단계, $arg = $var1	    
# 루프 2 단계, $arg = $var2	    
# 루프 3 단계, $arg = $var3	    
# ...
# 루프 N 단계, $arg = $varN

# [list]의 인자들은 쿼우팅을 해서 낱말 조각남(word splitting)을 막아 줘야함.

list 인자에는 와일드 카드가 올 수도 있습니다.

dofor와 한 줄에 쓴다면 list 뒤에 세미콜론이 있어야 합니다.

for arg in [list] ; do

예 10-1. 간단한 for 루프

#!/bin/bash
# 떠돌이별 목록.

for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
do
  echo $planet
done

echo

# 따옴표로 묶인 전체 '목록'은 한 개의 변수를 만들어 냅니다.
for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
do
  echo $planet
done

exit 0

참고: [list] 항목은 다중 인자를 가질수 있습니다. 이는 인자가 그룹으로 되어 있을 때 유용한데 이런 경우에는 set 명령어(예 11-10 참고)를 써서 [list] 각 항목이 위치 매개변수로 파싱되어 할당되도록 해 주면 됩니다.

예 10-2. 각 [list] 항목이 인자를 두 개씩 갖는 for

#!/bin/bash
# 떠돌이별 재검토.

# 각 떠돌이별의 이름과 해(sun)까지 거리를 한 쌍으로 묶음.

for planet in "Mercury 36" "Venus 67" "Earth 93"  "Mars 142" "Jupiter 483"
do
  set -- $planet  # "planet" 변수를 파싱해서 위치 매개변수로 세팅.
  # "--" 를 쓰면 $planet 이 널이거나 대쉬 문자로 시작하는 등의 까다로운 상황을 처리해 줍니다.

  # 원래의 위치 매개변수는 덮어써지기 때문에 다른 곳에 저장해 놓아야 할지도 모릅니다.
  # 배열을 써서 해 볼 수 있겠네요.
  #        original_params=("$@")

  echo "$1		해까지 거리 $2,000,000 마일"
done

# (S.C. 가 확실한 설명을 더 해 줬습니다.)

exit 0

for 문의 [list]에 변수가 들어갈 수도 있습니다.

예 10-3. Fileinfo: 변수에 들어 있는 파일 목록에 대해 동작

#!/bin/bash
# fileinfo.sh

FILES="/usr/sbin/privatepw
/usr/sbin/pwck
/usr/sbin/go500gw
/usr/bin/fakefile
/sbin/mkreiserfs
/sbin/ypbind"     # 여러분이 궁금해하는 파일들 목록.
                  # 더미 파일인 /usr/bin/fakefile 을 그냥 넣었습니다.

echo

for file in $FILES
do

  if [ ! -e "$file" ]       # 파일이 존재하는지 확인.
  then
    echo "$file 은 존재하지 않는 파일입니다."; echo
    continue                # 다음 파일을 확인.
   fi

  ls -l $file | awk '{ print $9 "         파일 크기: " $5 }'  # 2개의 필드를 출력.
  whatis `basename $file`   # 파일에 대한 정보.
  echo
done  

exit 0

[list]에는 파일명 globbing이 올 수도 있습니다. 즉, 파일명 확장을 위한 와일드 카드를 쓸 수 있습니다.

예 10-4. for 문에서 파일 조작하기

#!/bin/bash
# list-glob.sh: "globbing"으로 for 루트의 [list] 만들어내기.

echo

for file in *
do
  ls -l "$file"  # $PWD(현재 디렉토리)의 모든 파일을 나열.
  # 와일드 카드 문자인 "*"는 모든 문자와 일치합니다. 기억나죠?
  # 하지만 "globbing"에서는 점(dot) 파일은 일치하지 않습니다.

  #  만약에 패턴이 아무 파일과 일치하지 않는다면 그냥 자기 자신으로 확장되는데,
  #+ 이걸 피하려면 nullglob 옵션을 주면 됩니다.
  #  (shopt -s nullglob).
  #  S.C.의 지적 사항.
done

echo; echo

for file in [jx]*
do
  rm -f $file    # $PWD에서 "j"나 "x"로만 시작하는 모든 파일을 지움.
  echo "\"$file\" 이 지워졌습니다."
done

echo

exit 0

for 문에서 in [list]을 안 써주면 명령어 줄에서 넘어온 인자인 $@에 대해서 동작합니다. 이런 식으로 처리하는 멋진 예제를 보려면 예 A-11를 참고하세요.

예 10-5. in [list]가 빠진 for

#!/bin/bash

# 인자를 줘서 실행시켜도 보고 안 줘서 실행시켜 본 다음, 어떻게 되는지 보세요.

for a
do
 echo -n "$a "
done

# 'in list' 가 없기 때문에 '$@'에 대해서 동작합니다.
# ('$@'는 공백문자를 포함하는 명령어줄 인자 리스트).

echo

exit 0

for 문의 [list]명령어 치환를 쓸 수도 있습니다. 예 12-31예 10-9, 예 12-29를 참고하세요.

예 10-6. for 문의 [list]에 명령어 치환 쓰기

#!/bin/bash
# for 루프의 [list]에 명령어 치환을 쓰기.

NUMBERS="9 7 3 8 37.53"

for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
do
  echo -n "$number "
done

echo 
exit 0

다음은 [list]를 만들어 낼 때 좀 더 복잡한 명령어 치환을 쓰는 예제입니다.

예 10-7. 이진 파일에 grep 걸기

#!/bin/bash
# bin-grep.sh: 이진 파일에서 일치하는 문자열 찾아내기.

# "grep"을 이진 파일에 걸기.
# "grep -a"라고 해도 비슷합니다.

E_BADARGS=65
E_NOFILE=66

if [ $# -ne 2 ]
then
  echo "사용법: `basename $0` string filename"
  exit $E_BADARGS
fi

if [ ! -f "$2" ]
then
  echo "\"$2\" 은 존재하지 않는 파일입니다."
  exit $E_NOFILE
fi  


for word in $( strings "$2" | grep "$1" )
# "strings" 명령어는 이진 파일에 들어 있는 문자열들을 보여주고,
# 그 출력을 "grep"에 파이프로 걸어 원하는 문자열을 찾아냅니다.
do
  echo $word
done

# S.C. 가 위의 for 루프는 다음과 같이 더 간단하게 할 수 있다고 지적해 주었습니다.
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'


# "./bin-grep.sh mem /bin/ls"  같은 식으로 해서 이 스크립트를 연습해 보세요.

exit 0

다음도 명령어 치환으로 [list]를 만들어 내는 예제입니다.

예 10-8. 특정 디렉토리의 모든 바이너리 파일에 대해 원저작자(authorship)를 확인 하기

#!/bin/bash
# findstring.sh: 주어진 디렉토리의 이진 파일에서 특정한 문자열 찾아내기.

directory=/usr/bin/
fstring="Free Software Foundation"  # FSF에서 만든 파일에는 어떤 것이 있나 알아볼까요?

for file in $( find $directory -type f -name '*' | sort )
do
  strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
  # 여기서는 "sed" 표현식의 $directory 가 원래 구분자인 "/"를 포함하고 있기 때문에 
  # "/" 구분자를 다른 것(%)으로 바꿔줘야 합니다.
  # 이렇게 하지 않으면 에러 메세지가 납니다(sed -e "s/$directory//" 라고 한 번 해보세요).
done  

exit 0

# 쉬운 연습문제 하나 내겠습니다:
# $directory 와 $fstring 을 명령어줄에서 받아 들일 수 있도록 수정해 보세요.

for 루프의 출력은 다른 명령어로 파이프를 걸 수도 있습니다.

예 10-9. 디렉토리에 들어 있는 심볼릭 링크들을 나열하기

#!/bin/bash
# symlinks.sh: 디렉토리의 심볼릭 링크 파일들을 나열하기.

ARGS=1                 # 명령어줄 인자는 한 개만 필요.

if [ $# -ne "$ARGS" ]  # 인자가 1 개가 아니면...
then
  directory=`pwd`      # 현재 작업 디렉토리
else
  directory=$1
fi

echo "\"$directory\" 디렉토리의 심볼릭 링크들"

for file in "$( find $directory -type l )"   # -type l = 심볼릭 링크
do
  echo "$file"
done | sort                                  # 파일 목록을 정렬.

#  Dominik 'Aeneas' Schnitzer 가 지적한대로,
#+ $( find $directory -type l ) 을 쿼우트 안 해주면
#+ 파일이름에 공백문자가 들어있는 파일에 대해서는 제대로 동작하지 않습니다.

exit 0

방금 전의 예제에 약간의 변경만 가하면 루프의 표준출력을 파일로 재지향 시킬 수 있습니다.

예 10-10. 디렉토리에 들어 있는 심볼릭 링크들을 파일로 저장하기

#!/bin/bash
# symlinks.sh: 디렉토리에 들어 있는 심볼릭 링크를 나열하기.

ARGS=1                 # 명령어줄 인자가 한 개 있어야 됩니다.
OUTFILE=symlinks.list  # 저장할 파일

if [ $# -ne "$ARGS" ]  # 인자가 1개가 아니라면...
then
  directory=`pwd`      # 현재 작업 디렉토리
else
  directory=$1
fi

echo "\"$directory\" 디렉토리의 심볼릭 링크들"

for file in "$( find $directory -type l )"   # -type l = 심볼릭 링크
do
  echo "$file"
done | sort > "$OUTFILE"                     # 루프의 표준 출력이
#           ^^^^^^^^^^^^                       저장될 파일로 재지향 됩니다.

exit 0

이중 소괄호를 쓰면 C 프로그래머들에게 익숙한 형태의 for 루프 문법도 쓸 수 있습니다.

예 10-11. C 형태의 for 루프

#!/bin/bash
# 10까지 세는 두 가지 방법.

echo

# 표준 문법.
for a in 1 2 3 4 5 6 7 8 9 10
do
  echo -n "$a "
done  

echo; echo

# +==========================================+

# 이제는 C 형태의 문법을 써서 똑같은 일을 해 보겠습니다.

LIMIT=10

for ((a=1; a <= LIMIT ; a++))  # 이중 소괄호와 "$" 없는 "LIMIT".
do
  echo -n "$a "
done                           # 'ksh93' 에서 빌려온 문법.

echo; echo

# +=========================================================================+

# C 의 "콤마 연산자"를 써서 두 변수를 동시에 증가시켜 보겠습니다.

for ((a=1, b=1; a <= LIMIT ; a++, b++))  # 콤마를 쓰면 여러 연산을 함께 할 수 있습니다.
do
  echo -n "$a-$b "
done

echo; echo

exit 0

예 26-6예 26-7도 참고하세요.

---

자, 이제는 "실제"로 어떻게 쓰이는지 봅시다.

예 10-12. 배치 모드로 efax 사용하기

#!/bin/bash

EXPECTED_ARGS=2
E_BADARGS=65

if [ $# -ne $EXPECTED_ARGS ]
# 적당한 명령어 줄 인자가 넘어 왔는지 확인.
then
   echo "사용법: `basename $0` phone# text-file"
   exit $E_BADARGS
fi


if [ ! -f "$2" ]
then
  echo "$2 는 텍스트 파일이 아닙니다."
  exit $E_BADARGS
fi
  

fax make $2              # 주어진 텍스트 파일에서 팩스 포맷의 파일을 생성.

for file in $(ls $2.0*)  # 변환된 파일을 이어 붙임.
                         # 변수 목록에서 와일드 카드를 사용.
do
  fil="$fil $file"
done  

efax -d /dev/ttyS3 -o1 -t "T$1" $fil   # 동작.


# S.C. 가 지적했듯이, 
#    efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
# 라고 하면 for 루프를 안 써도 됩니다만, 
# 그렇게 좋은 방법은 아닙니다. ^^

exit 0
while

while은 루프 최상단에서 특정 조건을 확인하면서 그 조건이 참일 동안 루프를 계속 돌도록 해 줍니다(종료 상태 0을 리턴합니다).

while [condition]
do
command...
done

for/in 경우처럼 do를 조건 테스트문과 같은 줄에 쓰려면 세미콜론을 써 줘야 합니다.

while [condition] ; do

여기서 설명했던 표준 형태가 아닌 getopts construct같은 특별한 형태의 while 문도 있다는 것에 주의하시기 바랍니다.

예 10-13. 간단한 while 루프

#!/bin/bash

var0=0
LIMIT=10

while [ "$var0" -lt "$LIMIT" ]
do
  echo -n "$var0 "        # -n 은 뉴라인을 없애줍니다.
  var0=`expr $var0 + 1`   # var0=$(($var0+1)) 이라고 해도 동작합니다.
done

echo

exit 0

예 10-14. 다른 while 루프

#!/bin/bash

echo

while [ "$var1" != "end" ]     # while test "$var1" != "end"
do                             # 라고 해도 동작함.
  echo "변수값을 넣으세요 #1 (끝내려면 end) "
  read var1                    # 'read $var1' 이 아니죠? 왜 그럴까요?
  echo "변수 #1 = $var1"       # "#" 때문에 쿼우트를 해줘야 됩니다.
  # 종료 조건이 루프 처음에 테스트되기 때문에 'end'도 에코됩니다.
  echo
done  

exit 0

while 문은 다중 조건을 가질 수 있습니다만 오직 마지막 조건이 루프를 끝낼 조건을 결정합니다. 그렇기 때문에 이럴 경우에는 문법이 약간 달라집니다.

예 10-15. 다중 조건 while 루프

#!/bin/bash

var1=unset
previous=$var1

while echo "이전 변수 = $previous"
      echo
      previous=$var1
      [ "$var1" != end ] # 바로 전의 "var1"이 무엇이었는지 계속 확인.
      # "while"에는 4가지 조건이 있지만 오직 마지막 조건이 루프를 제어합니다.
      # *마지막* 종료 상태가 중요하다는 말씀.
do
echo "변수값을 넣으세요 #1 (끝내려면 end) "
  read var1
  echo "변수 #1 = $var1"
done  

# 이 스크립트가 어떻게 돌아가는지 알아내 보세요.
# 약간 미묘한(tricky) 부분이 있습니다.

exit 0

for 루프처럼 while도 이중소괄호를 써서 C 형태의 문법을 적용할 수 있습니다(예 9-22 참고).

예 10-16. C 형태의 문법을 쓰는 while 루프

#!/bin/bash
# wh-loopc.sh: "while" 루프에서 10까지 세기.

LIMIT=10
a=1

while [ "$a" -le $LIMIT ]
do
  echo -n "$a "
  let "a+=1"
done           # 아직은 별로 놀랄게 없네요.

echo; echo

# +=================================================================+

# 이제 똑같은 것을 C 형태의 문법으로 해 봅시다.

((a = 1))      # a=1
# 이중 소괄호에서는 변수를 세팅할 때 C 처럼 빈 칸을 넣어도 됩니다.

while (( a <= LIMIT ))   # 이중 소괄호, 변수 앞에 "$"가 없네요.
do
  echo -n "$a "
  ((a += 1))   # let "a+=1"
  # 역시 되는군요.
  # 이중 소괄호를 쓰면 C 문법처럼 변수를 증가시킬 수 있군요.
done

echo

# 이제 C 프로그래머도 Bash 를 쓸 때 편안하게 쓸 수 있겠습니다.

exit 0

참고: while 루프 마지막에 <을 써 표준입력을 파일에서 재지향 받을 수 있습니다.

until

until은 루프 최상단에서 특정 조건을 확인하면서 그 조건이 거짓일 동안 루프를 계속 돌도록 해 줍니다(while과 반대).

until [condition-is-true]
do
command...
done

주의할 점은 until이 몇몇 프로그래밍 언어에서 비슷한 형태와는 다르게 루프 처음에서 끝내는 조건을 검사한다는 것 입니다.

for/in 경우처럼 do를 조건문과 한 줄에 같이 쓰려면 세미콜론을 적어줘야 합니다.

until [condition-is-true] ; do

예 10-17. until 루프

#!/bin/bash

until [ "$var1" = end ] # 테스트 조건이 루프 최상단에 들어갑니다.
do
  echo "변수값을 넣으세요 #1 "
  echo "(끝내려면 end)"
  read var1
  echo "변수 #1 = $var1"
done  

exit 0