26장. 배열

bash 새 버젼부터는 1차원 배열을 지원합니다. variable[xx]처럼 선언할 수도 있고 declare -a variable처럼 직접적으로 지정해 줄 수도 있습니다. 배열 변수를 역참조하려면(내용을 알아내려면) ${variable[xx]}처럼 중괄호 표기법을 쓰면 됩니다.

예 26-1. 간단한 배열 사용법

#!/bin/bash


area[11]=23
area[13]=37
area[51]=UFOs

# 배열 멤버들은 인접해 있거나 연속적이지 않아도 됩니다.

# 몇몇 멤버를 초기화 되지 않은 채 놔둬도 됩니다. 
# 배열 중간이 비어 있어도 괜찮습니다.


echo -n "area[11] = "
echo ${area[11]}    #  {중괄호}가 필요

echo -n "area[13] = "
echo ${area[13]}

echo "area[51]의 값은 ${area[51]} 입니다."

# 초기화 안 된 배열 변수는 빈 칸으로 찍힙니다.
echo -n "area[43] = "
echo ${area[43]}
echo "(area[43]은 할당되지 않았습니다)"

echo

# 두 배열 변수의 합을 세 번째 배열 변수에 할당합니다.
area[5]=`expr ${area[11]} + ${area[13]}`
echo "area[5] = area[11] + area[13]"
echo -n "area[5] = "
echo ${area[5]}

area[6]=`expr ${area[11]} + ${area[51]}`
echo "area[6] = area[11] + area[51]"
echo -n "area[6] = "
echo ${area[6]}
# 문자열에 정수를 더하는 것이 허용되지 않기 때문에 동작하지 않습니다.

echo; echo; echo

# -----------------------------------------------------------------
# 다른 배열인 "area2"를 봅시다.
# 배열 변수에 값을 할당하는 다른 방법을 보여줍니다...
# array_name=( XXX YYY ZZZ ... )

area2=( zero one two three four )

echo -n "area2[0] = "
echo ${area2[0]}
# 아하, 배열 인덱스가 0부터 시작하는군요(배열의 첫번째 요소는 [0]이지 [1]이 아닙니다).

echo -n "area2[1] = "
echo ${area2[1]}    # [1]은 배열의 두번째 요소입니다.
# -----------------------------------------------------------------

echo; echo; echo

# -----------------------------------------------
# 또 다른 배열 "area3".
# 배열 변수에 값을 할당하는 또 다른 방법...
# array_name=([xx]=XXX [yy]=YYY ...)

area3=([17]=seventeen [24]=twenty-four)

echo -n "area3[17] = "
echo ${area3[17]}

echo -n "area3[24] = "
echo ${area3[24]}
# -----------------------------------------------

exit 0

배열 변수는 독특한 문법을 갖고 있고 표준 Bash 연산자들도 배열에 쓸 수 있는 특별한 옵션을 갖고 있습니다.

 array=( zero one two three four five )

echo ${array[0]}       #  zero
echo ${array:0}        #  zero
                       #  첫번째 요소의 매개변수 확장.
echo ${array:1}        #  ero
                       #  첫번째 요소의 두 번째 문자에서부터 매개변수 확장.

echo ${#array}         #  4
                       #  배열 첫번째 요소의 길이.

몇몇 Bash 내장 명령들은 배열 문맥에서 그 의미가 약간 바뀝니다. 예를 들어, unset 은 배열의 한 요소를 지워주거나 배열 전체를 지워줍니다.

예 26-2. 배열의 특별한 특성 몇 가지

#!/bin/bash

declare -a colors
# 크기 지정없이 배열을 선언하게 해줍니다.

echo "좋아하는 색깔을 넣으세요(빈 칸으로 구분해 주세요)."

read -a colors    # 아래서 설명할 특징들 때문에, 최소한 3개의 색깔을 넣으세요.
#  'read'의 특별한 옵션으로 배열에 읽은 값을 넣어 줍니다.

echo

element_count=${#colors[@]}
# 배열 요소의 총 갯수를 알아내기 위한 특별한 문법.
#     element_count=${#colors[*]} 라고 해도 됩니다.
#
#  "@" 변수는 쿼우트 안에서의 낱말 조각남(word splitting)을 허용해 줍니다.
#+ (공백문자에 의해 나눠져 있는 변수들을 추출해 냄).

index=0

while [ "$index" -lt "$element_count" ]
do    # 배열의 모든 요소를 나열해 줍니다.
  echo ${colors[$index]}
  let "index = $index + 1"
done
# 각 배열 요소는 한 줄에 하나씩 찍히는데,
# 이게 싫다면  echo -n "${colors[$index]} " 라고 하면 됩니다.
#
# 대신 "for" 루트를 쓰면:
#   for i in "${colors[@]}"
#   do
#     echo "$i"
#   done
# (Thanks, S.C.)

echo

# 좀 더 우아한 방법으로 모든 배열 요소를 다시 나열.
  echo ${colors[@]}          # echo ${colors[*]}   라고 해도 됩니다.

echo

# "unset" 명령어는 배열 요소를 지우거나 배열 전체를 지워줍니다.
unset colors[1]              # 배열의 두번째 요소를 삭제.
                             # colors[1]=    라고 해도 됩니다.
echo  ${colors[@]}           # 배열을 다시 나열하는데 이번에는 두 번째 요소가 빠져있습니다.

unset colors                 # 배열 전체를 삭제.
                             #  unset colors[*] 나
                             #+ unset colors[@] 라고 해도 됩니다.
echo; echo -n "색깔이 없어졌어요."			   
echo ${colors[@]}            # 배열을 다시 나열해 보지만 비어있죠.

exit 0

위의 예제에서 살펴본 것처럼 ${array_name[@]}${array_name[*]}는 배열의 모든 원소를 나타냅니다. 배열의 원소 갯수를 나타내려면 앞의 표현과 비슷하게 ${#array_name[@]}${#array_name[*]}라고 하면 됩니다. ${#array_name}는 배열의 첫번째 원소인 ${array_name[0]}의 길이(문자 갯수)를 나타냅니다.

예 26-3. 빈 배열과 빈 원소

#!/bin/bash
# empty-array.sh

# 빈 배열과 빈 요소를 갖는 배열은 다릅니다.

array0=( first second third )
array1=( '' )   # "array1" 은 한 개의 요소를 갖고 있습니다.
array2=( )      # 요소가 없죠... "array2"는 비어 있습니다.

echo

echo "array0 의 요소들:  ${array0[@]}"
echo "array1 의 요소들:  ${array1[@]}"
echo "array2 의 요소들:  ${array2[@]}"
echo
echo "array0 의 첫번째 요소 길이 = ${#array0}"
echo "array1 의 첫번째 요소 길이 = ${#array1}"
echo "array2 의 첫번째 요소 길이 = ${#array2}"
echo
echo "array0 의 요소 갯수 = ${#array0[*]}"  # 3
echo "array1 의 요소 갯수 = ${#array1[*]}"  # 1  (놀랍죠!)
echo "array2 의 요소 갯수 = ${#array2[*]}"  # 0

echo

exit 0  # Thanks, S.C.

${array_name[@]}${array_name[*]}의 관계는 $@ 와 $*의 관계와 비슷합니다. 이 강력한 배열 표기법은 쓸모가 아주 많습니다.

# 배열 복사.
array2=( "${array1[@]}" )

# 배열에 원소 추가.
array=( "${array[@]}" "새 원소" )
# 혹은
array[${#array[*]}]="새 원소"

# Thanks, S.C.

--

배열을 쓰면 쉘 스크립트에서도 아주 오래되고 익숙한 알고리즘을 구현할 수 있습니다. 이것이 반드시 좋은 생각인지 아닌지는 독자 여러분이 결정할 일입니다.

예 26-4. 아주 오래된 친구: 버블 정렬(Bubble Sort)

#!/bin/bash

# 불완전한 버블 정렬

# 버블 정렬 알고리즘을 머리속에 떠올려 보세요. 이 스크립트에서는...

# 정렬할 배열을 매번 탐색할 때 마다 인접한 두 원소를 비교해서 
# 순서가 다르면 두 개를 바꿉니다.
# 첫번째 탐색에서는 "가장 큰" 원소가 제일 끝으로 갑니다.
# 두번째 탐색에서는 두번째로 "가장 큰" 원소가 끝에서 두 번째로 갑니다.
# 이렇게 하면 각 탐색 단계는 배열보다 작은 수 만큼을 검색하게 되고,
# 뒤로 갈수록 탐색 속도가 빨라지는 것을 느낄 수 있을 겁니다.


exchange()
{
  # 배열의 두 멤버를 바꿔치기 합니다.
  local temp=${Countries[$1]} # 바꿔치기할 두 변수를 위한 임시 저장소
  Countries[$1]=${Countries[$2]}
  Countries[$2]=$temp
  
  return
}  

declare -a Countries  # 변수 정의, 밑에서 초기화 되기 때문에 여기서는 안 써도 됩니다.

Countries=(Netherlands Ukraine Zair Turkey Russia Yemen Syria Brazil Argentina Nicaragua Japan Mexico Venezuela Greece England Israel Peru Canada Oman Denmark Wales France Kashmir Qatar Liechtenstein Hungary)
# X로 시작하는 나라 이름은 생각이 안 나네요, 쩝...

clear  # 시작하기 전에 화면을 깨끗이 지우고...

echo "0: ${Countries[*]}"  # 0번째 탐색의 배열 전체를 보여줌.

number_of_elements=${#Countries[@]}
let "comparisons = $number_of_elements - 1"

count=1 # 탐색 횟수.

while [ $comparisons -gt 0 ]   # 바깥쪽 루프의 시작
do

  index=0  # 각 탐색 단계마다 배열의 시작 인덱스를 0으로 잡음

  while [ $index -lt $comparisons ] # 안쪽 루프의 시작
  do
    if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
    # 순서가 틀리면...
    # \> 가 아스키 비교 연산자였던거 기억나시죠?
    then
      exchange $index `expr $index + 1`  # 바꿉시다.
    fi  
    let "index += 1"
  done # 안쪽 루프의 끝
  

let "comparisons -= 1"
# "가장 큰" 원소가 제일 끝으로 갔기 때문에 비교횟수를 하나 줄일 필요가 있습니다.

echo
echo "$count: ${Countries[@]}"
# 각 탐색 단계가 끝나면 결과를 보여줍니다.
echo
let "count += 1"   # 탐색 횟수를 늘립니다.

done  # 바깥쪽 루프의 끝

# 끝!

exit 0

--

배열을 쓰면 에라토스테네스의 체(Sieve of Erastosthenes)의 쉘 스크립트 버전을 구현할 수 있습니다. 물론, 이렇게 철저히 리소스에 의존하는 어플리케이션은 C 같은 컴파일 언어로 쓰여져야 합니다. 이 쉘 스크립트 버전은 굉장히 느리게 동작합니다.

예 26-5. 복잡한 배열 어플리케이션: 에라토스테네스의 체(Sieve of Erastosthenes)

#!/bin/bash
# sieve.sh

# 에라토스테네스의 체(Sieve of Erastosthenes)
# 소수를 찾아주는 고대의 알고리즘.

# 이 스크립트는 똑같은 C 프로그램보다 두 세배는 더 느리게 동작합니다.

LOWER_LIMIT=1       # 1 부터.
UPPER_LIMIT=1000    # 1000 까지.
# (시간이 주체못할 정도로 남아 돈다면 이 값을 더 높게 잡아도 됩니다.)

PRIME=1
NON_PRIME=0

let SPLIT=UPPER_LIMIT/2
# 최적화:
# 오직 상한값의 반만 확인해 보려고 할 경우 필요.


declare -a Primes
# Primes[] 는 배열.


initialize ()
{
# 배열 초기화.

i=$LOWER_LIMIT
until [ "$i" -gt "$UPPER_LIMIT" ]
do
  Primes[i]=$PRIME
  let "i += 1"
done
# 무죄가 밝혀지기 전까지는 배열의 모든 값을 유죄(소수)라고 가정.
}

print_primes ()
{
# Primes[] 멤버중 소수라고 밝혀진 것들을 보여줍니다.

i=$LOWER_LIMIT

until [ "$i" -gt "$UPPER_LIMIT" ]
do

  if [ "${Primes[i]}" -eq "$PRIME" ]
  then
    printf "%8d" $i
    # 숫자당 8 칸을 줘서 예쁘게 보여줍니다.
  fi
  
  let "i += 1"
  
done

}

sift () # 소수가 아닌 수를 걸러냅니다.
{

let i=$LOWER_LIMIT+1
# 1 이 소수인 것은 알고 있으니, 2 부터 시작합니다.

until [ "$i" -gt "$UPPER_LIMIT" ]
do

if [ "${Primes[i]}" -eq "$PRIME" ]
# 이미 걸러진 숫자(소수가 아닌 수)는 건너뜁니다.
then

  t=$i

  while [ "$t" -le "$UPPER_LIMIT" ]
  do
    let "t += $i "
    Primes[t]=$NON_PRIME
    # 모든 배수는 소수가 아니라고 표시합니다.
  done

fi  

  let "i += 1"
done  


}


# 함수들을 순서대로 부릅니다.
initialize
sift
print_primes
# 이런것을 바로 구조적 프로그래밍이라고 한답니다.

echo

exit 0



# ----------------------------------------------- #
# 다음 코드는 실행되지 않습니다.

# 이것은 Stephane Chazelas 의 향상된 버전으로 실행 속도가 좀 더 빠릅니다.

# 소수의 최대 한계를 명령어줄에서 지정해 주어야 됩니다.

UPPER_LIMIT=$1                  # 명령어줄에서의 입력.
let SPLIT=UPPER_LIMIT/2         # 최대수의 중간.

Primes=( '' $(seq $UPPER_LIMIT) )

i=1
until (( ( i += 1 ) > SPLIT ))  # 중간까지만 확인 필요.
do
  if [[ -n $Primes[i] ]]
  then
    t=$i
    until (( ( t += i ) > UPPER_LIMIT ))
    do
      Primes[t]=
    done
  fi  
done  
echo ${Primes[*]}

exit 0

이 배열 기반의 소수 생성기와 배열을 쓰지 않는 예 A-11를 비교해 보세요.

--

배열의 "첨자"(subscript)를 능숙하게 조작하려면 임시로 쓸 변수가 있어야 합니다. 다시 말하지만, 이런 일이 필요한 프로젝트들은 펄이나 C 처럼 더 강력한 프로그래밍 언어의 사용을 고려해 보기 바랍니다.

예 26-6. 복잡한 배열 어플리케이션: 기묘한 수학 급수 탐색(Exploring a weird mathematical series)

#!/bin/bash

# Douglas Hofstadter 의 유명한 "Q-급수"(Q-series):

# Q(1) = Q(2) = 1
# Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), for n>2

# "무질서한" Q-급수는 이상하고 예측할 수 없는 행동을 보입니다.
# 이 급수의 처음 20개 항은 다음과 같습니다:
# 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12 

# Hofstadter 의 책, "Goedel, Escher, Bach: An Eternal Golden Braid",
# p. 137, ff. 를 참고하세요.


LIMIT=100     # 계산할 항 수
LINEWIDTH=20  # 한 줄에 출력할 항 수

Q[1]=1        # 처음 두 항은 1.
Q[2]=1

echo
echo "Q-급수 [$LIMIT 항]:"
echo -n "${Q[1]} "             # 처음 두 항을 출력
echo -n "${Q[2]} "

for ((n=3; n <= $LIMIT; n++))  # C 형태의 루프 조건.
do   # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]]  for n>2
# Bash 는 복잡한 배열 연산을 잘 처리할 수 없기 때문에
# 위의 식을 한번에 계산하지 않고 중간에 다른 항을 두어 계산할 필요가 있습니다.

  let "n1 = $n - 1"        # n-1
  let "n2 = $n - 2"        # n-2
  
  t0=`expr $n - ${Q[n1]}`  # n - Q[n-1]
  t1=`expr $n - ${Q[n2]}`  # n - Q[n-2]
  
  T0=${Q[t0]}              # Q[n - Q[n-1]]
  T1=${Q[t1]}              # Q[n - Q[n-2]]

Q[n]=`expr $T0 + $T1`      # Q[n - Q[n-1]] + Q[n - ![n-2]]
echo -n "${Q[n]} "

if [ `expr $n % $LINEWIDTH` -eq 0 ]    # 예쁜 출력
then   #    나머지
  echo # 각 줄이 구분되도록 해 줌.
fi

done

echo

exit 0

# 여기서는 Q-급수를 반복적으로 구현했습니다.
# 좀 더 직관적인 재귀적 구현은 독자들을 위해 남겨 놓겠습니다.
# 경고: 이 급수를 재귀적으로 계산하면 "아주" 긴 시간이 걸립니다.

--

bash는 1차원 배열만 지원합니다만, 약간의 속임수를 쓰면 다차원 배열을 흉내낼 수 있습니다.

예 26-7. 2차원 배열을 흉내낸 다음, 기울이기(tilting it)

#!/bin/bash
# 2차원 배열을 시뮬레이트.

# 2차원 배열은 열(row)을 연속적으로 저장해서 구현합니다.

Rows=5
Columns=5

declare -a alpha     # C 에서 
                     # char alpha[Rows][Columns];
					 # 인 것처럼. 하지만 불필요한 선언입니다.

load_alpha ()
{
local rc=0
local index


for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
do
  local row=`expr $rc / $Columns`
  local column=`expr $rc % $Rows`
  let "index = $row * $Rows + $column"
  alpha[$index]=$i   # alpha[$row][$column]
  let "rc += 1"
done  

# declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
# 라고 하는 것과 비슷하지만 이렇게 하면 웬지 2차원 배열같은 느낌이 들지 않습니다.
}

print_alpha ()
{
local row=0
local index

echo

while [ "$row" -lt "$Rows" ]   # "열 우선"(row major) 순서로 출력
                               # 열(바깥 루프)은 그대로고 행이 변함.
do                            
  local column=0
  
  while [ "$column" -lt "$Columns" ]
  do
    let "index = $row * $Rows + $column"
    echo -n "${alpha[index]} "  # alpha[$row][$column]
    let "column += 1"
  done

  let "row += 1"
  echo

done  

# 간단하게 다음처럼 할 수도 있습니다.
#   echo ${alpha[*]} | xargs -n $Columns

echo
}

filter ()     # 배열의 음수 인덱스를 걸러냄.
{

echo -n "  "  # 기울임(tilt) 제공.

if [[ "$1" -ge 0 &&  "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
then
    let "index = $1 * $Rows + $2"
    # 이제, 회전(rotate)시켜 출력.
    echo -n " ${alpha[index]}"  # alpha[$row][$column]
fi    

}
  



rotate ()  # 배열 왼쪽 아래를 기준으로 45도 회전.
{
local row
local column

for (( row = Rows; row > -Rows; row-- ))  # 배열을 뒤에서부터 하나씩 처리.
do

  for (( column = 0; column < Columns; column++ ))
  do

    if [ "$row" -ge 0 ]
    then
      let "t1 = $column - $row"
      let "t2 = $column"
    else
      let "t1 = $column"
      let "t2 = $column + $row"
    fi  

    filter $t1 $t2   # 배열의 음수 인덱스를 걸러냄.
  done

  echo; echo

done 

# 배열 회전(array rotation)은 Herbert Mayer 가 쓴
# "Advanced C Programming on the IBM PC"에 나온 예제(143-146 쪽)에서
# 영감을 받아 작성했습니다(서지사항 참고).

}


#-----------------------------------------------------#
load_alpha     # 배열을 읽고,
print_alpha    # 출력한 다음,
rotate         # 반시계 방향으로 45도 회전.
#-----------------------------------------------------#


# 이 스크립트는 예제를 위한 예제이기 때문에 약간 어색한 면이 있습니다.
#
# 독자를 위한 연습문제 1:
# 배열을 읽어 들이고 출력하는 함수를 
# 좀 더 교육적이고 우아하게 다시 작성해 보세요.
#
# 연습문제 2:
# 배열 회전 함수가 어떻게 동작하는지 알아내 보세요.
# 힌트: 배열의 역인덱싱이 의미하는 바가 뭘까요?

exit 0