几次阅读编程知识时,我都遇到了“回调”概念。
有趣的是,我从来没有找到一个我可以称呼为“回调函数”或“清晰”的解释(对于我来说,几乎我读过的任何解释似乎与其他解释都足够不同,我感到困惑)。
Bash中存在编程的“回调”概念吗?如果是这样,请用一个简单的Bash小示例进行回答。
declarative.bash
有趣的情况,因为该框架显式地利用了配置为需要给定值时调用的功能。
几次阅读编程知识时,我都遇到了“回调”概念。
有趣的是,我从来没有找到一个我可以称呼为“回调函数”或“清晰”的解释(对于我来说,几乎我读过的任何解释似乎与其他解释都足够不同,我感到困惑)。
Bash中存在编程的“回调”概念吗?如果是这样,请用一个简单的Bash小示例进行回答。
declarative.bash
有趣的情况,因为该框架显式地利用了配置为需要给定值时调用的功能。
Answers:
在典型的命令式编程中,您编写指令序列,并使用显式的控制流程一个接一个地执行它们。例如:
if [ -f file1 ]; then # If file1 exists ...
cp file1 file2 # ... create file2 as a copy of a file1
fi
等等
从示例中可以看出,在命令式编程中,您很容易遵循执行流程,始终从任何给定的代码行向上确定其执行上下文,知道您给出的任何指令将根据它们的执行而执行流程中的位置(如果您正在编写函数,则为其呼叫站点的位置)。
当您使用回调时,而不是在地理上放置一组指令,而是描述何时应调用它。其他编程环境中的典型示例是诸如“下载此资源,下载完成后,调用此回调”之类的情况。Bash没有这种通用的回调构造,但是它有回调,用于错误处理和其他一些情况。例如(必须首先了解命令替换和Bash 退出模式才能了解该示例):
#!/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
,即 “亲爱的猛砸,请运行该cleanup
命令,当您退出”(和cleanup
恰好是我前面定义的功能,但它可以是任何东西猛砸理解)。Bash支持所有非致命信号,退出,命令失败和常规调试(您可以指定在每个命令之前运行的回调)。这里的回调是该cleanup
函数,在shell退出之前,Bash会对其进行“回调”。
您可以使用Bash的能力将shell参数作为命令进行评估,以构建面向回调的框架;这在某种程度上超出了此答案的范围,并且可能通过建议传递函数始终涉及回调来引起更多的混乱。请参阅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
在给脚本的所有参数上调用函数。结果是一个脚本,该脚本将其所有参数一个接一个地复制到备份目录。
这种方法允许将函数编写为单一职责:doonall
职责是在其所有参数上一次运行某项;backup
的责任是在备份目录中复制其(唯一的)自变量。双方doonall
并backup
可以在其他情况下,允许更多的代码复用,更好的测试等使用
在这种情况下,回调是backup
函数,我们告诉doonall
它对其每个其他参数“回调”-我们提供doonall
行为(其第一个参数)以及数据(其余的参数)。
(请注意,在第二个示例中演示的那种用例中,我自己不会使用术语“回调”,但这可能是我使用的语言所导致的习惯。我认为这是在周围传递函数或lambdas ,而不是在面向事件的系统中注册回调。)
首先,重要的是要注意,使函数成为回调函数的是其用法,而不是其用途。回调是指从未编写的代码中调用编写的代码时。您要求系统在发生某些特定事件时给您回电。
陷阱是shell编程中回调的一个示例。陷阱是一种回调,它不表示为函数,而是表示为要评估的一段代码。您要让外壳程序在外壳程序收到特定信号时调用您的代码。
回调的另一个示例是命令的-exec
动作find
。该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
}
回调在shell编程中不像在其他一些环境中那样常见,因为shell主要是为简单程序设计的。在数据和控制流更可能在独立编写和分发的部分代码之间来回移动的环境中,回调更为常见:基本系统,各种库,应用程序代码。
foreach_server() { declare callback="$1"; declare server; for server in 192.168.0.1 192.168.0.2 192.168.0.3; do "$callback" "$server"; done; }
你可以为运行foreach_server echo
,foreach_server nslookup
等declare callback="$1"
是一样简单,因为它可以得到,但:回调已在某处被传递,或它不是回调。
“回调”只是作为参数传递给其他函数的函数。
在外壳程序级别上,这仅表示脚本/函数/命令作为参数传递给其他脚本/函数/命令。
现在,作为一个简单的示例,请考虑以下脚本:
$ 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中所做的事情(只是在开玩笑;-)
有些人坚持将“回调”一词限制为“事件处理程序”和/或“关闭”(功能+数据/环境元组)。这是没有办法的普遍 接受的含义。而在狭窄的意义上说“回调”在shell中用处不大的原因之一是因为管道+并行+动态编程功能更为强大,即使您已经在性能方面付出了代价,尝试将shell用作perl
或的笨拙版本python
。
%
在过滤器中不进行插值,则整个过程可以简化为:cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2")
。但这不是那么有用和说明性的恕我直言。
$1 <($2 "$3") <($2 "$4")
的种类。
在bash中实现回调的一种简单方法是接受程序名称作为参数,用作“回调函数”。
# 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中没有闭包。因此,回调函数无法访问调用方的变量。但是,您可以将回调所需的数据存储在环境变量中。将信息从回调传递回调用程序脚本比较棘手。数据可以放入文件中。
如果您的设计允许所有事情都在一个进程中处理,则可以使用shell函数进行回调,并且在这种情况下,回调函数当然可以访问调用方的变量。
只是在其他答案中添加几句话。函数回调对回调函数外部的函数进行操作。为此,需要将要回调的函数的整个定义传递给回调函数,或者应将其代码提供给回调函数。
前者(将代码传递给另一个函数)是可能的,尽管我将跳过一个示例,因为这将涉及复杂性。后者(通过名称传递函数)是一种常见的做法,因为在一个函数的作用域之外声明的变量和函数在该函数中可用,只要它们的定义先于对对其进行操作的函数的调用即可(依次调用该函数) ,以便在调用之前进行声明)。
还要注意,导出函数时也会发生类似的情况。导入函数的shell可能已经准备好框架,并且仅在等待函数定义将其付诸实践。函数导出存在于Bash中,并导致以前严重的问题,btw(称为Shellshock):
我将通过将一个函数传递给另一个函数的另一种方法来完成此答案,这在Bash中没有明确存在。这是通过地址而不是名称传递它。例如,这可以在Perl中找到。Bash既不提供函数,也不提供变量。但是,如您所陈述的,如果您想以Bash为例进行更广泛的介绍,那么您应该知道,功能代码可能驻留在内存中的某个位置,并且该代码可以通过该内存位置进行访问,即称为地址。
bash中最简单的回调示例之一是很多人熟悉但不知道他们实际使用的设计模式的示例:
克朗
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
如您在上面的表格中所见,回调很少是语言的直接功能。他们通常使用现有的语言功能以创新的方式进行编程。可以存储指针/引用/某些代码块/功能/脚本的副本的任何语言都可以实现回调。
watch
和find
(当与-exec
参数一起使用时)
回调是在发生某些事件时调用的函数。用bash
,唯一的事件处理机制与信号,shell退出有关,并扩展到shell错误事件,调试事件和函数/源脚本返回事件。
这是一个无用但简单的回调信号陷阱示例。
首先创建实现回调的脚本:
#!/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
然后在一个终端上运行脚本:
$ ./callback-example
然后将USR1
信号发送到Shell进程。
$ pkill -USR1 callback-example
发送的每个信号应触发在第一个终端中显示类似以下的行:
I've been called at 20180925T003515
I've been called at 20180925T003517
ksh93
作为实现了bash
后来采用的许多功能的shell ,提供了所谓的“学科功能”。bash
当修改或引用外壳程序变量(即读取)时,将调用这些功能(不适用于)。这为更有趣的事件驱动的应用程序打开了道路。
例如,此功能允许图形小部件上的X11 / Xt / Motif样式回调可以在旧版本的图形小部件中实现,该旧版本ksh
包括称为的图形扩展dtksh
。请参见dksh手册。