zsh の配列操作の基本から応用まで

zsh は当然配列を使える。今まであんまり気にしてなかったんだけど、ちょっと調べてみるといろんな配列操作ができるようになっていた。普通のプログラム言語とは違う感じのややこしい書き方もあったのでまとめてみた。

## 基本

### 宣言
普通は宣言する必要はない。もし明示的に配列であると宣言したい場合は `-a` を使用する。

```zsh
# ローカル変数として宣言する
local -a arr2
# または
typeset -a arr1

# グローバル変数として宣言する
typeset -g -a arr1
```

配列に限らないことだけど、関数の中で `typeset` で宣言した場合は関数内のローカル変数になる。`typeset` で宣言しつつグローバル変数にしたい場合は `-g` オプションを付ける。

### 代入
カッコでくくって要素を並べると代入できる。

```zsh
arr1=(aaa bbb ccc ddd)

# カッコだけ書くと空の配列になる
empty_array=()
```

### 要素の参照
各要素を参照する方法はこんな感じ。

```zsh
arr1=(aaa bbb ccc ddd)

# 配列の個別の要素を取得する
# 番号が1から始まることに注意
echo $arr1[1] #=> aaa

# マイナスの番号を使うと後ろから数えて指定できる
# -1が一番最後の要素、-2が最後から2番目...
echo $arr1[-1] #=> ddd
echo $arr1[-2] #=> ccc

# [M,N] で M番目からN番目までの部分配列を取得
echo $arr1[2,4] #=> bbb ccc ddd

# [M,N] 形式でもマイナスの値が使える
echo $arr1[2,-2] #=> bbb ccc

# 要素数
echo $#arr1 #=> 4
```

### 変更
配列と言えば push, pop とかそういう普通の操作をしたくなる。
shift だけ組み込みの関数が用意されてるのでそれを使う。他は自力で書く。

```zsh
arr1=(aaa bbb ccc ddd)

# shift (先頭の要素を削除)
shift arr1
echo $arr1 #=> bbb ccc ddd

# unshift (先頭に要素を追加)
arr1=(ppp $arr1)
echo $arr1 #=> ppp bbb ccc ddd

# push (末尾に要素を追加)
arr1=($arr1 qqq)
echo $arr1 #=> ppp bbb ccc ddd qqq
# または += でもOK
# arr1+=(qqq)

# pop (末尾の要素を削除)
arr1=($arr1[1,-2])                     
echo $arr1 #=> ppp bbb ccc ddd
```

### ループ
ループは普通に `for` を使う。C のようなインデックス付きのループではなくて、他の言語で言う foreach のような動作をする。

```zsh
arr2=("aaa 1" "bbb     2" "ccc 3")
for i in $arr2; do
    echo $i
done
# =>
# aaa 1
# bbb     2
# ccc 3
# 
# 要素の中に空白が入っていても大丈夫
```


### 検索
ここまでは最近の bash でも同じようなことができるんだけど、zsh らしい使い方として「配列の中から指定した条件にマッチする要素だけを集める」というような使い方を紹介する。
他のプログラム言語で言うと collect にあたる。

`${(M)配列名:#パターン}` という形式で「条件にマッチした要素だけを集めた配列を作る」 という動作になる。
M の部分を消して `${配列名:#パターン}` と書くと条件が逆転して、「条件にマッチしない要素だけを集めた配列を作る」とう意味になる。

```zsh
# aaa で始まる要素だけを集める
arr3=(aaa bbb1 ccc aaa1 bbb2 aaa33)
echo ${(M)arr3:#aaa*} #=> aaa aaa1 aaa33

# aaa で始まる要素だけを取り除く
echo ${arr3:#aaa*} #=> bbb1 ccc bbb2
```

`${配列名[(r)条件]}`、 `${配列名[(i)条件]}` という形式も使える。
1つ目の方は「条件にマッチした最初の要素を返す」、2つ目の方は「条件にマッチした最初の要素番号を返す」という意味になる。

```zsh
arr3=(aaa bbb1 ccc aaa1 bbb2 aaa33)

# bbb で始まる最初の要素を取得する
echo ${arr3[(r)bbb*]} #=> bbb1

# bbb で始まる最初の要素の要素番号を取得する
echo ${arr3[(i)bbb*]} #=> 2
# 見つからなかった場合は最後の要素番号+1になる
echo ${arr3[(i)xxx*]} #=> 7
```

`${配列名[(R)条件]}`、 `${配列名[(I)条件]}` というふうに大文字にすると探す順が逆になって、後ろから探すようになる。後ろから探すので、`${配列名[(I)条件]}` で見つからなかった場合は0になる。

`${配列名[(I)条件]}` を使うと、配列の中に特定の要素が含まれているかどうかをスマートにチェックできる。

```zsh
arr4=(aaa bbb ccc ddd)

if (( ${arr4[(I)bbb]} )); then
    echo "FOUND bbb"
else
    echo "NOT FOUND bbb"
fi
# => FOUND bbb

if (( ${arr4[(I)zzz]} )); then
    echo "FOUND zzz"
else
    echo "NOT FOUND zzz"
fi
# => NOT FOUND zzz
```



### 各要素の変更
ちょっとした map のようなこともできる。まずは例を紹介する。

```zsh
arr5=("aaa,111" "bbb,222" "ccc,333")

echo ${(M)arr5#*,} #=> aaa, bbb, ccc,
echo ${(M)arr5%,*} #=> ,111 ,222 ,333

echo ${(R)arr5#*,} #=> 111 222 333
echo ${(R)arr5%,*} #=> aaa bbb ccc
```

意味がわからないと思うので説明すると、まず '# パターン' は「先頭からパターンまでの部分にマッチ」、'% パターン' は「末尾からパターンまでの部分にマッチ」、という意味になる。
次に M と R だけど、M は各要素について「マッチした部分だけを残す」、R は「マッチしなかった部分だけを残す」、という意味。

さっきの例ではこれを組み合わせて、カンマで区切って変更する操作をやっている。
例えば1つ目の場合は、`#*,` が先頭から , までにマッチする。`M` を指定してるのでマッチしたところだけが残って、結果として各要素の , より後を削除した新しい配列を作っている。

ちなみに R は Rest の R という意味らしい。


## 応用例

いままで説明したことを使うと zsh だけで sed とか grep のようなことができる。

まず INPUT.txt というファイルがあってこんなふうに書いてあったとしよう。

```
% cat INPUT.txt
# # で始まる行はコメントです
#
# xxx1.net,192.168.1.1
xxx2.net,192.168.1.2
xxx3.net,192.168.1.3
xxx3.net,192.169.1.3
xxx4.net,192.168.1.4
```

この中で「xxx3.net」を含む行だけを取り出す、というような操作がしたいとする。これを zsh の機能でやってみる。

まず、ファイルの内容を改行区切りで配列に入れる。

```zsh
lines=( ${(@f)"$(< INPUT.txt)"} )
```

次に # で始まる行はコメントという仕様なので、コメント行を取り除く。

```zsh
# # で始まる行を取り除く
lines=( ${lines:#\#*} )
```

そのあとは上で説明したことを使えば色々できる。

```zsh
# xxx2.net を含む行が存在するかどうか判別する
if [ ${lines[(i)*xxx2.net*]} -le $#lines ]; then
    echo "FOUND"
else
    echo "NOT FOUND"
fi

# xxx3.net を含む行だけを取得する
echo ${(M)lines:#*xxx3.net*}
#=> xxx3.net,192.168.1.3
#   xxx3.net,192.169.1.3

# xxx4.net を含まない行だけを取得する
echo ${lines:#*xxx4.net*}
#=> xxx2.net,192.168.1.2
#   xxx3.net,192.168.1.3
#   xxx3.net,192.169.1.3

# , の右側だけを取得する
echo ${(R)lines#*,}
#=> 192.168.1.2
#   192.168.1.3
#   192.169.1.3
#   192.168.1.4

# xxx3.net で始まる行の , の右側だけを取得する
echo ${(R)${(M)lines:#xxx3.net*}#*,}
#=> 192.168.1.3
#   192.169.1.3
```

あと、`$(< ファイル)` のところを `$(コマンド)` にすればコマンドを実行した結果の出力(標準出力)を配列に入れれる。

```zsh
lines=( ${(@f)"$(ls ~)"} )
```

便利。

こんな感じで zsh の配列はいろいろ出来るので試してみてください。