#!/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
shopt -s globstar
call_on_regular_files () {
declare callback="$1"
declare file
for file in **/*; do
if [[ -f $file ]]; then
"$callback" "$file"
fi
done
}
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
#! /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'
#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
#!/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
然后在一个终端中运行脚本:
$ ./回调示例
在另一个上,将USR1信号发送到 shell 进程。
$ pkill -USR1 callback-example
发送的每个信号都应触发在第一个终端中显示如下行:
I've been called at 20180925T003515
I've been called at 20180925T003517
在典型的命令式编程中,您编写指令序列,它们一个接一个地执行,具有显式控制流。例如:
等等
从示例中可以看出,在命令式编程中,您很容易遵循执行流程,总是从任何给定的代码行开始确定其执行上下文,知道您给出的任何指令都将作为它们的结果而被执行流中的位置(或它们的呼叫站点的位置,如果您正在编写函数)。
回调如何改变流程
当您使用回调时,您不是在“地理上”放置一组指令的使用,而是描述何时应该调用它。其他编程环境中的典型例子是“下载该资源,下载完成后调用该回调”等情况。Bash 没有这种通用的回调构造,但它确实有回调,用于错误处理和其他一些情况;例如(必须先了解命令替换和 Bash退出模式才能理解该示例):
如果您想自己尝试一下,请将上述内容保存在一个文件中,例如
cleanUpOnExit.sh
,使其可执行并运行它:我的代码从不显式调用该
cleanup
函数;它告诉 Bash 何时调用它,使用trap cleanup EXIT
,即“亲爱的 Bash,请在cleanup
退出时运行命令”(cleanup
恰好是我之前定义的一个函数,但它可以是 Bash 理解的任何东西)。Bash 支持所有非致命信号、退出、命令失败和一般调试(您可以指定在每个命令之前运行的回调)。这里的回调是cleanup
函数,它在 shell 退出之前被 Bash “回调”。您可以使用 Bash 将 shell 参数评估为命令的能力来构建面向回调的框架;这有点超出了这个答案的范围,并且可能会通过暗示传递函数总是涉及回调而引起更多的混乱。有关底层功能的一些示例,请参阅Bash:将函数作为参数传递。与事件处理回调一样,这里的想法是函数可以将数据作为参数,但也可以将其他函数作为参数——这允许调用者提供行为以及数据。这种方法的一个简单示例可能如下所示
(我知道这有点没用,因为
cp
可以处理多个文件,这只是为了说明。)在这里,我们创建一个函数 ,
doonall
它接受另一个命令,作为参数,并将其应用于其余参数;然后我们用它来调用backup
给脚本的所有参数的函数。结果是一个脚本,将其所有参数一一复制到备份目录。这种方法允许编写具有单一职责的函数:
doonall
的职责是在其所有参数上运行一些东西,一次一个;backup
的职责是在备份目录中制作其(唯一)参数的副本。两者都doonall
可以backup
在其他上下文中使用,这允许更多的代码重用、更好的测试等。在这种情况下,回调是一个
backup
函数,我们告诉doonall
它“回调”它的每个其他参数——我们提供doonall
行为(它的第一个参数)以及数据(其余参数)。(请注意,在第二个示例中演示的那种用例中,我自己不会使用术语“回调”,但这可能是我使用的语言产生的一种习惯。我认为这是传递函数或 lambdas ,而不是在面向事件的系统中注册回调。)
首先,重要的是要注意使函数成为回调函数的原因是它的使用方式,而不是它的作用。回调是指从您未编写的代码中调用您编写的代码。当某些特定事件发生时,您要求系统给您回电。
shell 编程中回调的一个例子是陷阱。陷阱是一个回调,它不表示为函数,而是表示为一段要评估的代码。当外壳接收到特定信号时,您要求外壳调用您的代码。
回调的另一个例子是命令的
-exec
动作find
。该find
命令的工作是递归地遍历目录并依次处理每个文件。默认情况下,处理是打印文件名(隐式-print
),但-exec
处理是运行您指定的命令。这符合回调的定义,尽管它不是很灵活,因为回调在单独的进程中运行。如果你实现了一个类似查找的函数,你可以让它使用一个回调函数来调用每个文件。这是一个超简化的 find-like 函数,它以函数名(或外部命令名)作为参数,并在当前目录及其子目录中的所有常规文件上调用它。该函数用作每次
call_on_regular_files
找到常规文件时调用的回调。回调在 shell 编程中不像在其他一些环境中那样常见,因为 shell 主要是为简单程序设计的。回调在数据和控制流更可能在独立编写和分发的代码部分之间来回移动的环境中更为常见:基础系统、各种库、应用程序代码。
“回调”只是作为参数传递给其他函数的函数。
在 shell 级别,这仅仅意味着脚本/函数/命令作为参数传递给其他脚本/函数/命令。
现在,举一个简单的例子,考虑以下脚本:
有概要
将应用于
filter
每个file
参数,然后command
以过滤器的输出作为参数调用。例如:
这与您在 lisp 中可以做的非常接近(开个玩笑;-))
有些人坚持将“回调”术语限制为“事件处理程序”和/或“闭包”(函数+数据/环境元组);这绝不是普遍 接受的意思。狭义上的“回调”在 shell 中没有多大用处的一个原因是,管道 + 并行性 + 动态编程能力要强大得多,而且你已经在性能方面为它们付出了代价,即使你尝试将 shell 用作
perl
or的笨重版本python
。有点儿。
在 bash 中实现回调的一种简单方法是接受程序名称作为参数,它充当“回调函数”。
这将像这样使用:
当然,你在 bash 中没有闭包。因此,回调函数无法访问调用方的变量。但是,您可以将回调所需的数据存储在环境变量中。将信息从回调传递回调用程序脚本比较棘手。可以将数据放入文件中。
如果您的设计允许在单个进程中处理所有内容,则可以使用 shell 函数进行回调,在这种情况下,回调函数当然可以访问调用者端的变量。
只是在其他答案中添加几句话。函数回调对回调函数外部的函数进行操作。为此,需要将要回调的函数的整个定义传递给回调函数,或者它的代码应该可供回调函数使用。
前者(将代码传递给另一个函数)是可能的,但我将跳过一个示例,因为这会涉及复杂性。后者(通过名称传递函数)是一种常见的做法,因为在一个函数范围之外声明的变量和函数在该函数中是可用的,只要它们的定义在调用对它们进行操作的函数之前(反过来,在它被调用之前声明)。
另请注意,导出函数时也会发生类似的事情。导入函数的 shell 可能已经准备好框架,并且正在等待函数定义将它们付诸行动。函数导出存在于 Bash 中并导致以前的严重问题,顺便说一句(称为 Shellshock):
我将使用另一种将函数传递给另一个函数的方法来完成这个答案,这在 Bash 中没有明确存在。这个是通过地址而不是名称传递的。例如,这可以在 Perl 中找到。Bash 既不为函数提供这种方式,也不为变量提供这种方式。但是,如果,正如您所说,您希望以 Bash 为例进行更广泛的了解,那么您应该知道,函数代码可能驻留在内存中的某个位置,并且该代码可能会被该内存位置访问,即叫它的地址。
bash 中回调的最简单示例之一是很多人熟悉但没有意识到他们实际使用的设计模式:
cron
Cron 允许您指定一个可执行文件(二进制文件或脚本),当满足某些条件(时间规范)时,cron 程序将回调该可执行文件
假设您有一个名为
doEveryDay.sh
. 编写脚本的非回调方式是:写它的回调方式很简单:
然后在 crontab 你会设置类似
然后,您无需编写代码来等待事件触发,而是依靠
cron
回调您的代码。现在,考虑如何在 bash 中编写此代码。
您将如何在 bash 中执行另一个脚本/函数?
让我们写一个函数:
现在您已经创建了一个接受回调的函数。你可以简单地这样称呼它:
当然,函数 every24hours 永远不会返回。Bash 有点独特,因为我们可以很容易地使其异步并通过附加来生成一个进程
&
:如果您不希望将此作为函数,则可以将其作为脚本执行:
如您所见,bash 中的回调是微不足道的。很简单:
调用回调很简单:
正如您在上面的表格中看到的,回调很少是语言的直接特征。他们通常使用现有的语言功能以创造性的方式进行编程。任何可以存储某些代码块/函数/脚本的指针/引用/副本的语言都可以实现回调。
回调是在某些事件发生时调用的函数。使用
bash
,唯一的事件处理机制与信号、shell 退出相关,并扩展到 shell 错误事件、调试事件和函数/源脚本返回事件。这是一个利用信号陷阱的无用但简单的回调示例。
首先创建实现回调的脚本:
然后在一个终端中运行脚本:
$ ./回调示例
在另一个上,将
USR1
信号发送到 shell 进程。发送的每个信号都应触发在第一个终端中显示如下行:
ksh93
,作为实现许多bash
后来采用的功能的外壳,提供了它所谓的“纪律功能”。bash
当修改或引用(即读取)shell 变量时,将调用这些函数,这些函数不适用于。这为更有趣的事件驱动应用程序开辟了道路。例如,此功能允许图形小部件上的 X11/Xt/Motif 样式回调在
ksh
包含图形扩展名的旧版本中实现,称为dtksh
. 请参阅dksh 手册。