bashの配列変数に関するTips

序文(飛ばしてよし)

まず話は僕が書いてたスクリプトで、引数から"最後の"値だけを分離したものを使いたかったってのが始まりです。
最初の引数の分離なら shift でいけるんだが、最後をいきなり分離するのが直接は出来ないっぽいので、格闘の結果以下のように解決しました。

#!/bin/sh
ARGS=("$@")
echo "最後以外: ${ARGS[@]:0:((${#ARGS[@]}-1))}"
echo "最後のみ: ${ARGS[((${#ARGS[@]}-1))]}"
echo "最後のみ: ${ARGS[-1]}" # bash-4.2 以降なら負のインデックスが使える!

以下が上記スクリプトの実行結果です。

$ ./test.sh a b c d
最後以外: a b c
最後のみ: d

やっていることは、まず引数をARGVという配列変数に詰めなおして、その後配列変数に対するスライスや引数処理で欲しい値を取得してます。
配列変数なんて滅多に扱わないだろうし特有の表現もあるしで、詳しくない人は意味不明ですよね(^^;
なので備忘録としてここで使ってるTipsを以下に纏めておきます。

よく考えたら配列に詰め直す必要なかったわ
#!/bin/sh
echo "最後以外: " "${@:1:$#-1}"
echo "最後のみ: " "${@:$#}"

引数処理だと$1に1番目の引数が入ってて、配列変数の場合だとインデックスは0から始まるから、インデックス処理が1個ずれるのがポイントだな。

ちなみに文字列の一部として展開したい場合は@じゃなく以下のように*を使ったほうが良い。

#!/bin/sh
echo "最後以外: ${*:1:$#-1}"
echo "最後のみ: ${*:$#}"

ちなみに上記2つのスクリプトにを仮に atmark.sh と asterisk.sh とした場合以下の様な違いが出るよ。

sh atmark.sh a b c d
# ↑これは↓こう解釈される
echo "最後以外: a" "b" "c" 
echo "最後のみ: d"

sh asterisk.sh a b c d
# ↑これは↓こう解釈される
echo "最後以外: a b c" 
echo "最後のみ: d"

普通の配列(インデックス配列)

例を見たほうが早いと思うので早速…

# 要素数3の配列を作成
ARRAY=(a b "c c c")

# 値の取得
echo ${ARRAY[0]}  # -> a
echo ${ARRAY[1]}  # -> b
echo ${ARRAY[2]}  # -> c c c
echo ${ARRAY[3]}  # -> (空)

# 配列サイズの取得
echo ${#ARRAY[@]}  # -> 3

# 配列の全ての値を取得(@と*はダブルクオートで囲った時の動作が異なる)
echo ${ARRAY[@]}    # echo a b c c c と同じ
echo ${ARRAY[*]}    # echo a b c c c と同じ
echo "${ARRAY[@]}"  # echo a b "c c c" と同じ
echo "${ARRAY[*]}"  # echo "a b c c c" と同じ

# 配列の値を書き換える
ARRAY[0]=A  # -> ARRAY=(A b "c c c")

# 配列に値をインデックス指定で追加する1(最後のインデックス+1のインデックスを指定する)
ARRAY[3]=d  # -> ARRAY=(A b "c c c" d)

# ループで回す1(ダブルクオートで囲まない場合は@も*も同じ動作で、全て空白展開された後に解釈される)
for v in ${ARRAY[@]}; do
  echo "$v"
done
# a
# b
# c
# c
# c

# ループで回す2([@]をダブルクオートで囲むと配列の値が1つずつ取り出せる)
for v in "${ARRAY[@]}"; do
  echo "$v"
done
# a
# b
# c c c

# ループで回す3([*]をダブルクオートで囲むと1つの文字列になるだけなので1回しかループしない)
for v in "${ARRAY[*]}"; do
  echo "$v"
done
# a b c c c

# 配列に値をconcat的に追加する(複数の値を纏めて追加もできる)
ARRAY+=(e)   # -> ARRAY=(A b "c c c" d e)
ARRAY+=(f g) # -> ARRAY=(A b "c c c" d e f g)

# 最後の値を取得する(配列サイズを取得する式を使って以下のようにすれば最後の値の取得もできます)
echo ${ARRAY[((${#ARRAY[@]}-1))]}  # -> g

# 配列をコピーする
A2=("$ARRAY[@]")  # -> A2=(A b "c c c" d e f g)

# 配列の一部分だけをスライスして取得する
echo ${ARRAY[@]:0:2}  # -> A b (インデックス0から2個取り出す)
echo ${ARRAY[@]:1:3}  # -> b c c c d (インデックス1から3個取り出す)
echo ${ARRAY[@]:4}    # -> d e f g (第2引数を省略すると最後までの意)
echo ${ARRAY[@]:-1}   # -> (空) (マイナス表現で最後の値を取得するようなことは残念ながら出来ません)

# 配列の各値に対して置換や前方一致削除や後方一致削除等の式を適用することも可能です
ARRAY=(/bin /usr/bin /usr/local/bin)
echo ${ARRAY[@]/bin/lib}  # -> /lib /usr/lib /usr/local/lib (文字列置換)
echo ${ARRAY[@]#*/}       # -> bin usr/bin usr/local/bin    (最短前方一致削除)
echo ${ARRAY[@]##*/}      # -> bin bin bin                  (最長前方一致削除)
echo ${ARRAY[@]%[a-z]*}   # -> /bi /usr/bi /usr/local/bi    (最短後方一致削除)
echo ${ARRAY[@]%%[a-z]*}  # -> / / /                        (最長後方一致削除)

# なんかキモイので僕は余り使わないが配列変数に引数無しでアクセスすると
# 最初の値に対する操作と同じ意味になる。
ARRAY=(a b c)
echo $ARRAY        # -> a (最初の値が返される)
ARRAY=A            # これは ARRAY[0]=A と同じ
echo "${ARRAY[@]}" # -> A b c (最初の値だけが変更されている)

# インデックスを飛ばすことも出来る
ARRAY=(a b)
ARRAY[100]=c
echo ${ARRAY[0]}   # -> a
echo ${ARRAY[1]}   # -> b
echo ${ARRAY[2]}   # -> (空)
echo ${ARRAY[100]} # -> c
echo ${ARRAY[@]}   # -> a b c    # インデックス順で取得される
echo ${#ARRAY[@]}  # -> 3        # 配列サイズはインデックスが飛んでても正しいサイズ
echo ${!ARRAY[@]}  # -> 0 1 100  # キー一覧は飛んだ値になる

連想配列

bashでは連想配列も使えます。※残念ながら連想配列bash 4.x以上でしか使えません。古いbashではdeclare -Aをした時点でinvalid optionエラーになります。
連想配列を使う場合は変数に連想配列だよーという属性を追加するためにdeclare -Aをしてやる必要があります。

# 連想配列だよーと宣言(必須)
declare -A HASH

# 代入
HASH[foo]=bar
HASH[a]=1
HASH[2]=ni # declare -Aで連想配列化されてる場合は数字添字も単に文字列キーとして扱われる

# 利用
echo ${HASH[foo]} # -> bar
echo ${HASH[a]}   # -> 1
echo ${HASH[2]}   # -> ni

# まとめて代入も出来ます
HASH=([one]=1 [two]=2 [three]=3)
echo ${HASH[one]}   # -> 1
echo ${HASH[two]}   # -> 2
echo ${HASH[three]} # -> 3

# 同じ書式で、declareと同時に初期化もできます
declare -A H2=([tatuya]=dead [takuya]=alive [minami]=alive)

# インデックス配列と同じようにconcat的な追加もできます
HASH+=([four]=4 [five]=5) 

# 値の列挙もインデックス配列と同じように[@]や[*]で出来ます(ただし順不同)
echo ${HASH[@]} # -> 4 1 5 2 3

# 配列サイズもインデックス配列の時と同じように#で出来ます
echo ${#HASH[@]} # -> 5

# キーの列挙も出来ます(ただし順不同)
echo ${!HASH[@]} # -> four one five two three

# 特定のキーを削除するには添字指定で unset すればよい
unset HASH[one]
echo ${HASH[@]}  # -> 4 5 2 3
echo ${!HASH[@]} # -> four five two three

# foreach的なループをしてみる
for key in "${!HASH[@]}";do
  value="${HASH[$key]}"
  echo "$key is $value"
done
# four is 4
# five is 5
# two is 2
# three is 3

その他:配列な特殊変数

配列変数の扱いからはちょっと外れるけど配列繋がりってことで…

  • $PIPESTATUS という特殊変数の紹介。知ってると結構便利。
echo foo | grep foo | grep bar | cat
echo ${PIPESTATUS[@]} # -> 0 0 1 0 (直前のパイプの各コマンドの終了コードが個別に取れる)
echo foo | grep foo | grep bar | cat
echo ${PIPESTATUS[2]} # -> 1 (配列変数なので一部のみを狙って取れる)

さいごに

慣れれば配列を使えるというのはやはり便利ではありますが、知らないとイミフなので他人が見る可能性のある各所に説明もしくは以下のようなコメントををつけておくのが無難でしょうw

# 配列操作に関してはココを参照>http://bit.ly/SLZ3Gx