プログラミングについて読んだとき、「コールバック」の概念に出くわしました。
おかしなことに、私はこの用語「コールバック関数」について「教訓的」または「明確」と呼ぶことができる説明を見つけられませんでした(私が読んだほとんどの説明は、他のものとはかなり違って見え、混乱しました)。
プログラミングの「コールバック」概念はBashに存在しますか?もしそうなら、小さくてシンプルなBashの例で答えてください。
一般的な 命令型プログラミング では、一連の命令を記述し、明示的な制御フローを使用して次々に実行されます。例えば:
if [ -f file1 ]; then # If file1 exists ...
cp file1 file2 # ... create file2 as a copy of a file1
fi
等.
例からわかるように、命令型プログラミングでは、実行フローを非常に簡単にたどり、常にコードの特定の行から上に向かって実行コンテキストを決定し、指定した命令はその結果として実行されることを知っています。フロー内の場所(または関数を作成している場合は、呼び出しサイトの場所)。
コールバックを使用する場合、一連の命令を「地理的に」使用する代わりに、いつ呼び出す必要があるかを記述します。他のプログラミング環境の典型的な例は、「このリソースをダウンロードし、ダウンロードが完了したら、このコールバックを呼び出す」などのケースです。 Bashには、この種の一般的なコールバックコンストラクトはありませんが、 error-handling およびその他のいくつかの状況でのコールバックはあります。たとえば(その例を理解するには、まず コマンド置換 とBash exitモード を理解する必要があります):
#!/bin/bash
scripttmp=$(mktemp -d) # Create a temporary directory (these will usually be created under /tmp or /var/tmp/)
cleanup() { # Declare a cleanup function
rm -rf "${scripttmp}" # ... which deletes the temporary directory we just created
}
trap cleanup EXIT # Ask Bash to call cleanup on exit
これを自分で試したい場合は、上記をcleanUpOnExit.sh
などのファイルに保存し、実行可能にして実行します。
chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh
ここのコードでは、cleanup
関数を明示的に呼び出すことはありません。 trap cleanup EXIT
、ieを使用して、いつ呼び出すかをBashに通知します。「Bash様、cleanup
コマンドをexit」(そしてcleanup
はたまたま先に定義した関数ですが、Bashが理解できるものなら何でもかまいません)。 Bashは、すべての致命的でない信号、終了、コマンドの失敗、および一般的なデバッグ(すべてのコマンドの前に実行されるコールバックを指定できます)に対してこれをサポートします。ここでのコールバックはcleanup
関数であり、シェルが終了する直前にBashによって「コールバック」されます。
Bashの機能を使用して、シェルパラメーターをコマンドとして評価し、コールバック指向のフレームワークを構築できます。これはこの回答の範囲をやや超えており、関数の受け渡しには常にコールバックが含まれることを示唆しているため、混乱を招く可能性があります。基本的な機能のいくつかの例については、 Bash:関数をパラメーターとして渡す を参照してください。ここでの考え方は、イベント処理コールバックと同様に、関数はデータをパラメーターとして受け取ることができるということです。これにより、呼び出し元がデータだけでなく動作も提供できるようになります。このアプローチの簡単な例は次のようになります
#!/bin/bash
doonall() {
command="$1"
shift
for arg; do
"${command}" "${arg}"
done
}
backup() {
mkdir -p ~/backup
cp "$1" ~/backup
}
doonall backup "$@"
(cp
は複数のファイルを処理できるため、これは少し役に立たないことを知っています。これは説明のためだけです。)
ここでは、パラメーターとして指定された別のコマンドを受け取り、それを残りのパラメーターに適用する関数doonall
を作成します。次に、それを使用して、スクリプトに指定されたすべてのパラメーターでbackup
関数を呼び出します。結果は、すべての引数を1つずつバックアップディレクトリにコピーするスクリプトです。
この種類のアプローチにより、関数を単一の責任で記述することができます。doonall
の責任は、一度に1つずつすべての引数に対して何かを実行することです。 backup
の責任は、その(唯一の)引数のコピーをバックアップディレクトリに作成することです。 doonall
とbackup
の両方を他のコンテキストで使用できるため、より多くのコードの再利用、より良いテストなどが可能になります。
この場合、コールバックはbackup
関数であり、doonall
に他の各引数で「コールバック」するように指示します— doonall
に動作(最初の引数)を提供します)およびデータ(残りの引数)。
(2番目の例で示した種類の使用例では、私自身は「コールバック」という用語を使用しませんが、それはおそらく私が使用する言語に起因する習慣です。これは、関数またはラムダを渡すと考えています、イベント指向のシステムでコールバックを登録するのではなく)。
まず、関数をコールバック関数にするのは、その機能ではなく、その使用方法であることに注意することが重要です。コールバックとは、作成したコードが、作成していないコードから呼び出された場合です。特定のイベントが発生したときにコールバックするようにシステムに要求しています。
シェルプログラミングでのコールバックの例はトラップです。トラップは、関数としてではなく、評価するコードの一部として表現されるコールバックです。シェルが特定のシグナルを受信したときにコードを呼び出すようにシェルに要求しています。
コールバックのもう1つの例は、find
コマンドの-exec
アクションです。 find
コマンドの仕事は、ディレクトリを再帰的に走査し、各ファイルを順番に処理することです。デフォルトでは、処理はファイル名(暗黙の-print
)を出力することですが、-exec
の場合、処理は指定したコマンドを実行することです。これはコールバックの定義に適合しますが、コールバックは別のプロセスで実行されるため、コールバックの実行時にはあまり柔軟ではありません。
検索のような関数を実装した場合、コールバック関数を使用して各ファイルを呼び出すようにすることができます。以下は、関数名(または外部コマンド名)を引数として取り、現在のディレクトリとそのサブディレクトリ内のすべての通常のファイルで呼び出す、非常に単純化されたfindに似た関数です。この関数は、call_on_regular_files
が通常のファイルを見つけるたびに呼び出されるコールバックとして使用されます。
shopt -s globstar
call_on_regular_files () {
declare callback="$1"
declare file
for file in **/*; do
if [[ -f $file ]]; then
"$callback" "$file"
fi
done
}
シェルは主に単純なプログラム用に設計されているため、シェルプログラミングではコールバックは他のいくつかの環境ほど一般的ではありません。コールバックは、データと制御フローが、ベースシステム、さまざまなライブラリ、アプリケーションコードなど、独立して記述および配布されるコードの部分間を行き来する可能性が高い環境でより一般的です。
「コールバック」は、他の関数の引数として渡される関数です。
シェルレベルでは、それは単に他のスクリプト/関数/コマンドへの引数として渡されるスクリプト/関数/コマンドを意味します。
ここで、簡単な例として、次のスクリプトを考えてみます。
$ cat ~/w/bin/x
#! /bin/bash
cmd=$1; shift
case $1 in *%*) flt=${1//\%/\'%s\'};; *) flt="$1 '%s'";; esac; shift
q="'\\''"; f=${flt//\\/'\\'}; p=`printf "<($f) " "${@//\'/$q}"`
eval "$cmd" "$p"
あらすじ
x command filter [file ...]
filter
を各file
引数に適用し、フィルターの出力を引数としてcommand
を呼び出します。
例えば:
x diff zcat a.gz b.bz # diff gzipped files
x diff3 zcat a.gz b.gz c.gz # same with three-way diff
x diff hd a b # hex diff of binary files
x diff 'zcat % | sort -u' a.gz b.gz # first uncompress the files, then sort+uniq them, then compare them
x 'comm -12' sort a b # find common lines in unsorted files
これは、LISPでできることと非常に似ています(冗談です;-))
一部の人々は、「コールバック」の用語を「イベントハンドラ」や「クロージャ」(関数+データ/環境タプル)に制限することを主張しています。これは決して 一般に受け入れられる の意味ではありません。そして、これらの狭い意味での「コールバック」がシェルであまり使用されない理由の1つは、パイプ+並列処理+動的プログラミング機能が非常に強力であり、シェルをPerl
またはpython
の不格好なバージョンとして使用してみてください。
やや。
Bashでコールバックを実装する簡単な方法の1つは、「コールバック関数」として機能するプログラムの名前をパラメーターとして受け入れることです。
# This is script worker.sh accepts a callback in $1
cb="$1"
....
# Execute the call back, passing 3 parameters
$cb foo bar baz
これは次のように使用されます。
# Invokes mycb.sh as a callback
worker.sh mycb.sh
もちろん、bashにはクロージャーはありません。したがって、コールバック関数は呼び出し側の変数にアクセスできません。ただし、コールバックに必要なデータを環境変数に格納できます。コールバックから呼び出し元のスクリプトに情報を渡すのは難しいです。データはファイルに配置できます。
デザインですべてが1つのプロセスで処理される場合は、コールバックにシェル関数を使用できます。この場合、コールバック関数はもちろん、呼び出し側の変数にアクセスできます。
他の回答にいくつかの単語を追加するだけです。関数コールバックは、コールバックする関数の外部の関数で動作します。これを可能にするには、コールバックされる関数の定義全体を関数のコールバックに渡す必要があるか、そのコードが関数のコールバックで使用できる必要があります。
前者(コードを別の関数に渡すこと)は可能ですが、この例ではスキップするので複雑になります。後者(名前で関数を渡す)は一般的な方法です。1つの関数のスコープ外で宣言された変数と関数は、その定義がそれらを操作する関数の呼び出しの前にある限り、その関数で使用できます(これにより、 、呼び出される前に宣言されるため)。
また、関数がエクスポートされるときにも同様のことが起こります。関数をインポートするシェルは、フレームワークの準備ができていて、関数の定義が機能するのを待っているだけの場合があります。関数のエクスポートはBashにあり、以前は深刻な問題を引き起こしていましたが、これはShellshockと呼ばれていました。
Bashには明示的に存在しない、関数を別の関数に渡すもう1つの方法でこの答えを完成させます。これは名前ではなくアドレスで渡します。これは、たとえばPerlで見つけることができます。 Bashはこの方法を関数にも変数にも提供しません。しかし、あなたが述べているように、例としてBashを使用してより広い視野を持ちたい場合は、関数コードがメモリ内のどこかに存在し、そのコードがそのメモリ位置からアクセスされる可能性があることを知っておく必要があります。そのアドレスを呼び出しました。
Bashでのコールバックの最も単純な例の1つは、多くの人々が精通しているが、実際に使用しているデザインパターンがわからないものです。
cron
Cronを使用すると、いくつかの条件が満たされたときにcronプログラムがコールバックする実行可能ファイル(バイナリまたはスクリプト)を指定できます(時間指定)。
doEveryDay.sh
というスクリプトがあるとします。スクリプトを作成する非コールバック方法は次のとおりです。
#! /bin/bash
while true; do
doSomething
sleep $TWENTY_FOUR_HOURS
done
それを書くためのコールバック方法は単純です:
#! /bin/bash
doSomething
次にcrontabで次のように設定します
0 0 * * * doEveryDay.sh
その場合、イベントがトリガーされるのを待つコードを記述する必要はなく、代わりにcron
を使用してコードをコールバックします。
ここで、このコードをbashでどのように記述するかを検討します。
Bashで別のスクリプト/関数をどのように実行しますか?
関数を書いてみましょう:
function every24hours () {
CALLBACK=$1 ;# assume the only argument passed is
# something we can "call"/execute
while true; do
$CALLBACK ;# simply call the callback
sleep $TWENTY_FOUR_HOURS
done
}
これで、コールバックを受け入れる関数が作成されました。次のように呼び出すだけです。
# "ping" google website every day
every24hours 'curl google.com'
もちろん、24時間ごとの関数は決して戻りません。 Bashは少しユニークであり、&
を追加することで、非同期を非常に簡単に作成してプロセスを生成できます。
every24hours 'curl google.com' &
これを関数として使用したくない場合は、代わりにスクリプトとして実行できます。
#every24hours.sh
CALLBACK=$1 ;# assume the only argument passed is
# something we can "call"/execute
while true; do
$CALLBACK ;# simply call the callback
sleep $TWENTY_FOUR_HOURS
done
ご覧のとおり、bashのコールバックは簡単です。それは単に:
CALLBACK_SCRIPT=$3 ;# or some other
# argument to
# function/script
そして、コールバックの呼び出しは単純です:
$SOME_CALLBACK_FUNCTION_OR_SCRIPT
上記のフォームを見るとわかるように、コールバックが言語の直接的な機能であることはほとんどありません。彼らは通常、既存の言語機能を使用して独創的な方法でプログラミングしています。いくつかのコードブロック/関数/スクリプトのポインタ/参照/コピーを保存できる言語であれば、コールバックを実装できます。
コールバックは、イベントが発生したときに呼び出される関数です。 bash
を使用すると、唯一のイベント処理メカニズムは信号、シェル出口に関連し、シェルエラーイベント、デバッグイベント、関数/ソーススクリプトの戻りイベントに拡張されます。
これは、シグナルトラップを利用した役に立たないが単純なコールバックの例です。
まず、コールバックを実装するスクリプトを作成します。
#!/bin/bash
myCallback() {
echo "I've been called at $(date +%Y%m%dT%H%M%S)"
}
# Set the handler
trap myCallback SIGUSR1
# Main loop. Does nothing useful, essentially waits
while true; do
read foo
done
次に、1つのターミナルでスクリプトを実行します。
$ ./callback-example
もう1つはUSR1
シェルプロセスへのシグナル。
$ pkill -USR1 callback-example
送信された各信号は、最初の端末で次のような行の表示をトリガーする必要があります。
I've been called at 20180925T003515
I've been called at 20180925T003517
ksh93
は、後でbash
が採用した多くの機能を実装するシェルとして、「規律機能」と呼ばれる機能を提供します。 bash
では使用できないこれらの関数は、シェル変数が変更または参照(つまり、読み取り)されるときに呼び出されます。これにより、より興味深いイベント駆動型アプリケーションへの道が開かれます。
たとえば、この機能により、グラフィックウィジェットのX11/Xt/Motifスタイルのコールバックを、ksh
と呼ばれるグラフィック拡張機能を含む古いバージョンのdtksh
に実装できました。 dksh manual を参照してください。