シェルスクリプトでクロージャ(的な何か)

by @dekokun on 2012/07/24 10:43

Tagged as: ShellScript, Advent Calender.

まえがき

どうも、社会人生活4年目にしてようやく技術ブログを書き始めました@dekokunです。

変態アドベントカレンダー in Summer(http://atnd.org/events/29918) 11日目となります。

前回は@megascusさん大人数開発を支える技術でした。

やること

変態ってなんでしょうね。よくわかりませんが、最近やってて「変態的」と言われたシェルスクリプトに関するTipsを今日は書いていきます。

というわけで、シェルスクリプトでクロージャ(的な何か)を実装したいと思います。

クロージャとは何か、ざっくり言えば、関数に環境が紐付いているものですよね多分。

(ちなみに、クロージャの定義を「親関数の外に参照が渡される内部関数をクロージャと呼ぶ」とかにすると今回のものは全く以てクロージャではなくなります。ですので、このタイトルは「クロージャ(的な何か)」なのです)

やろうと思った経緯

最近、シェルスクリプトでちょっとしたプログラムを書くことが増えてきて、グローバル変数が漏洩しまくるシェルスクリプトに嫌気がさしたので内部状態を持つ関数をつくってみたのでした。

実装・動作

早速、シェルスクリプトで作られたクロージャ(的な何か)の実装を見てみましょう

#!/bin/zsh

func(){
  echo 1
  local pre_func_definition
  local pre_count
  local new_func_definition
  pre_func_definition=$(which func)
  pre_count=$(echo $pre_func_definition | awk 'NR==2, NR==2 {print $2}')
  new_count=$(expr $pre_count + 1)
  new_func_definition=$(echo $pre_func_definition | sed -e "s/echo $pre_count/echo $new_count/")
  eval $new_func_definition
}

echo "1回目"
func
echo "2回目"
func
echo "3回目"
func
echo "4回目"
func
echo "5回目"
func

こちらを実行した際、出力が何になるか分かるでしょうか?

正解は以下の通り

$ ./test.sh
1回目
1
2回目
2
3回目
3
4回目
4
5回目
5

関数が呼び出される度に値がインクリメントされているのがお分かりかと思います。

説明

上記関数が呼び出された際に何を行なっているかというと、whichで自分自身の関数定義を文字列として取得し、それをsedで書き換えevalで評価することによって関数定義自体を書き換えて内部状態(?)をもたせています

では、細かく何を行なっているのか。関数定義をコメント付きでどうぞ。

func(){
  # 以下の"1"の部分が関数が呼び出される毎に書き変わっていく
  echo 1

  # 関数内のみで使用する変数宣言
  local pre_func_definition
  local pre_count
  local new_count
  local new_func_definition

  # 自分自身(func関数)の定義取得
  pre_func_definition=$(which func)
  # 上記echo 1の"1"の部分を取得
  pre_count=$(echo $pre_func_definition | awk 'NR==2, NR==2 {print $2}')
  # 上記で得られた値に1を足している
  new_count=$(expr $pre_count + 1)
  # sedによって、関数定義の文字列中のechoで出力する値を上記で得られた数値で置き換える
  new_func_definition=$(echo $pre_func_definition | sed -e "s/echo $pre_count/echo $new_count/")
  # 上記で書き換えられた関数定義の文字列をevalで評価し、新たな関数を定義する
  eval $new_func_definition
}

うん、こんなことができるんです。

問題点

先ほど、「グローバル変数が漏洩しまくるシェルスクリプトに嫌気がさしたので内部状態を持つ関数をつくってみたのでした。」と書きましたが、グローバル変数が漏洩することよりこんなわけのわからないプログラムが記述されているほうが保守コスト圧倒的に上がるよね。

こんなん私が読まされたらキレるわ。

まぁそれはいいとして、上記クロージャ(的な何か)には現在、以下問題点を抱えております。

  • bashでは動かない…orz

shebangに“/bin/zsh”とあるところから「何か怪しいぞ」と思った人はご明察、bashで動かないのです。。

致命的です。。。。zshスクリプトなんて使わねぇよ!!!

なぜ動かないかといいますと、bashのwhichが関数の文字列をとってきてくれないからです。(他にも、bashだとevalが改行文字含んだ関数定義を解釈してくれないっぽいという問題もあるが、それはどうにでもなる)

誰か、bashで関数定義の文字列を取得するコマンドを知っていたら教えていただけたらと思います。是非!!!

質問

ちなみに、教えて欲しいのですが、これまで、ずっと「クロージャ(的な何か)」で通してきましたが、実際、こういうのって一般的にクロージャっていうんですか…???

新たな関数を関数内で定義しているだけなので、「内部状態を持つ」とすら言えないのかもしれない…

ご意見募集中。

以上、11日目のアドベントカレンダーはこれにて終了。次回はもっと変態的なネタをもってこれたらなと思います。

今後など

現在、HaskellとGitHubPagesを使用したブログへの移行遂行中なので、次回はそっちで書けたらなと (というか、アドベントカレンダーって期間が終わるまで何週もするんですね。やばい!もうネタ切れ!!もっと変態ネタ集めなきゃ!!)

この日記は以前書いた別のブログの日記を移設したものです(2012/08/05)

comments powered by Disqus