LoginSignup
7
5

More than 3 years have passed since last update.

POSIX準拠シェルスクリプトでオブジェクト指向プログラミング

Last updated at Posted at 2021-03-15

はじめに

まずはじめにシェルスクリプトでオブジェクト指向プログラミングを行うのは推奨しません。他にもっと良い言語があるからです。この記事は何かをシェルスクリプトで実装したいけれど、オブジェクト指向的なことが必要になってしまったという運が悪い(良い?)場合に、それが可能であることを示す概念実証のようなものです。クラスとインスタンス相当のみの実装で完全なオブジェクト指向とは程遠いです。

既存フレームワーク

実はシェルスクリプト(bash)でオブジェクト指向プログラミングを行う事ができる、Bash Infinity (bash-oo-framework) というフレームワークが存在します。私はこのフレームワークを使ったことはありませんが、これはオブジェクト指向以外にも多くの機能を備えており重厚でその構文はシェルスクリプトとは随分異なっているように見えます。またその名の通り bash でしか動作しないでしょう。このようなフレームワークは便利だとは思いますが、自作の簡単なスクリプトでちょっと使いたい場合には過剰であると思います。この記事で紹介する手法は、ほんの十数行のヘルパー関数を定義するだけで、あなたのシェルスクリプトでオブジェクト指向プログラミングを行うことが出来ます

サンプルコード

まずシェルスクリプトによるオブジェクト指向コードがどのようなものになるかを紹介します。(あくまでもこれは概念実証なので改善の余地はあるでしょう)

MyClass がクラス、foobar がインスタンス、set_valuemethod がメソッドです。

#!/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

foobar はインスタンスと書きましたが、実際にはこれらはシェル関数です。本来オブジェクト指向言語ではないシェルスクリプトでオブジェクト指向を実現するため、多少奇妙なコードが含まれるのは免れませんが foobarをコマンド名、set_valuemethod をサブコマンドと考えればそれほど違和感はないと思います。

ヘルパー関数

オブジェクト指向を実現するためのヘルパー関数はわずか十数行のコードです。インスタンス(シェル関数)の生成と破棄を行う newdelete 関数、インスタンス変数にアクセスするための __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"; }

クラス定義

クラス定義は、[クラス名]_[メソッド名] という形のシェル関数を定義するだけです。関数の第一引数は他の言語で言う thisself に相当する値です。

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:1MyClass:2 がクラス名とインスタンス ID です。これにより foobar 関数にはクラス名とインスタンス ID が紐付けられます。またこの foobar 関数 は [クラス名]:[インスタンス ID] のペア(this 相当)を第一引数にしてメソッドを呼び出すという処理を行っています。

インスタンス変数へのアクセスは __store____fetch____delete__ を通して行います。それぞれの関数の第一引数からインスタンス ID がわかるので、インスタンス変数にアクセスするのは容易です。

そして delete 関数でインスタンス変数(シェル関数)を削除します。

ちなみにこの仕組みは Perl を知ってる方にとっては bless に近いと言えばわかるのではないでしょうか?

さいごに

この記事の内容はオブジェクト指向プログラミングの基本しか実装していませんが、シェルスクリプトは eval や関数の再定義などメタプログラミング的なことも行えるので、継承や多態性といった他の機能も実装できるのではないかと思います。興味がある方はトライしてみてはいかがでしょうか?

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5