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 |