はじめに
まずはじめにシェルスクリプトでオブジェクト指向プログラミングを行うのは推奨しません。他にもっと良い言語があるからです。この記事は何かをシェルスクリプトで実装したいけれど、オブジェクト指向的なことが必要になってしまったという運が悪い(良い?)場合に、それが可能であることを示す概念実証のようなものです。クラスとインスタンス相当のみの実装で完全なオブジェクト指向とは程遠いです。
既存フレームワーク
実はシェルスクリプト(bash)でオブジェクト指向プログラミングを行う事ができる、Bash Infinity (bash-oo-framework) というフレームワークが存在します。私はこのフレームワークを使ったことはありませんが、これはオブジェクト指向以外にも多くの機能を備えており重厚でその構文はシェルスクリプトとは随分異なっているように見えます。またその名の通り bash でしか動作しないでしょう。このようなフレームワークは便利だとは思いますが、自作の簡単なスクリプトでちょっと使いたい場合には過剰であると思います。この記事で紹介する手法は、ほんの十数行のヘルパー関数を定義するだけで、あなたのシェルスクリプトでオブジェクト指向プログラミングを行うことが出来ます
サンプルコード
まずシェルスクリプトによるオブジェクト指向コードがどのようなものになるかを紹介します。(あくまでもこれは概念実証なので改善の余地はあるでしょう)
MyClass がクラス、foo、bar がインスタンス、set_value、method がメソッドです。
# !/bin/sh
set -eu
# ヘルパー関数(省略)
# クラス定義(省略)
new MyClass:foo a
new MyClass:bar b
foo set_value 123
bar set_value 456
foo method # => a123
bar method # => b456
delete foo
delete bar
foo、bar はインスタンスと書きましたが、実際にはこれらはシェル関数です。本来オブジェクト指向言語ではないシェルスクリプトでオブジェクト指向を実現するため、多少奇妙なコードが含まれるのは免れませんが foo、barをコマンド名、set_value や method をサブコマンドと考えればそれほど違和感はないと思います。
ヘルパー関数
オブジェクト指向を実現するためのヘルパー関数はわずか十数行のコードです。インスタンス(シェル関数)の生成と破棄を行う new と delete 関数、インスタンス変数にアクセスするための __store__、__fetch__、__delete__ 関数からなります。
object_id=1
new() {
eval "${1#*:}() { eval 'shift; ${1%:*}_'\"\$1\"' ${1%:*}:$object_id \"\$@\"'; }"
eval "shift; ${1#*:} __init__ \"\$@\""
object_id=$((object_id + 1))
}
delete() {
eval "${1#*:} __del__"
unset -f "${1#*:}"
}
__store__() { eval "object_${1#*:}_$2=\$3"; }
__fetch__() { eval "$3=\$object_${1#*:}_$2"; }
__delete__() { unset "object_${1#*:}_$2"; }
クラス定義
クラス定義は、[クラス名]_[メソッド名] という形のシェル関数を定義するだけです。関数の第一引数は他の言語で言う this や self に相当する値です。
MyClass___init__() { # コンストラクタ
__store__ "$1" init "$2"
}
MyClass___del__() { # デストラクタ
__delete__ "$1" init
__delete__ "$1" value
}
MyClass_method() {
__fetch__ "$1" init _init
__fetch__ "$1" value _value
echo "MyClass_method : ${_init}${_value}"
}
MyClass_set_value() {
__store__ "$1" value "$2"
}
仕組み
オブジェクト指向(正確にはクラスやインスタンス生成)を実現するのに必要なのはクラス(関数の集まり)とインスタンス(変数の集まり)を結びつけることです。それを new 関数で行っています。
POSIX シェル準拠の範囲では、シェルスクリプトは配列も連想配列もありません。そのため工夫した変数名にインスタンス変数を保存します。変数名は object_[オブジェクトID]_[変数名] です。オブジェクト ID はインスタンスを new するたびにインクリメントされ現在のシェル実行環境において一意の ID が割り当てられます。
2021-03-20 追記 オブジェクトID ではなくインスタンス変数名を使った方が良さそうです。
new を実行するとインスタンス=シェル関数が eval によって動的に定義されます。例えばサンプルのコードの場合以下のような関数が定義されます。
foo() { eval 'shift; MyClass_'"$1"' MyClass:1 "$@"'; }
bar() { eval 'shift; MyClass_'"$1"' MyClass:2 "$@"'; }
このコードの MyClass:1、MyClass:2 がクラス名とインスタンス ID です。これにより foo、bar 関数にはクラス名とインスタンス ID が紐付けられます。またこの foo、bar 関数 は [クラス名]:[インスタンス ID] のペア(this 相当)を第一引数にしてメソッドを呼び出すという処理を行っています。
インスタンス変数へのアクセスは __store__、__fetch__、__delete__ を通して行います。それぞれの関数の第一引数からインスタンス ID がわかるので、インスタンス変数にアクセスするのは容易です。
そして delete 関数でインスタンス変数(シェル関数)を削除します。
ちなみにこの仕組みは Perl を知ってる方にとっては bless に近いと言えばわかるのではないでしょうか?
さいごに
この記事の内容はオブジェクト指向プログラミングの基本しか実装していませんが、シェルスクリプトは eval や関数の再定義などメタプログラミング的なことも行えるので、継承や多態性といった他の機能も実装できるのではないかと思います。興味がある方はトライしてみてはいかがでしょうか?