Arrays

쉘 스크립트에서 array 는 프로그래밍 언어에서처럼 주요 기능이 아닙니다. 실제 스크립트를 작성하는데 있어서 array 가 잘 사용되지도 않습니다. 따라서 array 는 POSIX 에 정의되어있지 않으며 sh 에서는 사용할 수 없습니다. 하지만 interactive shell 에 사용되는 bash 의 경우는 select 문이나 사용자 정의 command completion 에서 array 가 사용되므로 참고삼아 보면 될 것 같습니다.

array 변수는 export 할 수 없습니다.

Array 만들기

array 는 index 로 숫자를 사용하는 indexed array 와 스트링을 사용할수 있는 associative array 가 있습니다. indexed array 는 별다른 설정없이 사용할수 있으나 associative array 는 declare -A 변수명 으로 먼저 선언을 해줘야 합니다. index 에는 매개변수 확장, 명령 치환, 산술 확장 을 모두 사용할 수 있습니다.

############### indexed array ################

AA=( 11 "hello array" 22 )
BB=( /usr/bin/x* /usr/bin/y* )
CC=( {001..100} )

idx=2                # index 값으로 사용되는 변수
AA=( [0]=11 [1]="hello array" [idx]=22 )

AA[0]=11
AA[1]="hello array"
AA[idx]=22           # indexed array 는 변수를 사용할때 '$' 를 붙이지 않아도 된다.
..........................

$ echo ${AA[2]} ${AA[idx]} 
22 22

############# associative array ##############

declare -A BB        # 먼저 declare -A 로 선언

idx=ef               # index 값으로 사용되는 변수
BB=( [ab]=11 [cd]="hello array" [$idx]=22 )

BB[ab]=11
BB[cd]="hello array"
BB[$idx]=22          # associative array 는 변수를 사용할때 '$' 를 붙여야 한다.

key1=foo key2=bar key3=zoo value=100
BB[$key1,$key2,$key3]=$value
.....................................

$ echo ${BB[$key1,$key2,$key3]}
100

만약에 indexed array 의 index 로 문자를 사용하게 되면 [0] 에 할당됩니다. 따라서 indexed array 를 바로 associative array 로 사용할 수는 없고 먼저 unset 을 한후 associative array 로 만들어 주어야 합니다.

$ CC=(11 22 33)    # indexed array
$ declare -p CC
declare -a CC=([0]="11" [1]="22" [2]="33")

$ CC[hello]=100    # index 로 문자를 사용하면 [0] 에 할당된다.
$ declare -p CC
declare -a CC=([0]="100" [1]="22" [2]="33")

$ unset -v CC      # 먼저 unset 한후 associative array 로 지정
$ declare -A CC
$ CC[hello]=100
$ declare -p CC
declare -A CC=([hello]="100" )

Array 값 조회하기

$ AA=(apple banana orange)

$ declare -p AA
declare -a AA='([0]="apple" [1]="banana" [2]="orange")'

현재 shell 에 정의된 모든 array 변수명 보기

$ compgen -A arrayvar

Array 값의 사용

array 값을 사용할 때는 반드시 { } 를 사용해야 합니다. 만약에 index 가 [2] 인 원소를 사용하기 위해 $AA[2] 와 같이 한다면 먼저 $AA 가 변수 확장이 되어 foo 가 되고 이어 foo[2] 에서 globbing 이 일어나게 됩니다.

$ AA=(foo bar zoo)

# 만약 현재 디렉토리에 'foo2' 라는 파일이 있다면 globbing 이 일어나 결과는 'foo2' 가된다.
$ echo $AA[2]   
foo[2]

$ echo ${AA[2]}   # 반드시 '{ }' 를 사용해야 한다.
zoo

indexed array 에서는 기본적으로 index 에서 산술연산이 가능합니다.
associative array 경우는 index 값으로 스트링을 사용하므로 안됩니다.

$ AA=(11 22 33 44 55)
$ idx=2

$ echo ${AA[idx]}
33
$ echo ${AA[ idx + 1 ]}
44
$ echo "${AA[ idx == 2 ? 3 : 4 ]}"
44

# index 값으로 음수를 사용할수도 있습니다.
$ echo ${AA[-1]}
55
$ echo ${AA[-2]}
44

associative array 에서는 index 값에 공백의 포함 여부에 따라 각각 다른값으로 인식하므로 주의할 필요가 있습니다. ( index 에서 globbing 은 발생하지 않습니다.)

$ declare -A CC

$ CC[$(echo aaa)]=100
$ CC[ $(echo aaa)]=200
$ CC[ $(echo aaa) ]=300     # 공백으로 인해 각각 다른 값으로 인식

$ declare -p CC
declare -A CC=([aaa]="100" [" aaa "]="300" [" aaa"]="200" )

array 변수를 index 없이 일반 변수처럼 사용할 수도 있는데 이때는 기본적으로 index 가 [0] 인 원소가 됩니다. 이것은 associative array 도 마찬가지 입니다.

$ foo=100

$ echo $foo ${foo[0]}      # 변수 $foo 의 값은 ${foo[0]} 와도 같다.
100 100
---------------------------------------------------

# indexed array                 # associative array
                                $ declare -A BB

$ AA=( 11 22 33 )               $ BB=( [ab]=11 [cd]=22  )

$ echo $AA                      $ echo $BB  # 현재 index 가 '[0]' 인 원소는 없으므로
11                              $      
                                $ BB=100    # index 가 '[0]' 인 원소가 생성된다.
$ AA=100                        
                                $ echo $BB
$ echo $AA                      100
100                             
                                $ declare -p BB   # 100 은 '[0]' 에 할당된다.
$ echo ${AA[@]}                 declare -A BB=([0]="100" [ab]="11" [cd]="22" )
100 22 33                       
                                $ echo ${BB[@]}
                                100 11 22

@ , * 차이점

Double quotes 을 하지 않을 경우 ${array[@]}${array[*]} 는 차이가 없습니다. 왜냐하면 똑같이 IFS 값에 의해 단어 분리가 되기 때문입니다. 하지만 quote 하였을 경우는 @* 는 의미가 달라집니다. "array[@]" 는 array 개개의 원소를 " " 로 quote 하여 나열하는 것과 같고, "array[*]" 는 array 의 모든 원소를 하나의 " " 안에 IFS 변수값을 구분자로 하여 넣는 것과 같습니다.

Quote 여부 의미
${AA[@]}
${AA[*]}
quote 하지 않으면 둘은 차이가 없다
"${AA[@]}" "${AA[0]}" "${AA[1]}" "${AA[2]}" ...
"${AA[*]}" "${AA[0]}X${AA[1]}X${AA[2]}..."
여기서 'X' 는 IFS 변수값 중에 첫 번째 문자
# 예를 위해 공백을 5 개씩 주었다.
$ AA=( "Arch     Linux" "Ubuntu     Linux" "Fedora     Linux" )  
$ echo ${#AA[@]}
3

# quote 하지 않아 IFS 값에 의해 단어분리가 일어나 공백이 유지되지 않고 
# for 문을 돌려도 6 개로 나온다
$ echo ${AA[@]}                           # 연이은 공백이 하나의 공백으로 바뀌였다.
Arch Linux Ubuntu Linux Fedora Linux      

$ for v in ${AA[@]}; do echo "$v"; done   # 원소개수가 6 개로 나온다
Arch
Linux
Ubuntu
Linux
Fedora
Linux

$ echo ${AA[*]}                           # 마찬가지다.
Arch Linux Ubuntu Linux Fedora Linux

$ for v in ${AA[*]}; do echo "$v"; done
Arch
Linux
Ubuntu
Linux
Fedora
Linux

################# quote 하였을 경우 #################

$ echo "${AA[@]}"                        
Arch     Linux Ubuntu     Linux Fedora     Linux   # 공백이 유지된다.

$ echo "${AA[*]}"
Arch     Linux Ubuntu     Linux Fedora     Linux

$ for v in "${AA[@]}"; do echo "$v"; done   # "array[@]" 는 원소 개수가 유지된다.
Arch     Linux
Ubuntu     Linux
Fedora     Linux

$ for v in "${AA[*]}"; do echo "$v"; done   # "array[*]" 는 원소 개수가 1 개가 된다.
Arch     Linux Ubuntu     Linux Fedora     Linux

${arr[*]} 값을 변수에 대입할 경우 해당 변수도 ${arr[*]} 와 같이 출력됩니다.

$ IFS=XYZ
$ arr=(11 22 33 44 55)

$ echo "${arr[*]}" , ${arr[*]}                 $ echo "${arr[@]}" , ${arr[@]}
11X22X33X44X55 , 11 22 33 44 55                11 22 33 44 55 , 11 22 33 44 55

$ foo=${arr[*]}                                $ bar=${arr[@]}

# 변수 $foo 도 ${arr[*]} 와 같이 출력된다.
$ echo "$foo" , $foo                           $ echo "$bar" , $bar
11X22X33X44X55 , 11 22 33 44 55                11 22 33 44 55 , 11 22 33 44 55

$ IFS=YZ                             # IFS 변수값 변경

$ echo "${arr[*]}" , ${arr[*]}
11Y22Y33Y44Y55 , 11 22 33 44 55   

$ echo "$foo" , $foo                 # IFS 값이 변경되면 기존의 $foo 변수값은
11X22X33X44X55 , 11X22X33X44X55      # 인수 구분자가 X 로 고정된다.

-----------------------------------------------------------------

# array 값을 특정 문자로 join 해서 출력하려면 다음과같이 하면 됩니다.
$ echo $( IFS=':'; echo "${arr[*]}" ) 
11:22:33:44:55

명령을 실행할때 ${arr[@]} 를 인수에 포함시키면 ?

$ AA=(11 22 33 44 55)
$ args.sh "aa ${AA[@]} bb"  
$1 : aa 11
$2 : 22                # 인수가 분리된다.
$3 : 33
$4 : 44
$5 : 55 bb

------------

$ args.sh "aa ${AA[*]} bb"
$1 : aa 11 22 33 44 55 bb

IFS 값을 이용해 단어를 분리하여 원소를 설정하는 것은 quote 을 하지 않았을 때 적용됩니다.

$ AA=( aa:bb cc:dd ee:ff )

# ${AA[*]} 를 quote 하지 않으면 IFS=: 에의해 원소가 6 개로 분리된다.
# ${AA[*]} 는 array 이므로 원소단위로 분리가 됩니다.
$ ( IFS=:; BB=( ${AA[*]} ); echo "${BB[0]}" "${#BB[@]}" )
aa 6

# ${AA[@]} 를 quote 하지 않으면 *, @ 는 차이가 없다.
$ ( IFS=:; BB=( ${AA[@]} ); echo "${BB[0]}" "${#BB[@]}" )
aa 6

$ ( IFS=:; BB=( "${AA[*]}" ); echo "${BB[0]}" )    # quote 을 하여 원소가 1 개가됨
aa:bb:cc:dd:ee:ff

$ ( BB=( "${AA[@]}" ); echo "${BB[0]}" )           # quote 을 하여 원소가 3 개가됨
aa:bb

특수 표현식

표현식 의미
${#array[@]}
${#array[*]}
array 전체 원소의 개수를 나타냄
${#array[N]}
${#array[string]}
indexed array 에서 N 번째 원소의 문자수 ( stringlength ) 를 나타냄
associative array 에서 index 가 string 인 원소의 문자수를 나타냄
${array[@]}
${array[*]}
array 전체 원소의 value 값만 추출됨
${!array[@]}
${!array[*]}
array 전체 원소의 index 값만 추출됨
${!name@}
${!name*}
name 으로 시작하는 이름을 갖는 모든 변수를 나타냄
예) echo "${!BASH@}"

Array iteration 하기

########## indexed array #########

ARR=(11 22 33)

for idx in ${!ARR[@]}; do
    echo ARR index : $idx, value : "${ARR[idx]}"      # 또는 ${ARR[$idx]}
done

ARR index : 0, value : 11
ARR index : 1, value : 22
ARR index : 2, value : 33

######## associative array ########

declare -A ARR

ARR=( [ab]=11 [cd]="hello array" [ef]=22 )

for idx in "${!ARR[@]}"; do
    echo ARR index : "$idx", value : "${ARR[$idx]}"
done

ARR index : ef, value : 22
ARR index : cd, value : hello array
ARR index : ab, value : 11

Array 대입과 복사

array AA 를 BB 에 대입할경우 BB=${AA[@]} 와 같이 하면 array 가 아니라 AA 값 전체가 BB 변수에 대입됩니다. array 대입을 하려면 항상 =( ) 를 사용해야 합니다.

$ AA=( 11 22 33 )

$ BB=${AA[@]}           
$ echo ${#BB[@]} : ${BB[1]}  # 정상적으로 array 대입이 되지 않는다.
1 :                       

$ echo "$BB"                 # array AA 의 전체 값이 $BB 에 대입된다.
11 22 33

$ BB=( "${AA[@]}" )          # array 대입은 항상 '=( )' 를 사용해야 한다 
$ echo ${#BB[@]} : ${BB[1]}
3 : 22

array 전체를 복사할 때는 다음과 같은 방법도 있습니다.

$ declare -A AA BB

$ AA=( [ab]=11 [cd]="hello       world" [ef]=22 )

$ declare -p AA
declare -A AA=([ab]="11" [cd]="hello       world" [ef]="22" )

$ tmp=$(declare -p AA); eval "${tmp/declare -A AA=/declare -A BB=}"

$ declare -p BB
declare -A BB=([ab]="11" [cd]="hello       world" [ef]="22" )

Array 원소 삭제하기

명령 의미
array=()
unset -v array
unset -v "array[@]"
array 전체를 삭제
unset -v "array[N]" indexed array 에서 N 번째 원소를 삭제
unset -v "array[string]" associative array 에서 index 가 string 인 원소를 삭제

array 원소 전체를 삭제하는 방법에는 array=()unset 두 가지가 있는데 어떤 경우이건 실행이 되면 변수는 존재하지 않는 상태로 되고 매개변수 확장도 그에 따라 처리됩니다.

# array=() 의 경우                            # unset 의 경우
$ AA=( 1 2 3 )                               $ BB=( 1 2 3 )
$ [ -v AA ]; echo $?                         $ [ -v BB ]; echo $?
0                                            0
$ AA=()                                      $ unset -v BB
$ [ -v AA ]; echo $?                         $ [ -v BB ]; echo $?
1                                            1

하지만 차이점이 있는데 array=() 는 전체 원소가 삭제되지만 변수 정보는 남아있는 상태가 되어 associative array 같은 속성이 유지가 됩니다. 반면에 unset 은 변수가 완전히 없어지는 것과 같습니다.

# array=() 의 경우                             # unset 의 경우
$ declare -A CC=( [ab]=11 )                   $ declare -A DD=( [ab]=11 )
$ CC=()                                       $ unset -v DD
$ declare -p CC                               $ declare -p DD
declare -A CC=()                              bash: declare: DD: not found
$ CC+=( 22 )                                  $ DD+=( 22 )
bash: CC: 22: must use subscript              $
when assigning associative array
# associative array 속성이 남아 있어서 오류가 된다.

index 정렬 하기

array 원소를 삭제하면 자동으로 ${#array[@]} 개수에도 반영이 되고 for 문을 돌려도 삭제된 원소는 나타나지 않습니다. 그러나 삭제된 원소의 index 는 그대로 남아있고 뒤에오는 원소들의 index 값들도 바뀌지 않습니다. 이때는 다시 array 를 재할당하면 index 가 정렬됩니다.

$ AA=(11 22 33 44 55)

$ unset -v "AA[2]"

$ echo ${#AA[@]}                           # 삭제후 원소 개수가 정상적으로 반영됨
4                                      

$ for v in "${AA[@]}"; do echo "$v"; done  
11                                         # for 문에서도 정상적으로 반영됨.
22
44         
55

$ echo ${AA[1]} :  ${AA[2]} : ${AA[3]}     # 그런데 삭제된 index 2 가 공백으로 남아있다!
22 : : 44                              

$ AA=( "${AA[@]}" )                        # 재할당 하면 index 가 정렬된다
$ echo ${AA[1]} :  ${AA[2]} : ${AA[3]}
22 : 44 : 55

삭제된 원소의 index 값이 남는 이유는 다음과 같이 index 를 이용해 array 순회도중에 원소를 삭제할 수 있기 때문입니다. 이때는 또한 ${#AA[@]} 값을 직접 순회에 사용해서는 안됩니다. ( 그러면 첫번째와 같이 원소가 모두 표시되지 않습니다.)

$ AA=(11 22 33 44 55)                           $ AA=(11 22 33 44 55) num=${#AA[@]}
$ for (( i = 0; i < ${#AA[@]}; i++ )); do       $ for (( i = 0; i < num; i++ )); do
    (( i == 1 )) && unset -v "AA[i+1]"              (( i == 1 )) && unset -v "AA[i+1]"
    echo ${AA[i]}                                   echo ${AA[i]}
done                                            done
11                                              11
22                                              22

44                                              44
                                                55

null 값을 가지고 있는 원소 삭제하기

unset 한 원소는 존재하지 않는 원소인 반면에 null 값을 가지고 있는 원소는 존재하고 있는 원소입니다.
다음과 같이 삭제할 수 있습니다.

$ AA=":arch linux:::ubuntu linux::::fedora linux::"

$ IFS=: read -a ARR <<<  "$AA"

$ echo ${#ARR[@]}
10
$ echo ${ARR[0]}

$ echo ${ARR[1]}
arch linux

# ARR 에서 null 값을 가지고 있는 원소 삭제하기

$ IFS=                              # 먼저 IFS 값을 null 로 설정
$ ARR=( ${ARR[*]} )                 # quotes 을 사용하지 않는다.
$ unset -v IFS

$ echo ${#ARR[@]}
3
$ echo "${ARR[0]},${ARR[1]},${ARR[2]}"
arch linux,ubuntu linux,fedora linux

Array 원소 추출하기

array 의 특정 원소들을 추출하려고 할때는 ${array[@]:offset:length} 형식을 사용합니다.

$ AA=( Arch Ubuntu Fedora Suse Mint );

$ echo "${AA[@]:2}"
Fedora Suse Mint

$ echo "${AA[@]:0:2}"
Arch Ubuntu

$ echo "${AA[@]:1:3}"
Ubuntu Fedora Suse

Array 원소 추가하기

array 에 원소를 추가할 땐 +=( ) 를 사용할 수 있습니다.

$ AA=( "Arch Linux" "Ubuntu Linux" "Suse Linux" )
$ echo ${#AA[@]}
3

$ AA+=( "Fedora Linux" )

$ echo "${AA[@]}"
Arch Linux Ubuntu Linux Suse Linux Fedora Linux

$ echo ${#AA[@]}
4

#######################################

$ declare -A BB=( [aa]="Arch Linux" [bb]="Ubuntu Linux" [cc]="Suse Linux" )
$ echo ${#BB[@]}
3

$ BB+=( [dd]="Fedora Linux" )

$ echo ${#BB[@]}
4

#######################################

$ AA=( "Arch Linux" Ubuntu Fedora )

$ AA=( "${AA[@]}" AIX HP-UX )

$ echo "${AA[@]}"
Arch Linux Ubuntu Fedora AIX HP-UX

$ echo ${#AA[@]}
5

전체 array 원소에 패턴을 적용하기

패턴을 적용하여 매칭되는 부분을 바꾸거나 삭제할 수 있습니다. 패턴에는 맨 앞을 가리키는 #, 맨 뒤를 가리키는 % anchor 를 사용할 수 있습니다.

$ AA=( "Arch Linux" "Ubuntu Linux" "Suse Linux" "Fedora Linux" )

# 전체 원소들의 'u' 문자가 'X' 로 바뀐것을 볼 수 있습니다.
# 그런데 Ubuntu Linux 와 Suse Linux 에서는 첫자만 바뀌고 나머지는 바뀌지 않았습니다.
$ echo "${AA[@]/u/X}"
Arch LinXx UbXntu Linux SXse Linux Fedora LinXx

# 원소 전체에 적용하려면 '//pattern' 을 사용합니다.
$ echo "${AA[@]//u/X}"
Arch LinXx UbXntX LinXx SXse LinXx Fedora LinXx

# Su* 패턴과 매칭되는 원소가 없어졌습니다.
# 이것은 원소가 삭제된것이 아니라 공백으로 치환된 것입니다.
$ echo "${AA[@]/Su*/}"
Arch Linux Ubuntu Linux Fedora Linux

$ AA=( "${AA[@]/Su*/}" )

$ echo ${#AA[@]}                    # 원소개수가 4 개로 그대로다.
4

$ for v in "${AA[@]}"; do echo "$v"; done
Arch Linux
Ubuntu Linux
                                   # index 2 는 공백으로 나온다.
Fedora Linux

패턴을 이용해 매칭되는 원소 삭제하기

$ AA=( "Arch Linux" "Ubuntu Linux" "Suse Linux" "Fedora Linux" )

# "${AA[*]}" 는 "elem1Xelem2Xelem3X..." 와 같습니다. ('X' 는 IFS 변수의 첫번째 값)
# 그러므로 IFS 값을 '\n' 바꾸고 echo 한것을 명령치환 값으로 보내면 
# '\n' 에의해 원소들이 분리되어 array 에 저장되게 됩니다.

$ IFS=$'\n'
$ AA=( $(echo "${AA[*]/Su*/}") )
$ unset -v IFS                    # array 입력이 완료되었으므로 IFS 값 복구

$ echo ${#AA[@]}                  # 삭제가 반영되어 원소개수가 3 개로 나온다
3

$ echo "${AA[1]},${AA[2]}"        # index 도 정렬되었다.
Ubuntu Linux,Fedora Linux

맨앞을 가리키는 #, 맨뒤를 가리키는 % anchor 의 사용

# 각 원소에서 'Linux' 의 위치가 다르다.
$ AA=( "Linux Arch" "Ubuntu Linux" "Suse Linux" "Linux Fedora" )

# 'Linux' 가 맨앞에 위치한 경우만 변경됨
$ echo "${AA[@]/#Linux/XXX}" 
XXX Arch Ubuntu Linux Suse Linux XXX Fedora

# 'Linux' 가 맨뒤에 위치한 경우만 변경됨
$ echo "${AA[@]/%Linux/XXX}" 
Linux Arch Ubuntu XXX Suse XXX Linux Fedora

# 각 원소의 맨앞에 '::' 가 추가된다.
$ echo "${AA[@]/#/::}" 
::Linux Arch ::Ubuntu Linux ::Suse Linux ::Linux Fedora

# 각 원소의 맨뒤에 '::' 가 추가된다.
$ echo "${AA[@]/%/::}" 
Linux Arch:: Ubuntu Linux:: Suse Linux:: Linux Fedora::

# 각 원소의 맨앞 2 문자가 삭제된다.
$ echo "${AA[@]/#??/}" 
nux Arch untu Linux se Linux nux Fedora

# 각 원소의 맨뒤 2 문자가 삭제된다.
$ echo "${AA[@]/%??/}" 
Linux Ar Ubuntu Lin Suse Lin Linux Fedo

스트링에서 특정 문자를 구분자로 하여 필드 분리하기.

$ AA="Arch Linux:Ubuntu Linux:Suse Linux:Fedora Linux"

$ IFS=: read -a ARR <<< "$AA"
$ echo ${#ARR[@]}
4
$ echo "${ARR[1]}"
Ubuntu Linux

---------------------------------------------------------------------
# 입력되는 원소값에 glob 문자가 있을경우 globbing 이 발생할수 있으므로 noglob 옵션 설정
$ set -f; IFS=:             # IFS 값을 ':' 로 설정
$ ARR=( $AA )
$ set +f; unset -v IFS      # array 입력이 완료되었으므로 IFS 값 복구

$ echo ${#ARR[@]}
4
$ echo "${ARR[1]}"
Ubuntu Linux

array 를 사용할 수 없는 sh 에서는 다음과 같이 할 수 있습니다.

$ set -f; IFS=:             # set -f 옵션을 이용해 globbing 을 방지
$ set -- $AA                # IFS 값에 따라 원소들을 분리하여 
$ set +f; unset -v IFS      # positional parameters 에 할당

$ echo $#
4
$ echo "$2"
Ubuntu Linux

Array 원소 정렬

따로 array 정렬 기능을 제공하는 것은 아니지만 다음과 같은 방법을 사용하여 정렬할 수 있습니다.

$ arr=( 11 44 22 55 33 )

$ arr=( $( IFS=$'\n'; echo "${arr[*]}" | sort -n ) )

$ echo ${#arr[@]}; echo "${arr[@]}"
5
11 22 33 44 55
--------------------------------------------------

$ arr2=( "Fedora Linux" "Ubuntu Linux" "Suse Linux" "Arch Linux" )

$ IFS=$'\n'
$ arr2=( $( echo "${arr2[*]}" | sort ) )
$ unset -v IFS

$ echo ${#arr2[@]}; echo "${arr2[@]}"
4
Arch Linux Fedora Linux Suse Linux Ubuntu Linux

파일 라인을 array 로 읽어들이기

$ cat datafile 
100  Emma    Thomas
200  Alex    Jason
300  Madison Randy

$ mapfile -t arr < datafile 

$ echo "${arr[0]}"
100  Emma    Thomas

$ echo "${arr[1]}"
200  Alex    Jason

Array index 와 globbing

array 에서 index 에 사용되는 [ ] 문자는 globbing 에서 사용하는 glob 문자와 겹칩니다. 그러므로 사용에 주의해야 될 점이 있습니다. 다음과 같은 경우 unset 명령을 실행할때 [ ] 에서 globbing 이 일어나 정상적으로 unset 이 되지 않고 있습니다.

$ array=( [10]=100 [11]=200 [12]=300 )
$ echo ${array[12]}
300

$ touch array1               # 현재 디렉토리에 임의로 array1 파일생성

# unset 을 실행하였으나 globbing 에의해 array[12] 이 array1 파일과 매칭이되어 
# 실질적으로 unset array1 명령과 같게되어 unset 이 되지 않고 있습니다.
$ unset -v array[12]         
$ echo ${array[12]}         
300                       

$ unset -v 'array[12]'       # globbing 을 방지하기 위해 quote.
$ echo ${array[12]}          # 이제 정상적으로 unset 이됨
$
-----------------------------------------------------------------------

# 대입 연산에서는 기본적으로 단어분리, globbing 이 발생하지 않습니다.

AA="400 500"
array[12]=$AA                    array[12]=*abc*

Array 대입 연산시 오류가 발생할 경우

Array 대입 연산을 할 때 =( ) 안에서 명령 치환을 사용할 수 있는데 이때 명령 치환에서 오류가 발생할 경우 명령 치환을 이용한 대입 연산에서와 같이 동일하게 처리됩니다.

#!/bin/bash
echo start ...
IFS=$'\n'
files=( $( cd xxx && find /usr/bin -type f ) )    # xxx 는 존재하지 않는 디렉토리
unset -v IFS
echo end ...

$ ./test.sh
start ...
./test.sh: line 5: cd: xxx: No such file or directory
end ...          # 실행중 오류가 발생했지만 끝까지 실행되고 

$ echo $?        # 종료 상태 값도 0 이 된다.
0
--------------------------------------------------

#!/bin/bash
echo start ...
IFS=$'\n'
files=( $( cd xxx && find /usr/bin -type f ) ) || exit 3    # "|| exit 3" 을 추가
unset -v IFS
echo end ...

$ ./test.sh 
start ...
./test.sh: line 5: cd: xxx: No such file or directory    # exit 3 에의해 실행이 중단된다.

$ echo $?
3
--------------------------------------------------

#!/bin/bash -e
echo start ...            # shebang 라인에 errexit 옵션 설정 
IFS=$'\n'
files=( $( cd xxx && find /usr/bin -type f ) )
unset -v IFS
echo end ...

$ ./test.sh 
start ...
./t.sh: line 5: cd: xxx: No such file or directory   # errexit 옵션에 의해 실행이 중단된다.

$ echo $?
1

Quiz

다음 AA array 가 Suse Linux 원소를 가지고 있는지는 어떻게 체크할 수 있을까요?

$ AA=( "Arch Linux" "Ubuntu Linux" "Suse Linux" "Fedora Linux" )

------------------------------------

# 첫번째는 grep 명령을 이용하는 방법입니다.

if printf '%s\n' "${AA[@]}" | grep -xq 'Suse Linux'; then
    echo yes
fi

------------------------------------

# 두번째는 for 문을 이용하는 방법입니다.

for elem in "${AA[@]}"; do
    if test "$elem" = "Suse Linux"; then
        echo "yes"
    fi
done

-------------------------------------

# 세번째는 매개변수확장 기능에서 substring removal 을 이용하여 해당 원소를 삭제한후
# 결과를 BB 에 대입하고 AA 와 BB 의 원소개수를 비교합니다.

IFS=$'\n'; BB=( `echo "${AA[*]#Suse Linux}"` ); unset -v IFS

if test ${#AA[@]} -ne ${#BB[@]}; then
    echo "yes"
fi

2 .

다음 AA array 에서 Suse Linux 원소를 삭제하려면 어떻게 할까요?

AA=( "Arch Linux" "Ubuntu Linux" "Suse Linux" "Fedora Linux" )

for (( i = 0; i < ${#AA[@]}; i++)); do 
    if [[ ${AA[i]} == "Suse Linux" ]]; then
        unset -v "AA[i]"
    fi
done

AA=( "${AA[@]}" )       # array 원소 삭제후 index 정렬

echo "number of array : " ${#AA[@]}
echo "${AA[0]},${AA[1]},${AA[2]}"

------------------------------------------

$ ./test.sh 
number of array :  3                        # 원소 개수도 맞고
Arch Linux,Ubuntu Linux,Fedora Linux        # index 도 올바로 정렬되었다.

associative array 는 주로 index 값을 이용해 값을 읽고, 삭제하지만 다음은 value 값을 검색해서 매칭이 될경우 삭제하는 예입니다.

declare -A AA=( 
    [aa]="Arch Linux"
    [bb]="Ubuntu Linux"
    [cc]="Suse Linux"
    [dd]="Fedora Linux" 
)

for idx in "${!AA[@]}"; do
    if [ "${AA[$idx]}" = "Suse Linux" ]; then 
        unset -v "AA[$idx]"                     # associative array 는 index 로 사용되는
    fi                                          # 변수에 '$' 문자를 사용해야 하고
done                                            # 원소 삭제후 따로 index 정렬이 필요 없습니다.

echo "number of array : " ${#AA[@]}
echo "${AA[aa]},${AA[bb]},${AA[cc]},${AA[dd]}"

---------------------------------------------

$ ./test.sh 
number of array :  3                            # 원소 개수도 맞고
Arch Linux,Ubuntu Linux,,Fedora Linux           # ${AA[cc]} 원소도 정상적으로 삭제되었다.

3 .

line='aaa bbb "foo bar" ccc' 와 같은 라인을 분리해서 array 로 만들려고 합니다. 이때 quotes 으로 둘러싸인 "foo bar" 가 하나의 원소가 되게 하려면 어떻게 할까요?

$ line='aaa bbb "foo bar" ddd'

$ arr=( $line )

$ echo ${#arr[@]}       # 기본적으로 IFS=$' \t\n' 에의해 분리되므로
5                       # array 의 원소가 5 개가 된다.

$ echo ${arr[2]}
"foo

$ echo ${arr[3]}
bar"

이때는 eval 을 사용하면 quotes 을 해석해서 분리할 수 있습니다.

$ line='aaa bbb "foo bar" ccc'

$ echo arr=\( $line \)
arr=( aaa bbb "foo bar" ddd )

$ eval arr=( $line )

$ echo ${#arr[@]}
4

$ echo ${arr[2]}
foo bar

4 .

변수가 array 인지 아닌지는 어떻게 체크할 수 있을까요?

declare -p 변수명 의 출력값을 이용하여 판단할 수 있습니다.

$ AA=100
$ declare -p AA
declare -- AA="100"                                # 일반 변수는 '--'

$ BB=( 1 2 3 )
$ declare -p BB
declare -a BB=([0]="1" [1]="2" [2]="3")            # indexed array 는 '-a'

$ declare -A CC
$ CC=( [ab]=100 [cd]=200 [ef]=300 )
$ declare -p CC
declare -A CC=([ab]="100" [cd]="200" [ef]="300" )  # associative array 는 '-A'

$ declare -i DD=100
$ declare -p DD
declare -i DD="100"                                # integer 속성 변수는 '-i'
----------------------------------------------------------------------------

$ type_check() {
    [[ `declare -p $1` =~ ^declare\ (..) ]] &&
    case ${BASH_REMATCH[1]} in
        --) echo normal variable ;;
        -a) echo indexed array ;;
        -A) echo associative array ;;
        -i) echo integer variable ;;
        *) echo other ... ;;
    esac
}

$ type_check AA 
normal variable

$ type_check BB
indexed array

그런데... bash 4.3 부터는 named reference 기능이 추가되었습니다. 따라서 named reference 일 경우 다음과 같이 원본 변수까지 loop 처리를 해주어야 합니다.

$ declare AA=( 1 2 3 )
$ declare -n AA2=AA
$ declare -n AA3=AA2

$ declare -p AA3
declare -n AA3="AA2"
$ declare -p AA2
declare -n AA2="AA"
$ declare -p AA
declare -a AA=([0]="1" [1]="2" [2]="3")
-------------------------------------------------

type_check() {
    local var reg 
    var=$( declare -p "$1" ) && {
        reg='^declare -n [^=]+="([^"]+)"$'
        while [[ $var =~ $reg ]]; do
            var=$( declare -p "${BASH_REMATCH[1]}" )
        done

        case ${var#declare -} in
            -*) echo normal variable ;;
            a*) echo indexed array ;;
            A*) echo associative array ;;
            i*) echo integer variable ;;
            *)  echo other ... ;;
        esac
    }
}

또는 매개변수 확장 기능을 이용할 수 있습니다.