有几次,当我阅读有关编程的内容时,我遇到了“回调”概念。
有趣的是,对于“回调函数”这个术语,我从来没有找到一个可以称之为“说教”或“清晰”的解释(几乎所有我读过的解释在我看来都与其他解释有很大不同,我感到很困惑)。
Bash 中存在编程的“回调”概念吗?如果是这样,请用一个小而简单的 Bash 示例来回答。
答案1
在典型的命令式编程,您编写指令序列,它们会通过明确的控制流一个接一个地执行。例如:
if [ -f file1 ]; then # If file1 exists ...
cp file1 file2 # ... create file2 as a copy of a file1
fi
ETC。
从示例中可以看出,在命令式编程中,您可以非常轻松地遵循执行流程,始终从任何给定的代码行开始确定其执行上下文,并知道您给出的任何指令都将作为其结果而执行。流程中的位置(或者它们的调用站点的位置,如果您正在编写函数)。
回调如何改变流程
当您使用回调时,您不是“按地理位置”放置一组指令的使用,而是描述何时应该调用它。其他编程环境中的典型例子是“下载这个资源,下载完成后调用这个回调”之类的情况。 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
函数;它告诉 Bash 何时调用它,使用trap cleanup EXIT
,IE“亲爱的 Bash,请cleanup
在退出时运行该命令”(cleanup
恰好是我之前定义的一个函数,但它可以是 Bash 理解的任何内容)。 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
行为(它的第一个参数)以及数据(其余参数)。
(请注意,在第二个示例中演示的用例中,我自己不会使用术语“回调”,但这可能是我使用的语言产生的习惯。我认为这是传递函数或 lambda ,而不是在面向事件的系统中注册回调。)
答案2
首先,需要注意的是,函数之所以成为回调函数,是因为它的使用方式,而不是它的作用。回调是指从您未编写的代码中调用您编写的代码。您要求系统在发生某些特定事件时给您回电。
shell 编程中回调的一个例子是陷阱。陷阱是一种回调,它不表示为函数,而是表示为要计算的一段代码。当 shell 收到特定信号时,您要求 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 主要是为简单程序设计的。在数据和控制流更有可能在独立编写和分发的代码部分(基本系统、各种库、应用程序代码)之间来回移动的环境中,回调更为常见。
答案3
“回调”只是作为参数传递给其他函数的函数。
在 shell 级别,这仅仅意味着脚本/函数/命令作为参数传递给其他脚本/函数/命令。
现在,作为一个简单的示例,请考虑以下脚本:
$ 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
。
答案4
只是为了在其他答案中添加几句话。函数回调对回调函数外部的函数进行操作。为了实现这一点,要么需要将要回调的函数的完整定义传递给回调函数,要么其代码应该可供回调函数使用。
前者(将代码传递给另一个函数)是可能的,尽管我将跳过一个示例,因为这会涉及复杂性。后者(按名称传递函数)是一种常见做法,因为在一个函数作用域之外声明的变量和函数在该函数中可用,只要它们的定义先于对对其进行操作的函数的调用(反过来, ,在调用之前声明)。
另请注意,导出函数时也会发生类似的情况。导入函数的 shell 可能已经准备好框架,并且只是等待函数定义将其付诸实践。 Bash 中存在函数导出,并且导致了之前的严重问题,顺便说一句(称为 Shellshock):
- env x='() { :;}; 是什么意思?命令' bash 执行的操作以及为什么它不安全?
- shellshock (CVE-2014-6271/7169) 错误是什么时候引入的?完全修复该错误的补丁是什么?
我将用另一种将函数传递给另一个函数的方法来完成这个答案,这种方法在 Bash 中没有明确提供。这种方法是通过地址传递,而不是通过名称传递。例如,可以在 Perl 中找到这种方法。Bash 既不为函数提供这种方法,也不为变量提供这种方法。但是,如果如您所说,您想以 Bash 为例来更全面地了解情况,那么您应该知道,函数代码可能驻留在内存中的某个位置,并且可以通过该内存位置(称为其地址)访问该代码。