mac の zsh と Linux/Windows の bash で互換性があるシェルスクリプトの連想配列
OS が異なると連想配列(associative array)の書き方が異なる問題
mac のベース部分は UNIX 系の OS ですが、Linux と同じように使えるかというとそうでもない部分があります。その1つがシェルスクリプトの連想配列です。
Linux や Windows Git bash の環境の bash と mac の環境の zsh および bash では連想配列(associative array)の書き方が異なります。
本記事では、どの OS 環境のシェルスクリプトでも動く互換性がある連想配列の関数の紹介と、その関数を使わない方法まで検討した結果を紹介します。その際、mac において bash がどう扱われているかについても学ぶことができます。
どのように書けば互換性を保てるかをあらかじめ知っておけば、安心してコードを書くことができるようになるでしょう。
連想配列とは
まず連想配列(associative array)がどういうものか説明します。
連想配列とは、連想配列と文字列の組み合わせに対して値を設定、および、取得できるものです。JavaScript で書くと以下のようなコードになります。
JavaScript:
const employee = {};
employee['first_name'] = 'John';
console.log( employee['first_name'] ); // John
このコードは、次の MDN のページに貼り付けることで動作確認することができます。
連想配列の関数
後で説明しますが、シェルスクリプトで連想配列を使うと互換性を持たせることができません。
そこで、配列を連想配列としてアクセスする関数を作成しました。連想配列の関数を使ったコードは以下のようになります。
employee=()
eval "$(_set employee first_name "John" )"
echo "$(_get employee first_name )" #// John
_set
関数と _get
関数は、配列にキーとバリュー(値)が交互に入っているものとして処理します。線形検索でキーを検索するのでかなり遅いですが、数十回程度のアクセスなら気にならないでしょう。
下記のコードは Linux, Windows Git bash, mac のすべてで動作します。
~/_tmp.sh
function main_func() {
employee=()
eval "$(_set employee first_name "John" )"
echo "$(_get employee first_name )" #// John
}
#// _get
#// Example:
#// object=(key_A "1" key_B "x")
#// echo "$(_get "${object[@]}" key_B )"
function _get() {
local object_name="${1}"
local key="$2"
local operation=""
operation="_get_sub \"\${${object_name}[@]}\" \"${key}\""
eval "${operation}"
}
function _get_sub() {
local object_entries=("${@}")
local key_index=$(( ${#object_entries[@]} - 1 ))
local key="${object_entries[${key_index}]}"
local value=""
for (( i = 0; i < "${key_index}"; i += 2 ));do
if [ "${object_entries[${i}]}" == "${key}" ]; then
value="${object_entries[${i}+1]}"
fi
done
echo "${value}"
}
#// _set
#// Example:
#// object=(key_A "1" key_B "x")
#// eval "$(_set object key_B "y" )"
function _set() {
local object_name="${1}"
local key="$2"
local value="$3"
local operation=""
operation="_set_sub \"\${${object_name}[@]}\" \"${object_name}\" \"${key}\" \"${value}\""
eval "${operation}"
}
function _set_sub() {
local object_entries=("${@}")
local count=${#object_entries[@]}
local object_name_index=$(( ${count} - 3 ))
local key_index=$(( ${count} - 2 ))
local value_index=$(( ${count} - 1 ))
local object_name="${object_entries[${object_name_index}]}"
local key="${object_entries[${key_index}]}"
local value="${object_entries[${value_index}]}"
local command=""
for (( i = 0; i < "${key_index}"; i += 2 ));do
if [ "${object_entries[${i}]}" == "${key}" ]; then
command="${object_name}[$(( ${i} + 1 ))]=\"${value}\""
fi
done
if [ "${command}" == "" ]; then
local new_key_index=$(( ${count} - 3 ))
local new_value_index=$(( ${count} - 2 ))
command="${object_name}[${new_key_index}]=\"${key}\"; ${object_name}[${new_value_index}]=\"${value}\""
fi
echo "${command}"
}
function test_of_get_set_func() {
object=(key_A "1" key_B "x")
echo "$(_get object key_A )"
echo "$(_get object key_B )"
eval "$(_set object key_A "2" )"
eval "$(_set object key_B "y" )"
eval "$(_set object key_C "z z" )"
echo "$(_get object key_A )"
echo "$(_get object key_B )"
echo "$(_get object key_C )"
}
#// test_of_get_set_func
main_func "$@"
動作確認コマンド (bash or zsh)
code ~/_tmp.sh
#// 上記コードを ~/_tmp.sh に貼り付けて保存
chmod +x ~/_tmp.sh
~/_tmp.sh
#// John
rm ~/_tmp.sh
bash 3 には連想配列がありません
mac に付いている bash はバージョン 3系で declare -A
による連想配列が使えません。一見、使えるような動きをしますが、連想配列ではない普通の変数の動きになります。間違った値で動き続けるので、非常に危険なコードです。
下記のコードは、mac 12.3.1 で実行したときの結果です。
~/_tmp.sh
#!/bin/bash
declare -A object_A #// associative array
object_A["Attr1"]="Value1"
echo ${object_A["Attr1"]} #// "Value1"
object_A["Attr2"]="Value2"
echo ${object_A["Attr1"]} #// "Value2" ... 間違った値!
echo ${object_A["Attr2"]} #// "Value2"
echo $BASH_VERSION #// 3.2.57(1)-release
mac に付いている bash がバージョン 3 で止まっている理由は、bash 4 から GPLv3 というライセンスを採用したかららしいです。GPL は伝統的にソースの公開義務がありアップルはそれを懸念したようです。なお、zsh はソースの公開義務がない MIT ライセンスです。
zsh の連想配列
mac の端末で起動するシェル(インタラクティブ シェル)は zsh ですが、スクリプトを実行するのときのデフォルトのシェルは bash 3 です。なお、スクリプトのデフォルトのシェルとは、シェバンを設定しないときのシェルのことです。
そこで、シェバンに zsh を設定すれば zsh の連想配列が使えます。ただし、zsh で連想配列を生成するときは、declare ではなく typeset を使います。名前が変わるので Linux と互換性はありません。
~/_tmp.sh
#!/bin/zsh
typeset -A object_A #// associative array
object_A["Attr1"]="Value1"
echo ${object_A["Attr1"]} #// "Value1"
object_A["Attr2"]="Value2"
echo ${object_A["Attr1"]} #// "Value1" OK
echo ${object_A["Attr2"]} #// "Value2"
echo $ZSH_VERSION #// 5.8
そこで、動いているシェルに応じて declare と typeset を切り替えるシェルスクリプトにします。また、シェバンを #!/bin/bash
に戻します。
~/_tmp.sh
#!/bin/bash
if [ "${BASH_VERSION}" != "" ]; then
if [ "${BASH_VERSION:0:1}" == "3" ]; then
echo "ERROR. BASH_VERSION=${BASH_VERSION}"; exit 2
fi
declare=declare
elif [ "${ZSH_VERSION}" != "" ]; then
declare=typeset
else
echo "ERROR"; exit 2
fi
$declare -A object_A #// associative array
object_A["Attr1"]="Value1"
echo ${object_A["Attr1"]} #// "Value1"
object_A["Attr2"]="Value2"
echo ${object_A["Attr1"]} #// "Value1" OK
echo ${object_A["Attr2"]} #// "Value2"
echo BASH_VERSION=${BASH_VERSION}
#// mac: 3.2.57(1)-release, Windows Git bash: 4.4.23(1)-release
echo ZSH_VERSION=${ZSH_VERSION}
#// mac: 5.8
mac から起動するときは、コマンド /bin/zsh ~/_tmp.sh
で起動する必要があります。~/_tmp.sh
で起動すると bash 3 で実行され、前述したように連想配列のつもりで書いたコードが普通の変数になって動作し続けます。この動作は非常に危険なので先頭でエラーにしています。
チェックしている行の exit を コメント アウト すれば普通の変数として動いていることを確認できます。
echo "ERROR. BASH_VERSION=${BASH_VERSION}"; #// exit 2
:
echo ${object_A["Attr1"]} #// "Value2" NG
HomeBrew にある最新 bash の連想配列
バージョン4以上の bash では連想配列が使えます。そこで、mac に最新の bash をインストールしてみます。
mac に最新の bash をインストールするには、https://brew.sh を参考に HomeBrew をインストールして、
/bin/bash -c "$(curl ...)"
下記のコマンドで bash をインストールします。
brew install bash
/opt/homebrew/bin/bash --version
インストールした最新の bash を使えば連想配列が使えます。しかし、シェバン(1行目)が HomeBrew 独特の設定になります。
~/_tmp.sh
#!/opt/homebrew/bin/bash
declare -A object_A #// associative array
object_A["Attr1"]="Value1"
echo ${object_A["Attr1"]} #// "Value1"
object_A["Attr2"]="Value2"
echo ${object_A["Attr1"]} #// "Value1" OK
echo ${object_A["Attr2"]} #// "Value2"
echo $BASH_VERSION #// 5.1.16(1)-release
Linux や Windows Git bash では/bin/bash または /usr/bin/bash に最新の bash があるので、シェバンを #!/bin/bash
または #!/usr/bin/bash
に設定しなければどの環境でも動くようにはなりません。しかし、mac では /bin/bash または /usr/bin/bash を変更しようとするとSystem Integrity Protection (SIP) によってエラーになります。SIP をオフにすることや /bin/ の内容を変えることは危険なのでお勧めしません。
sudo mv /bin/bash /bin/bash-old
#// mv: rename /bin/bash to /bin/bash-old: Operation not permitted
sudo ln -sf /opt/homebrew/bin/bash /usr/bin/bash
#// ln: /usr/bin/bash: Operation not permitted
csrutil disable
#// csrutil: This tool needs to be executed from Recovery OS.
csrutil disable; reboot
#// (効果なし)
csrutil status
#// System Integrity Protection status: enabled.
シェバンが #!/bin/bash
などのスクリプトを、mac から起動するときは、コマンド /opt/homebrew/bin/bash ~/_tmp.sh
で起動する必要があります。
結局、互換性がない部分は zsh で実行する方法と大して変わりませんでした。
(おまけ)連想配列の別名
ちなみに連想配列はいろいろな言い方をされます。
- 連想配列, associative array(Linux bash)
- 辞書, dictionary(Python, C#)
- ハッシュ, hash (Ruby)
- ハッシュテーブル、hash table, ハッシュ表 (.NET Framework)
- オブジェクト, object(JavaScript, JSON)
- マップ, map(JavaScript, Go, C++ STL)
- ハッシュマップ, hash map (Java, Rust)
- マッピング, mapping(YAML)
- 連想リスト, associative list (LISP)
- 連想コンテナ, associative container (C++)
- 結合配列, joined array (PL/SQL)
- キー バリュー, key value (データベースの種類)
これらはすべて同じ機能に対する呼び名です。独自用語が乱立していますね。
Discussion