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 |