動機
新しい計算機への環境セットアップのスクリプトなどで、Python
/Perl
/....といった言語に頼らずシェルスクリプトでいくつか作りつづけて久しいのだが、いまいち保守性がよくない。微妙に違う似たような同じコードがいろんなところにバラバラに書かれていたりする。なんとかならないだろうか?
- 新しいスクリプトAを書いているときに、前書いたスクリプトBで使った関数を使いたい。でも、
source B
とかしちゃうと、スクリプトBが実行されてしまう。たとえば、Python
でいうところのpythonだったら(スクリプトB)def function_in_b (): ... def main_b (): function_in_b() if __name__ == '__main__': main_b()
pythonだったら(スクリプトA)import B def main_a (): function_in_b if __name__ == '__main__': main_a()
- シンプルイズベストという意味では、「汎用的な作業のための単機能なシェルスクリプトをいっぱい作って外部コマンドとして実行する」というのが正しい姿かもしれない。ただし「汎用的=多用される」ということで、場面によっては実行時のオーバーヘッドが問題になるし、やりたいデータのやり取りはパイプと終了ステータスだけではないかも。ということで、(急いでいる時にはとくに)同じコードをコピペして逃げてしまう。その場合には、その後のバグ修正対応がスクリプトごとにまちまちになりがち。シェル関数を定義して
source
(.
)するのがいいのはわかっているんだけど...。 - たとえばシェル関数を定義して使おうとしても、シェル関数ごとに1つのファイルにしていちいち
source
(.
) で読み込みこむのは面倒. 一方で1つのファイルに沢山のシェル関数をまとめておいたものを読み込むと、使わない関数の定義が増えるのはうざいし、シェル変数とか関数名のバッティングとか副作用も怖い。たとえば、Python
でいうところのpythonだったら...from something import function1, function2, ...
- 今書いているシェルスクリプトに含まれている関数名が、
source
(.
)しようとするファイルに書かれている関数名と被っちゃっているかもしれない。単純に上書きされないようにしたいのだが......
シェル関数をsource
できて、コマンドとしても実行できるようにするには?
まず、動機の1点目と2点目について、bash
の場合にはスクリプト内部でsource
(.
)されたか、コマンド(新しいプロセスとして)実行されたかは、特殊変数$BASH_SOURCE
と$0
の比較で判定可能なのでなんとかなりそうだ。(緩募: zsh
とか別の*シェルでのやり方を教えて欲しい。)
#!/usr/bin/env bash
function sample1 () {
echo "Caling ${FUNCNAME[0]}"
}
if [ "$0" == "${BASH_SOURCE[0]:-$0}" ]; then
sample1 # Invoked as new process
exit $?
else
return # Read by source.
fi
# Something (data for scripts) can be embedded in the following lines.
$ ./sample1.sh
Caling sample1
$ . ./sample1.sh
$ declare -F sample1
sample1
$ sample1
Caling sample1
これで、最初にあげたpython
の例のようなことが可能になる。ちなみに${FUNCNAME[@]}
はbash
の特殊変数で、実行中の関数名、呼び出し元の関数名、さらに上の関数名...が入っている。上記のサンプルの重要な備忘録的ポイントとしては、(実行に必要なデータを埋め込んだりするなどのために)ファイルの途中でスクリプトの実行/読み込みを中断したいときに、コマンドとして実行された場合にはexit
を、source
(.
)された場合にはreturn
を、という使い分けが必要である! コマンドとして実行された場合にはreturn
はエラーを吐くし、exit
が入ったスクリプトをsource
(.
)すると、source
元のプロセスが終わってしまうので危険。
複数のシェル関数をまとめて1つのファイルに記述
-
動機の3点目について、一つのファイルに書いた関数の使い分けについての対処方としてまず思いつくのは、同じファイルに対して実行したいシェル関数毎に対応するシンボリックリンクを作って使い分ける方法である。
sample_src.sh#!/usr/bin/env bash function sampleA () { echo "Caling ${FUNCNAME[0]}" } function sampleB () { echo "Caling another ${FUNCNAME[0]}" } if [ "$0" == "${BASH_SOURCE[0]:-$0}" ]; then # Invoked as new process run_as="$(basename "${BASH_SOURCE[0]%.sh}")" "${run_as}" exit $? else return # Read by source. fi
上記の実行例$ ln -s sample_src.sh sampleA.sh $ ln -s sample_src.sh sampleB.sh $ ./sampleA.sh # 実体はsample_src.shだがsampleAが実行される Caling sampleA $ ./sampleB.sh # 実体はsample_src.shだがsampleBが実行される Caling another sampleB $ unset -f sampleA sampleB $ source sample_src.sh # sampleA sampleB両方が定義される。 $ declare -F sampleA sampleB sampleA sampleB $ sampleA Caling sampleA $ sampleB Caling another sampleB
この例ですと、
sampleA.sh
をsourceしても、sampleB.sh
をソースしても両方のシェル関数の定義がよみこまれてしまいますし、シンボリックリンクの名前をつけ間違えたり、元のファイル名で実行するとエラーを吐いてしまいます。上記sampleの実行エラー例$ ./sample_src.sh ./sample_src.sh: 行 14: sample_src: コマンドが見つかりません
-
これらを改良した版として,下記のような例はどうでしょう。
sample_mod.sh#!/usr/bin/env bash run_as="$(basename "${BASH_SOURCE[0]%.sh}")" if [ "$0" == "${BASH_SOURCE[0]:-$0}" -o "${run_as}" == 'sampleA' -o "${run_as}" == 'sample_mod' ]; then function sampleA () { echo "Caling ${FUNCNAME[0]}" } fi if [ "$0" == "${BASH_SOURCE[0]:-$0}" -o "${run_as}" == 'sampleB' -o "${run_as}" == 'sample_mod' ]; then function sampleB () { echo "Caling another ${FUNCNAME[0]}" } fi if [ "$0" == "${BASH_SOURCE[0]:-$0}" ]; then # Invoked as new process if declare -F "${run_as}" 1>/dev/null 2>&1; then "${run_as}" exit $? fi exit 1 else return # Read by source. fi
これを実行すると元ファイルを実行してもエラーを吐かなくできます。
sample_mod.shの実行例$ ln -s sample_mod.sh sampleA.sh $ ln -s sample_mod.sh sampleB.sh $ ./sampleA.sh Caling sampleA $ ./sampleB.sh Caling another sampleB $ ./sample_mod.sh $
また、
source
('.')した場合でも関数定義を使い分けられます。sample_mod.shの実行例$ unset -f sampleA sampleB ; source sample_mod.sh ; declare -F sampleA sampleB sampleA sampleB $ unset -f sampleA sampleB ; source sampleA.sh ; declare -F sampleA sampleB sampleA $ unset -f sampleA sampleB ; source sampleB.sh ; declare -F sampleA sampleB sampleB
ただし、
source
('.')した場合には副作用が発生してしまいます。sample_mod.shの副作用$ unset run_as; ./sample_mod.sh ; echo "${run_as:?}" bash: run_as: undefined $ unset run_as; source sample_mod.sh ; echo "${run_as:?}" # run_asという変数が残る sample_mod $ unset run_as; source sampleA.sh ; echo "${run_as:?}" # run_asという変数が残る sampleA $ unset run_as; source sampleB.sh ; echo "${run_as:?}" # run_asという変数が残る sampleB
source
('.')で実行される部分に変数定義を加えてしまったため、呼出後に変数が新しい定義されて残っています。もし呼び出し元のシェル環境やスクリプトで同名の変数を使っていたら上書きしてしまいますし、このスクリプト中でunset
するのも危険です。 この単純な例では、定義している変数${run_as}
の代わりに、その値である"$(basename "${BASH_SOURCE[0]%.sh}")"
を毎回使えば副作用はなくなりますが、毎回サブシェルを呼んで同じ変数展開を繰り返すのは避けたい気がします。(中で定義したいシェル関数の数に比例して増えてしまう。)
呼び出し元への副作用の回避
もっとより複雑なことをしたくなった場合にはシェル変数の数が増えてしまうのは避けらないと思います。変数名が増えてもsource
(.
)した場合に元の環境に対して副作用を起こさないようにするためには、シェル関数の定義をシェル関数に入れてしまうという手が使えます。もちろん殻となるシェル関数の名前は衝突しないように気をつける必要がありますが、より変数定義が増えた場合でもチェックすべき名前の数は増えません。
#!/usr/bin/env bash
if [ "$0" != "${BASH_SOURCE[0]:-$0}" ] && declare -F def_samples 1>/dev/null 2>&1 ; then
echo "Error: function name confliction: def_samples"
return 1
fi
function def_samples () {
local run_as="${1:-$(basename "${BASH_SOURCE[0]%.sh}")}"
if [ "$0" == "${BASH_SOURCE[0]:-$0}" -o "${run_as}" == 'sampleA' -o "${run_as}" == 'sample_mod2' ]; then
function sampleA () {
echo "Caling ${FUNCNAME[0]}"
}
fi
if [ "$0" == "${BASH_SOURCE[0]:-$0}" -o "${run_as}" == 'sampleB' -o "${run_as}" == 'sample_mod2' ]; then
function sampleB () {
echo "Caling another ${FUNCNAME[0]}"
}
fi
unset -f def_samples
}
if [ "$0" == "${BASH_SOURCE[0]:-$0}" ]; then
# Invoked as new process
run_as="$(basename "${BASH_SOURCE[0]%.sh}")"
def_samples "${run_as}"
if declare -F "${run_as}" 1>/dev/null 2>&1; then
"${run_as}"
exit $?
fi
exit 1
else
def_samples
return 0 # Read by source.
fi
この例では副作用は解消されています。
$ ln -s sample_mod2.sh sampleA.sh
$ ln -s sample_mod2.sh sampleB.sh
$ unset -f sampleA sampleB def_samples ; unset run_as
$ source ./sample_mod2.sh ; declare -F sampleA sampleB def_samples ; echo ${run_as:?}
sampleA
sampleB
bash: run_as: parameter null or not set # run_asは未定義
$ unset -f sampleA sampleB def_samples ; unset run_as
$ source ./sampleA.sh ; declare -F sampleA sampleB def_samples ; echo ${run_as:?}
sampleA
bash: run_as: parameter null or not set # run_asは未定義
$ unset -f sampleA sampleB def_samples ; unset run_as
$ source ./sampleB.sh ; declare -F sampleA sampleB def_samples ; echo ${run_as:?}
sampleB
bash: run_as: parameter null or not set # run_asは未定義
上記の実行例では、どの関数定義が読み込まれたか確認するまえに呼び出し前に関数定義を消していますが、もしsource
(.
)する元ですでにsampleA,sampleBという関数定義があった場合には、単純に上書きしてしまいますね
シェル関数名の衝突の問題は回避できないか?
先程のsample_mod2.sh
では、個々のシェル関数の定義を担う殻となる関数def_samples
については、すでに同名の関数があった場合にはエラーを吐いて何もしないようになっている。どうすればこの問題を回避できるだろうか? 素直に思いつくのは、あらかじめ元の定義を変数に書いておいて、あとで復元する方法である。単純化した例で試してみる。
#!/usr/bin/env bash
def_work_bak="$(declare -f work)"
function work () {
echo "${FUNCNAME[0]} : Modified definition"
unset -f work
[ -n "${def_work_bak}" ] && eval "${def_work_bak}"
return
}
work
$ function work () { echo "${FUNCNAME[0]} : Original definition"; }
$ work # もともとのwork()の動作。
work : Original definition
$ . ./solve_name_conflict1.sh # 関数work()が定義されてる状態で、新しいworkが定義されて実行される
work : Modified definition
$ work # もともとのwork()の定義が実行される。
work : Original definition
この例では確かにシェル関数work
の定義については、元の定義が保存されている。でも、新たにdef_work_bak
という名の(ローカルでない)シェル変数を使用してしまっており、名前衝突回避という意味では全く問題が解決できていない。 もう一工夫必要である。
名前衝突回避のためには、このdef_work_bak
という変数を関数の中のローカル変数として定義したい。ここで問題となるのは、シェル変数の定義の中のローカル変数の値として、"$(declare -f work)"
という実行時に展開評価されるプロセス置換文ではなく、シェル関数の定義時にdeclare -f work
の実行結果を渡す必要があるという点である。
#!/usr/bin/env bash
function work () {
local def_work_bak="$(declare -f work)"
echo "${FUNCNAME[0]} : Modified definition"
unset -f work
[ -n "${def_work_bak}" ] && eval "${def_work_bak}"
return
}
work
$ function work () { echo "${FUNCNAME[0]} : Original definition"; }
$ work # もともとのwork()の動作。
work : Original definition
$ . ./solve_name_conflict_ng1.sh # 関数work()が定義されてる状態で、新しいworkが定義されて実行される
work : Modified definition
$ work # 新しいwork()のままで復元されていない。
work : Modified definition
このダメな例では、 新しいシェル関数を定義した後の実行時点で評価されるため、上書きされた後のシェル関数定義を再現してしまうので、元の関数定義が消えてしまう。これの改良版が下記である。
#!/usr/bin/env bash
. /dev/stdin <<<"$(
cat - <(declare -f work | "${SED:-sed}" -Ee 's/([\"\$\\])/\\\1/g') <<'END1' ; cat <<'END2'
function work () {
local def_work_bak="
END1
"
echo "${FUNCNAME[0]} : Modified definition"
unset -f work
[ -n "${def_work_bak}" ] && eval "${def_work_bak}"
return
}
END2
)"
work
bash
では、source
('.')の読み込みファイルとしてプロセス置換(. <(cat - .... )
)を指定してもうまく動作しないので代わりにヒアストリングを使用した。(StackOverflowの記事を参照)
$ function work () { echo "${FUNCNAME[0]} : Original definition"; }
$ work # もともとのwork()の動作。
work : Original definition
$ . ./solve_name_conflict2.sh # 関数work()が定義されてる状態で、新しいworkが定義されて実行
work : Modified definition
$ work # もともとのwork()の定義が実行される。
work : Original definition
$ declare -f work # もともとのwork()の定義が復元されている。
work ()
{
echo "${FUNCNAME[0]} : Original definition"
}
期待通り、元のシェル関数定義に戻りました。
上記で使った、ヒアドキュメントを連続して使用する方法については、ここで見つけました。
ちょっとしたポイントとしては、declareで読み込んだシェル関数定義は、ローカル変数def_work_bak
ところでに入れられるときにダブルクォーテーション("
)で囲んで代入されているので、この変数が評価されるときにはエスケープ(\"
)してないといけないし、実行時ではなく定義時に変数展開されてしまわないように($
)もエスケープ(\$
)しないといけないところですね。
もう少しメンテしやすい名前衝突回避方法はないか?
前節の例で、期待通りに動作させることはテクニカルには可能であることは分かったが、正直なところヒアドキュメントで関数を分けて”文字列”として書いたりするのは、メンテナンス性が絶望的に悪い。
エディターのシンタックスハイライトや、カッコ({ ... }
,(...)
)や引用符(' ... '
, " ... "
)の対応のチェックが働かなくなってしまう。こんな機械的に追加できるコードはヒトがやる必要もない。
というので、さらにちょっと改良してみたものが下記です。
#!/usr/bin/env bash
. /dev/stdin <<< "$(
"${SED:-sed}" -nE \
-e '/^ *#{3,} *___BEGIN_FUNC_DEF___ *#{3,}/,/^ *#{3,} *___END_FUNC_DEF___ *#{3,}/ {
/___(BEGIN|END)_FUNC_DEF___/d ;
/__ORIGINAL_DEFINITION_HERE__/{
s/__ORIGINAL_DEFINITION_HERE__.*$//p; q ;
};
p ;
} ;' "${BASH_SOURCE[0]}";
declare -f work | "${SED:-sed}" -Ee 's/([\"\$\\])/\\\1/g' ;
"${SED:-sed}" -nE \
-e '/^ *#{3,} *___BEGIN_FUNC_DEF___ *#{3,}/,/^ *#{3,} *___END_FUNC_DEF___ *#{3,}/ {
/__ORIGINAL_DEFINITION_HERE__/,/___END_FUNC_DEF___/ {
s/^.*__ORIGINAL_DEFINITION_HERE__//;
/___(BEGIN|END)_FUNC_DEF___/ d; p ;
} ;
}' "${BASH_SOURCE[0]}"
)"
work
if [ "$0" == "${BASH_SOURCE[0]}" ]; then
exit
else
return
fi
### ___BEGIN_FUNC_DEF___ ###
function work () {
local def_work_bak="__ORIGINAL_DEFINITION_HERE__"
echo "${FUNCNAME[0]} : Modified definition"
unset -f work
[ -n "${def_work_bak}" ] && eval "${def_work_bak}"
return
}
### ___END_FUNC_DEF___ ###
この例ではヒトがコーディングする部分は、最後の実行されない部分にデータとして書いておいて、実際に読み込む際にはsed
で自分自身のファイルを読み込んで、キーワード__ORIGINAL_DEFINITION_HERE__
の部分を関数定義で置き換えかえます。この実装ではあえて、
-
sed
でキーワードの前までを抽出して出力 -
declare
で得たシェル関数定義をエスケープして出力 -
sed
でキーワードの以後を抽出して出力
と組み合わせています。これは、単に
-e "s/__ORIGINAL_DEFINITION_HERE__/$(declare -f work)/"
としてしまうと、$(declare -f work)
の中にsedで特別な意味をもつメタ文字(/
など)をエスケープする方法が複雑そうだからです。
$ function work () { echo "${FUNCNAME[0]} : Original definition"; }
$ work # もともとのwork()の動作。
work : Original definition
$ . ./solve_name_conflict2mod.sh # 関数work()が定義されてる状態で、新しいworkが定義されて実行
work : Modified definition
$ work # もともとのwork()の定義が実行される。
work : Original definition
$ declare -f work # もともとのwork()の定義が復元されている。
work ()
{
echo "${FUNCNAME[0]} : Original definition"
}
これならメンテナンス性は損なわれないと思います。
殻のシェル関数の名前衝突問題を回避して複数のシェル関数をまとめて1つのファイルに記述したサンプル
前節の最後で、一回しか使わないシェル関数で名前衝突問題を回避する方法を見つけました。これを最初にやりたかったことを盛り込んだスクリプトに反映させてみたのが、下記の例です。
#!/usr/bin/env bash
. /dev/stdin <<< "$(
"${SED:-sed}" -nE \
-e '/^ *#{3,} *___BEGIN_DEF_SAMPLE_CODE___ *#{3,}/,/^ *#{3,} *___END_DEF_SAMPLE_CODE___ *#{3,}/ {
/___(BEGIN|END)_DEF_SAMPLE_CODE___/d ;
/__DEF_SAMPLE_CODE_ORIGINAL__/{
s/__DEF_SAMPLE_CODE_ORIGINAL__.*$//p; q ;
};
p ;
} ;' "${BASH_SOURCE[0]}";
declare -f def_samples | "${SED:-sed}" -Ee 's/([\"\$\\])/\\\1/g' ;
"${SED:-sed}" -nE \
-e '/^ *#{3,} *___BEGIN_DEF_SAMPLE_CODE___ *#{3,}/,/^ *#{3,} *___END_DEF_SAMPLE_CODE___ *#{3,}/ {
/__DEF_SAMPLE_CODE_ORIGINAL__/,/___END_DEF_SAMPLE_CODE___/ {
s/^.*__DEF_SAMPLE_CODE_ORIGINAL__//;
/___(BEGIN|END)_DEF_SAMPLE_CODE___/ d; p ;
} ;
}' "${BASH_SOURCE[0]}"
)"
if [ "$0" == "${BASH_SOURCE[0]:-$0}" ]; then
# Invoked as new process
run_as="$(basename "${BASH_SOURCE[0]%.sh}")"
def_samples "${run_as}"
if declare -F "${run_as}" 1>/dev/null 2>&1; then
"${run_as}"
exit $?
fi
exit 1
else
def_samples
return 0 # Read by source.
fi
# Something (data for scripts) can be embedded in the following lines.
### ___BEGIN_DEF_SAMPLE_CODE___ ###
function def_samples () {
local run_as="${1:-$(basename "${BASH_SOURCE[1]%.sh}")}"
local def_def_samples_bak="__DEF_SAMPLE_CODE_ORIGINAL__"
if [ "$0" == "${BASH_SOURCE[1]:-$0}" -o "${run_as}" == 'sampleA' -o "${run_as}" == 'sample_mod3' ]; then
function sampleA () {
echo "Caling ${FUNCNAME[0]}"
}
fi
if [ "$0" == "${BASH_SOURCE[1]:-$0}" -o "${run_as}" == 'sampleB' -o "${run_as}" == 'sample_mod3' ]; then
function sampleB () {
echo "Caling another ${FUNCNAME[0]}"
}
fi
unset -f def_samples
[ -n "${def_def_samples_bak}" ] && eval "${def_def_samples_bak}"
return
}
### ___END_DEF_SAMPLE_CODE___ ###
ここで重要なポイントとなるのが、sample_mod2.sh
のdef_samples ()
の定義の中では"${BASH_SOURCE[0]}"
であったところを、sample_mod3.sh
のdef_samples ()
の定義の中では"${BASH_SOURCE[1]}"
となっています。これはsed
などで整形したシェル関数定義をヒアストリングでさらにsource
(.
)しているのでソースのスタック階層が一段深くなってしまっており、
-
"${BASH_SOURCE[0]}"
==/dev/stdin
-
"${BASH_SOURCE[1]}"
==sample_mod3.sh
(定義が書かれているスクリプトファイル)
になってしまったことへの対応です。
$ ln -s sample_mod3.sh sampleA.sh
$ ln -s sample_mod3.sh sampleB.sh
$ function def_samples () { echo "${FUNCNAME[0]} : Original definition"; }
$ unset -f sampleA sampleB ; unset run_as ; . ./sample_mod3.sh ; declare -F sampleA sampleB def_samples ; echo ${run_as?}
sampleA
sampleB
def_samples
bash: run_as: parameter null or not set
$ declare -f def_samples
def_samples ()
{
echo "${FUNCNAME[0]} : Original definition"
}
$ function def_samples () { echo "${FUNCNAME[0]} : Original definition"; }
$ unset -f sampleA sampleB ; unset run_as ; . ./sampleA.sh ; declare -F sampleA sampleB def_samples ; echo ${run_as?}
sampleA
def_samples
bash: run_as: parameter null or not set
$ declare -f def_samples
def_samples ()
{
echo "${FUNCNAME[0]} : Original definition"
}
$ function def_samples () { echo "${FUNCNAME[0]} : Original definition"; }
$ unset -f sampleA sampleB ; unset run_as ; . ./sampleB.sh ; declare -F sampleA sampleB def_samples ; echo ${run_as?}
sampleB
def_samples
bash: run_as: parameter null or not set
$ declare -f def_samples
def_samples ()
{
echo "${FUNCNAME[0]} : Original definition"
}
source
(.
)しても元の環境に余分な関数を残さず、(殻となるシェル関数が)他の定義済みのシェル関数と名前が衝突しても大丈夫なスクリプトのフレームワークができました。
定義する各関数の名前の衝突回避は?
前節の例では、実際に定義して使いたいシェル関数(sampleA
, sampleB
)に関しては名前衝突については何も気にしていない。汎用的に使い回したいシェル関数ほど普遍的な名前をつけたくなるので、名前がかぶりがちである。これに関しては完全な回避方法は思いつかない。次善の策として、元の関数定義を取っておいて後で戻せるようにしてみよう。この場合には元の定義をとっておくためのシェル変数はローカルにできないので、このシェル変数の名前も衝突する危険がある。これにはあまり使わなそうな接頭辞がついた名前にするなどでお茶を濁す。
. /dev/stdin <<< "$(
"${SED:-sed}" -nE \
-e '/^ *#{3,} *___BEGIN_DEF_SAMPLE_CODE___ *#{3,}/,/^ *#{3,} *___END_DEF_SAMPLE_CODE___ *#{3,}/ {
/___(BEGIN|END)_DEF_SAMPLE_CODE___/d ;
/__DEF_SAMPLE_CODE_ORIGINAL__/{
s/__DEF_SAMPLE_CODE_ORIGINAL__.*$//p; q ;
};
p ;
} ;' "${BASH_SOURCE[0]}";
declare -f def_samples | "${SED:-sed}" -Ee 's/([\"\$\\])/\\\1/g' ;
"${SED:-sed}" -nE \
-e '/^ *#{3,} *___BEGIN_DEF_SAMPLE_CODE___ *#{3,}/,/^ *#{3,} *___END_DEF_SAMPLE_CODE___ *#{3,}/ {
/__DEF_SAMPLE_CODE_ORIGINAL__/,/___END_DEF_SAMPLE_CODE___/ {
s/^.*__DEF_SAMPLE_CODE_ORIGINAL__//;
/___(BEGIN|END)_DEF_SAMPLE_CODE___/ d; p ;
} ;
}' "${BASH_SOURCE[0]}"
)"
if [ "$0" == "${BASH_SOURCE[0]:-$0}" ]; then
# Invoked as new process
run_as="$(basename "${BASH_SOURCE[0]%.sh}")"
def_samples "${run_as}"
if declare -F "${run_as}" 1>/dev/null 2>&1; then
"${run_as}"
exit $?
fi
exit 1
else
def_samples
return 0 # Read by source.
fi
# Something (data for scripts) can be embedded in the following lines.
### ___BEGIN_DEF_SAMPLE_CODE___ ###
function def_samples () {
local run_as="${1:-$(basename "${BASH_SOURCE[1]%.sh}")}"
local def_def_samples_bak="__DEF_SAMPLE_CODE_ORIGINAL__"
if [ "$0" == "${BASH_SOURCE[1]:-$0}" -o "${run_as}" == 'sampleA' -o "${run_as}" == 'sample_mod3' ]; then
if declare -f sampleA 1>/dev/null 2>&1 && [ ${opt_not_ovrwrt:-0} -eq 0 ]; then
__sampleA_bk=( "$(declare -f "sampleA")" "${__sampleA_bk[@]}" )
fi
function sampleA () {
echo "Caling ${FUNCNAME[0]}"
}
if declare -f restore_sampleA 1>/dev/null 2>&1 ; then
__restore_sampleA_bk=( "$(declare -f restore_sampleA)" "${__restore_sampleA_bk[@]}" )
fi
restore_sampleA () {
unset -f sampleA
if [ "x$1" == "x-f" -o "x$1" == "x-C" ]; then
if [ "${#__sampleA_bk[@]}" -gt 1 ]; then
__sampleA_bk=( "${__sampleA_bk[$((${#__sampleA_bk[@]}-1))]}" )
fi
if [ "${#__restore_sampleA_bk[@]}" -gt 1 ]; then
__restore_sampleA_bk=( "${__restore_sampleA_bk[$((${#__restore_sampleA_bk[@]}-1))]}" )
fi
fi
if [ ${#__sampleA_bk[@]} -gt 0 ]; then
test -n "${__sampleA_bk[0]}" -a "x$1" \!= "x-C" \
&& eval "${__sampleA_bk[0]}"
if [ ${#__sampleA_bk[@]} -gt 1 ]; then
__sampleA_bk=( "${__sampleA_bk[@]:1}" )
else
unset __sampleA_bk
fi
fi
unset -f restore_sampleA
if [ ${#__restore_sampleA_bk[@]} -gt 0 ]; then
test -n "${__restore_sampleA_bk[0]}" -a "x$1" \!= "x-C" \
&& eval "${__restore_sampleA_bk[0]}"
if [ ${#__restore_sampleA_bk[@]} -gt 1 ]; then
__restore_sampleA_bk=( "${__restore_sampleA_bk[@]:1}" )
else
unset __restore_sampleA_bk
fi
fi
}
fi
if [ "$0" == "${BASH_SOURCE[1]:-$0}" -o "${run_as}" == 'sampleB' -o "${run_as}" == 'sample_mod3' ]; then
if declare -f sampleB 1>/dev/null 2>&1 && [ ${opt_not_ovrwrt:-0} -eq 0 ]; then
__sampleB_bk=( "$(declare -f "sampleB")" "${__sampleB_bk[@]}" )
fi
function sampleB () {
echo "Caling another ${FUNCNAME[0]}"
}
if declare -f restore_sampleB 1>/dev/null 2>&1 ; then
__restore_sampleB_bk=( "$(declare -f restore_sampleB)" "${__restore_sampleB_bk[@]}" )
fi
restore_sampleB () {
unset -f sampleB
if [ "x$1" == "x-f" -o "x$1" == "x-C" ]; then
if [ "${#__sampleB_bk[@]}" -gt 1 ]; then
__sampleB_bk=( "${__sampleB_bk[$((${#__sampleB_bk[@]}-1))]}" )
fi
if [ "${#__restore_sampleB_bk[@]}" -gt 1 ]; then
__restore_sampleB_bk=( "${__restore_sampleB_bk[$((${#__restore_sampleB_bk[@]}-1))]}" )
fi
fi
if [ ${#__sampleB_bk[@]} -gt 0 ]; then
test -n "${__sampleB_bk[0]}" -a "x$1" \!= "x-C" \
&& eval "${__sampleB_bk[0]}"
if [ ${#__sampleB_bk[@]} -gt 1 ]; then
__sampleB_bk=( "${__sampleB_bk[@]:1}" )
else
unset __sampleB_bk
fi
fi
unset -f restore_sampleB
if [ ${#__restore_sampleB_bk[@]} -gt 0 ]; then
test -n "${__restore_sampleB_bk[0]}" -a "x$1" \!= "x-C" \
&& eval "${__restore_sampleB_bk[0]}"
if [ ${#__restore_sampleB_bk[@]} -gt 1 ]; then
__restore_sampleB_bk=( "${__restore_sampleB_bk[@]:1}" )
else
unset __restore_sampleB_bk
fi
fi
}
fi
unset -f def_samples
[ -n "${def_def_samples_bak}" ] && eval "${def_def_samples_bak}"
return
}
### ___END_DEF_SAMPLE_CODE___ ###
この例では、sampleA
がすでに定義されていた場合には、${__sampleA_bk[@]}
という変数にスタックする。この変数からsampleA
を復元するシェル関数restore_sampleA
を定義する。restore_sampleA
というシェル関数自体もバックアップをとって同時に復元する。
$ ln -s sample_mod4.sh sampleA.sh
$ ln -s sample_mod4.sh sampleB.sh
$ function sampleA () { echo "${FUNCNAME[0]} : Original definition"; }$ sampleA
sampleA : Original definition
$ . sampleA.sh # sampleAが定義済みの状態で、sampleAを再定義
$ sampleA # (再定義されたシェル関数の実行)
Caling sampleA
$ restore_sampleA # (もとのシェル関数定義をリストア)
$ sampleA # 元の定義のシェル関数が実行される。
sampleA : Original definition
$ declare -f sampleA restore_sampleA # declareで確認しても元のシェル関数定義に戻っている。
sampleA ()
{
echo "${FUNCNAME[0]} : Original definition"
}
この関数定義のバックアップ、復元するシェル関数の定義の部分は、シェル関数の中身によらず、シェル関数/変数の名前が違うだけなので、機械的に生成するルーチンを作れそうです。
フレームワークとしての実装
これまでに試したものを組み合わせて、シェルスクリプトのルーチンを提供するフレームワークにしてみました。かなり複雑なものになってしまいました...orz. ここまでする意味を問われると答えに窮するところはありますが、単にshell
でできることをためす試みですね。