それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

bashの配列のまとめ(定義・代入・参照と取得・ループ)

bash も配列を扱える

シェルスクリプトで配列を扱えるので 配列を扱ってみる。

今回は、サンプルのために、変数名を大文字にしています。もちろん小文字も使えます。

大文字は、おもに環境変数スクリプト間やプロセス間で値を渡すときに使うことが多いようです。

配列の定義

配列の定義には基本的に2つあります。

コマンドをの実行結果を配列に入れたい場合は、mapfile や IFS を使うのがいいとおもいます。

bash の配列の定義の例

declare ARR=()
declare -a ARR # または
arr=() #無精する場合

配列の定義を改行する場合は、文字列でクォートが手っ取り早い。

arr=("a b
  c
")

配列の削除

定義した配列は削除できます。

unset -v ARR 
unset -v ARR[@]  # または
unset -v ARR[*] # または

配列の初期値を指定した宣言

初期値を指定した宣言をすることで明確になります。

ARR=( a b c d e  )

区切り文字は「空白」文字列の続いたものです。

もっと詳細にいえば、区切り文字は環境変数 IFS で指定されたものになります。

配列の全要素を見る

配列の要素の中身を全部見ることが出来ます。

ARR=(a b c)
echo ${ARR[@]} 
echo ${ARR[*]} # または

配列の要素数

配列の要素数${#変数名}を使うことで取得することが出来る。

配列の要素数の取得例
length=${#ARR[@]}
length=${#ARR[*]} # または

@* の違い

ARR[@]ARR[*] は、ほぼ同等の機能を持つが、一点だけ異なる。

それは区切り文字。@ は 空白で区切るのに対し、 * は IFS で区切る。

@* の違いの例
$ ARR=(a b c d e )
$ IFS=-
$ echo "${ARR[@]}" a b c d e
$ echo "${ARR[*]}" a-b-c-d-e

ただし、ダブルクォテーションをつけない場合は[@]/[*]に違いが現れない。それはIFS変数が作用するから。

クォテーションをつけない場合だと、IFSで指定した区切り文字で出力されたあとに、IFSで区切って次にのコマンドに渡されるので違いはない。

環境変数のIFSで区切り文字を指定することで、出力と入力を使える。

# [*]にダブルクォーテーション比較
$ IFS=-
$ echo  ${ARR[*]}   a b c d e
$ echo "${ARR[*]}" a-b-c-d-e
# ダブルクォーテーションが無しの例さらに
IFS=#
ARR=(a b c )
echo ${ARR[*]} #=> a b c 

これは、bashでは次のように「順に解釈される」と覚ええおくと理解しやすいです。

IFS='#'
ARR=(a b c )
echo ${ARR[*]} #=> a b c 

# 1. echo a${IFS}b${IFS}c # IFS付きで変数処理され
# 2. echo a#b#c    # IFS付の文字列となり
# 3. echo a b c    #IFS=# で区切られた引数になる。コレがコマンドに渡される。 

配列の要素を走査する。

配列の指定番目の値を取り出すには インデックス番号を使う。

配列の要素を見ていく例。
$ ARR=(a b c )
$ echo ${ARR[0]}   # => a
$ echo ${ARR[1]}   # => b
$ echo ${ARR[2]}    # => c
$ echo ${ARR[3]}    #  なにもないので空っぽが出る

配列の逐次処理

配列の要素の中身を取り出して、順番に見ていくには。 for ループを使うのが楽

for e in ${ARR[@]} ; do 
  echo $e
done

配列の要素を削除する

配列の指定番目の要素を削除するのは、 変数の削除と同じく unset を使う。

削除した配列はもとの配列からインデックスが欠損したものとなる。

配列の要素の削除の例
unset -v ARR[1]
削除後の配列の構造
$ for idx in ${!ARR[@]} ; do    echo " [$idx] => ${ARR[$idx]} " ; done
 [0] => a
 [2] => c
削除後の配列の再度採番する

Reインデックスして、リナンバーするには、もう一度定義し直しが早いと思う

$ B=( ${ARR[@]} )
$ for idx in ${!B[@]} ; do    echo " [$idx] => ${B[$idx]} " ; done
 [0] => a
 [1] => c

配列の要素の追加。

要素の追加も、再度配列を再生成する事になりそうですが、Bashには専用の演算子が用意されています。

専用演算子 +=() がイメージしやすくていいと思います。

専用演算子 +=() について
ARR+=(c) # 要素の追加
配列の追加(再生成)の例
$ ARR=(a b)
$ ARR=(${ARR[@]} c) # 末尾に追加した
$ echo ${BRR[@]}
a b c
配列の追加(オペレータ)の例
$ ARR=(a b)
$ ARR+=(c) # 追加した
$ echo ${ARR[@]}
a b c
push / unshift

配列に push / unshift するために要素のインデックスを指定するのは無理なので、配列の再定義を使う

## push 
ARR=(${ARR[@]} 'element') # 末尾に追加(push)した
## unshift 
ARR=('element' ${ARR[@]} ) # 先頭に追加(unshift)した

配列の変数アクセス ${ARR} と $ARR

配列に、{ } をつけずにアクセスした場合、配列の先頭の要素が返ってくる。古くからの人にはおなじみなのかもしれないが、私は迷うので使わないことしてる。

$ echo ${ARR[@]}  # => a b c
$ echo $ARR  # => a

個人的にあまり使わないので、どういう役目があるのか知らない。

要素の切り出し スライス

配列をスライスしたい。こんなのよくあることなので要素のスライスする方法を知っておきたい。

要素をスライスするには、専用の記法 {ARRAY[@]:X:n} を使う。X はインデックス n はほしい個数

配列のスライスの例
$ ARR=(a1 a2 a3 a4 a5)
$echo ${ARR[@]:0:2} #=> a1 a2

ちなみに、-1 などのプログラムで定番のも使える。

配列の最大個数より大きな個数を指定しても、配列の最大個数までしか取り出せない。

どうしても末尾がほほしいときは計算する ${ARR[@]:0:((${#ARR[@]}-1))} このように取り出す。

配列を末尾からスライス:追記

bash 4 からの機能で、配列を末尾から参照することも出来るようになっています。

${ARR[@]: -1} # ' -1 ' スペースをつけて -1 を書く
配列をスライスして先頭だけを消す

先頭を捨ててshift 的なことも出来ます。

ARR=("${ARR[@]:1}")

スライスを応用すれば、こんなことが出来ます。

配列の長さ

配列の長さは $#ARR[@] でアクセスする

ARR=(this is a test of array)
echo ${#ARR[@]}

文:センテンス(words)を自動展開して配列に

配列のデフォルト定義 ARRAY=() は単語を区切るのに使われる。もともと英語はスペースで分けて書くのでそういうもの。

$ARR=( Hello I am takuya  )
$ IFS=,
$ echo "${ARR[*]}"
Hello,I,am,takuya

コマンドの出力結果や、整形された出力などは、かんたんに配列に展開できます。

IFSを使うと 配列の join ( 結合 ) ができる。

${my_arr[@]} だと区切りがスペースになるが、 * を使うと IFS区切り を使うので、IFSを上手に使えば、配列の結合(join) をすることができる。

結合を関数にする例

my_arr=( aaa bbb ccc )
( IFS=, ; printf %s "${a[*]}") ;

これを関数にして、

function join_arr()(  IFS=,; echo "$*" )

なんてこともできる。

function join_arr()(  IFS=,; echo "$*" )

join_arr abc xyz 123 # --> abc,xyz,123

IFSを知れば、配列の定義 a=( aaa bbb ) が、サブシェルとIFSの区切り文字だと気づくので面白い。

ループと併せて使う。for 編

配列はループと併せて使い、繰り返し処理をするために有ります。

そのため、ループと併せて使うのがほとんどだと思います。for ループを知っておけば、十分戦えると思います。

for と併せて使う例 ( for .. in
 for e in ${ARR[@]} ; do echo $e ; done
for と併せて使う例 ( for index in
 for idx in ${!ARR[@]} ; do echo ${ARR[$idx]} ; done
for と併せて使う例 ( C言語スタイル
for (( idx=0; idx< ${#ARR[@]}; idx++ )) ; do echo ${ARR[$idx]} ; done 

while は?

while は、for ループと等価交換可能なので、特に細かい説明必要はないかと思います。

ただ、配列をあるだけ回す無限ループはよく使いそうなので少し例を上げておきたいと思います。

ARR=(a b c)

while (( ${#ARR[@]}  > 0 )) ; do 
  echo $ARR
  ARR=("${ARR[@]:1}")
done

これは、配列を先頭から一つずつ切り出して、先頭の要素を$ARR で参照して、先頭の要素をスライス削除しています。

スクリプトを頭のなかで展開して理解するには。forループとしてはwhileであるだけ回すループのほうがシンプルで理解しやすいかもしれません。

あれ?shift は?位置変数は?

shift は 位置変数配列(コマンドライン引数・関数の引数)のときに使うもので、一般的な配列だけを扱うこの記事には登場させません。それでだろうか、unshift 関数が用意されていない。

連想配列

bash の配列では 連想配列(ハッシュ・dict ) も扱うことが出来ます。それはまた別記事に。

その他

xargs を使ってパイプで書き換えることは出来ません。

コマンドの実行結果を配列に入れたい場合は

read line や map fileを使います。

while / read / line を使う例。

routes=() # 空の配列を初期化

while IFS= read -r line; do
    routes+=("$line")
done < <(ip route show default)

mapfile -t を使う例

mapfile -t routes < <(ip route show default)

mapを使うほうが楽ですね。

行単位でfor-Eachする

行単位に行を配列にいれ場合、扱いは慎重に行う。とくに、foreachの使い方では注意する。

単純に for しただけではIFSで細分化されてしまう。

そのため、以下のように一旦コマンド結果な文字列にするとうまくいく。

mapfile -t routes < <(ip route show default)
## 行単位でforEach
for i in "${routes[@]}" ; do 
  echo $i
done 

行単位の配列に扱いになれないうちは、bashシェルスクリプトを書くのが難しいかもしれない。

要素ごとに処理をする例

配列を使うときは1行ずつ処理できたほうがいい。

$ mapfile -t routes < <(ip route show default)
$ printf "%s\n" "${routes[@]}"
default via 192.168.55.5 dev eth2 proto static metric 200
default via 192.168.99.1 dev wg11 proto static metric 201
default via 172.16.3.2 dev wg0 proto static metric 202

$ printf "ip route del %s\n" "${routes[@]}"
ip route del default via 192.168.55.5 dev eth2 proto static metric 200
ip route del default via 192.168.99.1 dev wg11 proto static metric 201
ip route del default via 172.16.3.2 dev wg0 proto static metric 202

for がない方が、むしろ便利かもしれない。1行ずつ処理する方法を知っておくと、bashがとても捗ると思います。

上記の例は、次のように展開される。

# コマンド
$ printf "ip route del %s\n" "${routes[@]}"
# bashがコマンドを解釈(変数の展開)
$ printf "ip route del %s\n" "${routes[0]}" "${routes[1]}" "${routes[2]}"
# printf が複数引数毎にフォーマットする
printf "ip route del %s\n" "${routes[0]}" 
printf "ip route del %s\n" "${routes[1]}" 
printf "ip route del %s\n" "${routes[2]}"
# printf の結果、改行され行に戻る

ip route del default via 192.168.55.5 dev eth2 proto static metric 200
ip route del default via 192.168.99.1 dev wg11 proto static metric 201
ip route del default via 172.16.3.2 dev wg0 proto static metric 202

これを応用すると、引数処理をまとめてできる

$ ips=(
  192.168.1.1
  192.168.1.2
  192.168.2.1
  192.168.1.2
)
$ printf -- '--bind %s ' "${ips[@]}"
--bind 192.168.1.1 --bind 192.168.1.2 --bind 192.168.2.1 --bind 192.168.1.2

配列処理を知っておくと、1行毎に処理をしたり、for-eachが使えたり、bashの引数展開でまとめて処理ができる。

配列の典型的な処理を知っておくと得をする。

2016-12-27 追記

bash の スライスの記述が間違っていました。修正しました。

a=(a b c)
echo ${a[@]: -1}  #=> c

出来ないと書きましたが、出来ました。私の記述方法が誤りでした。

${a[@]:-1} # 動かない
${a[@]: -1} # 動く

スペースが必要でした。

参考資料

http://wiki.bash-hackers.org/syntax/arrays

2021-02-16

IFS が検索されないので、キーワードを明示的に指定した。

IFS の区切り文字つかった、配列の結合 ( join ) について言及。

2025-11-04

コマンドの実行結果を配列に入れる方法を追記。