2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

よく使うシェルスクリプト(bash)のツールの保守性/汎用性を高める試み。

Posted at

動機

新しい計算機への環境セットアップのスクリプトなどで、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とか別の*シェルでのやり方を教えて欲しい。)

sample1.sh
#!/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(.)した場合に元の環境に対して副作用を起こさないようにするためには、シェル関数の定義をシェル関数に入れてしまうという手が使えます。もちろん殻となるシェル関数の名前は衝突しないように気をつける必要がありますが、より変数定義が増えた場合でもチェックすべき名前の数は増えません。

sample_mod2.sh
#!/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

この例では副作用は解消されています。

sample_mod2.shの実行例
$ 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については、すでに同名の関数があった場合にはエラーを吐いて何もしないようになっている。どうすればこの問題を回避できるだろうか? 素直に思いつくのは、あらかじめ元の定義を変数に書いておいて、あとで復元する方法である。単純化した例で試してみる。

solve_name_conflict1.sh
#!/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の実行結果を渡す必要があるという点である。

solve_name_conflict_ng1.sh(ダメな例)
#!/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

このダメな例では、 新しいシェル関数を定義した後の実行時点で評価されるため、上書きされた後のシェル関数定義を再現してしまうので、元の関数定義が消えてしまう。これの改良版が下記である。

solve_name_conflict2.sh
#!/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の記事を参照)

solve_name_conflict2.shの実行例
$ 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ところでに入れられるときにダブルクォーテーション(")で囲んで代入されているので、この変数が評価されるときにはエスケープ(\")してないといけないし、実行時ではなく定義時に変数展開されてしまわないように($)もエスケープ(\$)しないといけないところですね。

もう少しメンテしやすい名前衝突回避方法はないか?

前節の例で、期待通りに動作させることはテクニカルには可能であることは分かったが、正直なところヒアドキュメントで関数を分けて”文字列”として書いたりするのは、メンテナンス性が絶望的に悪い。
エディターのシンタックスハイライトや、カッコ({ ... },(...))や引用符(' ... ' , " ... ")の対応のチェックが働かなくなってしまう。こんな機械的に追加できるコードはヒトがやる必要もない。

というので、さらにちょっと改良してみたものが下記です。

solve_name_conflict2mod.sh
#!/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で特別な意味をもつメタ文字(/など)をエスケープする方法が複雑そうだからです。

solve_name_conflict2mod.sh実行例
$ 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つのファイルに記述したサンプル

前節の最後で、一回しか使わないシェル関数で名前衝突問題を回避する方法を見つけました。これを最初にやりたかったことを盛り込んだスクリプトに反映させてみたのが、下記の例です。

sample_mod3.sh
#!/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.shdef_samples ()の定義の中では"${BASH_SOURCE[0]}"であったところを、sample_mod3.shdef_samples ()の定義の中では"${BASH_SOURCE[1]}"となっています。これはsedなどで整形したシェル関数定義をヒアストリングでさらにsource(.)しているのでソースのスタック階層が一段深くなってしまっており、

  • "${BASH_SOURCE[0]}"==/dev/stdin
  • "${BASH_SOURCE[1]}"==sample_mod3.sh(定義が書かれているスクリプトファイル)
    になってしまったことへの対応です。
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)に関しては名前衝突については何も気にしていない。汎用的に使い回したいシェル関数ほど普遍的な名前をつけたくなるので、名前がかぶりがちである。これに関しては完全な回避方法は思いつかない。次善の策として、元の関数定義を取っておいて後で戻せるようにしてみよう。この場合には元の定義をとっておくためのシェル変数はローカルにできないので、このシェル変数の名前も衝突する危険がある。これにはあまり使わなそうな接頭辞がついた名前にするなどでお茶を濁す。

sample_mod4.sh
. /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というシェル関数自体もバックアップをとって同時に復元する。

sample_mod4.sh実行例
$ 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でできることをためす試みですね。

ファイル置き場

参考文献

2
1
3

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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?