Bash中存在编程的“回调”概念吗?


21

几次阅读编程知识时,我都遇到了“回调”概念。

有趣的是,我从来没有找到一个我可以称呼为“回调函数”或“清晰”的解释(对于我来说,几乎我读过的任何解释似乎与其他解释都足够不同,我感到困惑)。

Bash中存在编程的“回调”概念吗?如果是这样,请用一个简单的Bash小示例进行回答。


2
“回调”是一个实际概念还是“一流功能”?
Cedric H.

您可能会发现declarative.bash有趣的情况,因为该框架显式地利用了配置为需要给定值时调用的功能。
查尔斯·达菲

另一个相关的框架:bashup / events。它的文档包括了很多回调使用简单的演示,如进行验证,查询等
PJ伊比

1
@CedricH。为您投票。““回调”是一个实际的概念还是“一流的功能”?”作为另一个问题要问的一个好问题?
prosody-Gab Vereable语境,

我理解回调的含义是“在触发给定事件后会回调的函数”。那是对的吗?
JohnDoea

Answers:


44

在典型的命令式编程中,您编写指令序列,并使用显式的控制流程一个接一个地执行它们。例如:

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的责任是在备份目录中复制其(唯一的)自变量。双方doonallbackup可以在其他情况下,允许更多的代码复用,更好的测试等使用

在这种情况下,回调是backup函数,我们告诉doonall它对其每个其他参数“回调”-我们提供doonall行为(其第一个参数)以及数据(其余的参数)。

(请注意,在第二个示例中演示的那种用例中,我自己不会使用术语“回调”,但这可能是我使用的语言所导致的习惯。我认为这是在周围传递函数或lambdas ,而不是在面向事件的系统中注册回调。)


25

首先,重要的是要注意,使函数成为回调函数的是其用法,而不是其用途。回调是指从未编写的代码中调用编写的代码时。您要求系统在发生某些特定事件时给您回电。

陷阱是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主要是为简单程序设计的。在数据和控制流更可能在独立编写和分发的部分代码之间来回移动的环境中,回调更为常见:基本系统,各种库,应用程序代码。


1
特别
好的

1
@JohnDoea我认为这个想法是超级简化的,因为它不是您真正编写的函数。但也许是一个更简单的例子是一些与硬编码列表上运行的回调: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 echoforeach_server nslookupdeclare callback="$1"是一样简单,因为它可以得到,但:回调已在某处被传递,或它不是回调。
IMSoP '18

4
“回调是指从未编写的代码中调用您编写的代码。” 是完全错误的。您可以编写一个可以完成一些非阻塞异步工作的东西,并使用将在完成时运行的回调来运行它。与谁编写代码
无关

5
@mikemaccana当然,同一个人可能编写了代码的两部分。但这不是常见的情况。我在解释概念的基础,而不是给出正式定义。如果您解释所有极端情况,则很难传达基本信息。
吉尔斯(Gilles)“所以,别再邪恶了”

1
听到那个消息很开心。我不同意人们同时编写使用回调和回调的代码的情况,这并不常见,也不是偶然的情况,而且由于混淆,该答案传达了基本知识。
mikemaccana

7

“回调”只是作为参数传递给其他函数的函数。

在外壳程序级别上,这仅表示脚本/函数/命令作为参数传递给其他脚本/函数/命令。

现在,作为一个简单的示例,请考虑以下脚本:

$ 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


尽管您的示例看起来非常有用,但它的密度足够大,以至于我不得不在打开bash手册时真正将其拆开,以弄清其工作原理(多年来,我使用简单的bash进行了很多年的工作。)我从未学习过口齿不清。;)

1
@Joe如果可以只使用两个输入文件并且%在过滤器中不进行插值,则整个过程可以简化为:cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2")。但这不是那么有用和说明性的恕我直言。
mosvy

1
甚至更好$1 <($2 "$3") <($2 "$4")
mosvy

+1谢谢。您的评论,加上盯着它并玩了一段时间的代码,为我澄清了它。我还学到了一个新术语“字符串插值”,这是我一直使用的东西。

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函数进行回调,并且在这种情况下,回调函数当然可以访问调用方的变量。


3

只是在其他答案中添加几句话。函数回调对回调函数外部的函数进行操作。为此,需要将要回调的函数的整个定义传递给回调函数,或者应将其代码提供给回调函数。

前者(将代码传递给另一个函数)是可能的,尽管我将跳过一个示例,因为这将涉及复杂性。后者(通过名称传递函数)是一种常见的做法,因为在一个函数的作用域之外声明的变量和函数在该函数中可用,只要它们的定义先于对对其进行操作的函数的调用即可(依次调用该函数) ,以便在调用之前进行声明)。

还要注意,导出函数时也会发生类似的情况。导入函数的shell可能已经准备好框架,并且仅在等待函数定义将其付诸实践。函数导出存在于Bash中,并导致以前严重的问题,btw(称为Shellshock):

我将通过将一个函数传递给另一个函数的另一种方法来完成此答案,这在Bash中没有明确存在。这是通过地址而不是名称传递它。例如,这可以在Perl中找到。Bash既不提供函数,也不提供变量。但是,如您所陈述的,如果您想以Bash为例进行更广泛的介绍,那么您应该知道,功能代码可能驻留在内存中的某个位置,并且该代码可以通过该内存位置进行访问,即称为地址。


2

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

如您在上面的表格中所见,回调很少是语言的直接功能。他们通常使用现有的语言功能以创新的方式进行编程。可以存储指针/引用/某些代码块/功能/脚本的副本的任何语言都可以实现回调。


接受回调的程序/脚本的其他示例包括watchfind(当与-exec参数一起使用时)
slebetman

0

回调是在发生某些事件时调用的函数。用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手册

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.