Existiert das Rückrufkonzept der Programmierung in Bash?

21

Als ich ein paar Mal über das Programmieren las, stieß ich auf das "Rückruf" -Konzept.

Komischerweise habe ich nie eine Erklärung gefunden, die ich für diesen Begriff "Rückruffunktion" als "didaktisch" oder "klar" bezeichnen kann (fast jede Erklärung, die ich las, schien sich von der anderen zu unterscheiden und ich fühlte mich verwirrt).

Existiert das "Rückruf" -Konzept der Programmierung in Bash? Wenn ja, antworten Sie bitte mit einem kleinen, einfachen Bash-Beispiel.

JohnDoea
quelle
2
Handelt es sich bei "Rückruf" um ein konkretes Konzept oder um "erstklassige Funktion"?
Cedric H.
Sie finden dies möglicherweise declarative.bashals Framework interessant, das Funktionen explizit nutzt, die so konfiguriert sind, dass sie aufgerufen werden, wenn ein bestimmter Wert benötigt wird.
Charles Duffy
Ein weiterer relevanter Rahmen: Bashup / Events . Die Dokumentation enthält viele einfache Demos zur Verwendung von Rückrufen, wie zum Beispiel zur Validierung, zum Nachschlagen usw.
PJ Eby
1
@CedricH. Für dich gewählt. "Ist" Rückruf "ein konkretes Konzept oder ist es" erstklassige Funktion "?" Ist eine gute Frage, die Sie als weitere Frage stellen sollten?
Prosodie-Gab Vereable Context
Ich verstehe Rückruf als "eine Funktion, die zurückgerufen wird, nachdem ein bestimmtes Ereignis ausgelöst wurde". Ist das korrekt?
JohnDoea

Antworten:

45

In der typischen imperativen Programmierung schreiben Sie Befehlssequenzen und sie werden nacheinander mit explizitem Steuerungsfluss ausgeführt. Beispielsweise:

if [ -f file1 ]; then   # If file1 exists ...
    cp file1 file2      # ... create file2 as a copy of a file1
fi

etc.

Wie aus dem Beispiel hervorgeht, können Sie bei der imperativen Programmierung den Ablauf der Ausführung ganz einfach verfolgen, indem Sie sich immer von einer bestimmten Codezeile aus nach oben arbeiten, um den Ausführungskontext zu bestimmen Position im Flow (oder die Positionen der Anrufer, wenn Sie Funktionen schreiben).

Wie Rückrufe den Fluss verändern

Wenn Sie Rückrufe verwenden, geben Sie an, wann eine Anweisung aufgerufen werden soll, anstatt sie "geografisch" zu platzieren. Typische Beispiele in anderen Programmierumgebungen sind Fälle wie „Diese Ressource herunterladen und diesen Rückruf aufrufen, wenn der Download abgeschlossen ist“. Bash verfügt nicht über ein generisches Rückrufkonstrukt dieser Art, es verfügt jedoch über Rückrufe zur Fehlerbehandlung und in einigen anderen Situationen. Zum Beispiel (man muss zuerst die Befehlsersetzung und die Bash- Exit-Modi verstehen, um dieses Beispiel zu verstehen):

#!/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

Wenn Sie dies selbst ausprobieren möchten, speichern Sie die obigen Informationen in einer Datei cleanUpOnExit.sh, machen Sie sie beispielsweise ausführbar, und führen Sie sie aus:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Mein Code hier ruft die cleanupFunktion niemals explizit auf . Es teilt Bash mit, wann er es aufrufen soll, z trap cleanup EXIT. B. "Lieber Bash, führe den cleanupBefehl beim Beenden aus" (und ist cleanupzufällig eine Funktion, die ich zuvor definiert habe, aber es kann alles sein, was Bash versteht). Bash unterstützt dies für alle nicht schwerwiegenden Signale, Exits, Befehlsfehler und das allgemeine Debuggen (Sie können einen Rückruf angeben, der vor jedem Befehl ausgeführt wird). Der Rückruf ist hier die cleanupFunktion, die von Bash direkt vor dem Beenden der Shell "zurückgerufen" wird.

Sie können die Fähigkeit von Bash verwenden, Shell-Parameter als Befehle auszuwerten, um ein Callback-orientiertes Framework zu erstellen. Das würde den Rahmen dieser Antwort sprengen und vielleicht mehr Verwirrung stiften, wenn man annimmt, dass das Weitergeben von Funktionen immer Rückrufe beinhaltet. Unter Bash: Übergeben einer Funktion als Parameter finden Sie einige Beispiele für die zugrunde liegende Funktionalität. Die Idee hierbei ist, wie bei Rückrufen zur Ereignisbehandlung, dass Funktionen Daten als Parameter, aber auch andere Funktionen verwenden können. Dadurch können Anrufer sowohl Verhalten als auch Daten bereitstellen. Ein einfaches Beispiel für diesen Ansatz könnte so aussehen

#!/bin/bash

doonall() {
    command="$1"
    shift
    for arg; do
        "${command}" "${arg}"
    done
}

backup() {
    mkdir -p ~/backup
    cp "$1" ~/backup
}

doonall backup "$@"

(Ich weiß, das ist ein bisschen nutzlos, da cpes mit mehreren Dateien umgehen kann, es ist nur zur Veranschaulichung.)

Hier erstellen wir eine Funktion, doonalldie einen anderen Befehl als Parameter verwendet und auf die restlichen Parameter anwendet. dann verwenden wir das, um die backupFunktion für alle Parameter aufzurufen, die dem Skript gegeben wurden. Das Ergebnis ist ein Skript, das alle Argumente nacheinander in ein Sicherungsverzeichnis kopiert.

Mit dieser Art von Ansatz können Funktionen mit einer einzigen Verantwortung geschrieben werden: doonallDie Verantwortung der Funktion besteht darin, nacheinander alle Argumente auszuführen. backupEs liegt in der Verantwortung des Benutzers, eine Kopie seines (einzigen) Arguments in einem Sicherungsverzeichnis zu erstellen. Beides doonallund backupkann in anderen Kontexten verwendet werden, was mehr Wiederverwendung von Code, bessere Tests usw. ermöglicht.

In diesem Fall ist der Rückruf die backupFunktion, die wir doonallfür jedes ihrer anderen Argumente "zurückrufen" sollen - wir liefern das doonallVerhalten (das erste Argument) sowie die Daten (die verbleibenden Argumente).

(Beachten Sie, dass ich in dem im zweiten Beispiel gezeigten Anwendungsfall den Begriff „Rückruf“ nicht selbst verwenden würde, aber dies ist möglicherweise eine Gewohnheit, die sich aus den von mir verwendeten Sprachen ergibt. Ich betrachte dies als Weitergabe von Funktionen oder Lambdas anstatt Rückrufe in einem ereignisorientierten System zu registrieren.)

Stephen Kitt
quelle
25

Zunächst ist zu beachten, dass eine Funktion durch ihre Verwendung und nicht durch ihre Funktion als Rückruffunktion definiert wird. Ein Rückruf ist, wenn Code, den Sie schreiben, von Code aufgerufen wird, den Sie nicht geschrieben haben. Sie bitten das System, Sie zurückzurufen, wenn ein bestimmtes Ereignis eintritt.

Ein Beispiel für einen Rückruf bei der Shell-Programmierung sind Traps. Eine Falle ist ein Rückruf, der nicht als Funktion, sondern als auszuwertender Code ausgedrückt wird. Sie fordern die Shell auf, Ihren Code aufzurufen, wenn die Shell ein bestimmtes Signal empfängt.

Ein weiteres Beispiel für einen Rückruf ist die -execAktion des findBefehls. Die Aufgabe des findBefehls besteht darin, Verzeichnisse rekursiv zu durchlaufen und jede Datei nacheinander zu verarbeiten. Standardmäßig wird bei der Verarbeitung der Dateiname (implizit -print) gedruckt , bei -execder Verarbeitung wird jedoch ein von Ihnen angegebener Befehl ausgeführt. Dies passt zur Definition eines Rückrufs, obwohl dieser bei Rückrufen nicht sehr flexibel ist, da der Rückruf in einem separaten Prozess ausgeführt wird.

Wenn Sie eine Suchfunktion implementiert haben, können Sie festlegen, dass jede Datei mit einer Rückruffunktion aufgerufen wird. Hier ist eine ultra-vereinfachte find-like-Funktion, die einen Funktionsnamen (oder einen externen Befehlsnamen) als Argument verwendet und ihn für alle regulären Dateien im aktuellen Verzeichnis und seinen Unterverzeichnissen aufruft. Die Funktion wird als Rückruf verwendet, der jedes Mal aufgerufen wird, wenn call_on_regular_fileseine reguläre Datei gefunden wird.

shopt -s globstar
call_on_regular_files () {
  declare callback="$1"
  declare file
  for file in **/*; do
    if [[ -f $file ]]; then
      "$callback" "$file"
    fi
  done
}

Rückrufe sind in der Shell-Programmierung nicht so häufig wie in einigen anderen Umgebungen, da Shells hauptsächlich für einfache Programme entwickelt wurden. Rückrufe sind in Umgebungen häufiger, in denen Daten- und Steuerungsflüsse mit größerer Wahrscheinlichkeit zwischen Teilen des Codes hin und her wandern, die unabhängig voneinander geschrieben und verteilt werden: dem Basissystem, verschiedenen Bibliotheken und dem Anwendungscode.

Gilles 'SO - hör auf böse zu sein'
quelle
1
Besonders schön erklärt
Roaima
1
@JohnDoea Ich denke, die Idee ist, dass es extrem vereinfacht ist, da es keine Funktion ist, die Sie wirklich schreiben würden. Aber vielleicht ein noch einfacheres Beispiel etwas mit einer hartcodierte Liste würde den Rückruf läuft auf: 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; }was Sie laufen können foreach_server echo, foreach_server nslookupusw. Das declare callback="$1"ist ungefähr so einfach wie es bekommen kann wenn: der Rückruf in irgendwo geführt werden muss, oder es ist kein Rückruf.
IMSoP
4
"Ein Rückruf ist, wenn Code, den Sie schreiben, von Code aufgerufen wird, den Sie nicht geschrieben haben." ist einfach falsch. Sie können ein Element schreiben, das einige nicht blockierende asynchrone Aufgaben ausführt, und es mit einem Rückruf ausführen, der ausgeführt wird, wenn der Vorgang abgeschlossen ist. Nichts ist damit zu
tun
5
@mikemaccana Natürlich ist es möglich, dass dieselbe Person die beiden Teile des Codes geschrieben hat. Aber das ist nicht der Normalfall. Ich erkläre die Grundlagen eines Konzepts und gebe keine formale Definition. Wenn Sie alle Eckfälle erklären, ist es schwierig, die Grundlagen zu vermitteln.
Gilles 'SO- hör auf böse zu sein'
1
Froh das zu hören. Ich bin nicht einverstanden, dass Leute, die sowohl den Code schreiben, der einen Rückruf verwendet, als auch den Rückruf nicht häufig verwenden oder ein Randfall sind, und aufgrund der Verwirrung, dass diese Antwort die Grundlagen vermittelt.
Mikemaccana
7

"Rückrufe" sind nur Funktionen, die als Argumente an andere Funktionen übergeben werden.

Auf Shell-Ebene bedeutet dies einfach, dass Skripte / Funktionen / Befehle als Argumente an andere Skripte / Funktionen / Befehle übergeben werden.

Betrachten Sie als einfaches Beispiel das folgende Skript:

$ 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"

mit der Synopse

x command filter [file ...]

wird filterauf jedes fileArgument angewendet und dann commandmit den Ausgaben der Filter als Argumente aufgerufen .

Zum Beispiel:

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

Das kommt dem sehr nahe, was man in Lisp machen kann (nur ein Scherz ;-))

Einige Leute bestehen darauf, den "Rückruf" -Begriff auf "Ereignishandler" und / oder "Closure" (Funktion + Daten / Umgebungstupel) zu beschränken. Dies ist keineswegs die allgemein akzeptierte Bedeutung. Und ein Grund, warum "Rückrufe" im engeren Sinne in der Shell nicht viel Sinn machen, ist, dass Pipes + Parallelität + dynamische Programmierfähigkeiten so viel leistungsfähiger sind und Sie bereits für sie in Bezug auf die Leistung zahlen, selbst wenn Sie versuchen Sie, die Shell als eine klobige Version von perloder zu verwenden python.

Mosvy
quelle
Obwohl Ihr Beispiel ziemlich nützlich aussieht, ist es so dicht, dass ich es mit dem geöffneten Bash-Handbuch wirklich auseinander nehmen müsste, um herauszufinden, wie es funktioniert (und ich habe die meisten Tage über Jahre mit einfacherem Bash gearbeitet). Ich habe es nie gelernt lispeln. ;)
Joe
1
@ Joe , wenn es OK mit nur zwei Eingabedateien zu arbeiten und keine %Interpolation in Filtern, die ganze Sache könnte reduziert: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Aber das ist viel weniger nützlich und illustrativ imho.
Mosvy
1
Oder noch besser$1 <($2 "$3") <($2 "$4")
mosvy
+1 Danke. Ihre Kommentare sowie das Anstarren und einige Zeit das Spielen mit dem Code haben es für mich klarer gemacht. Ich habe auch einen neuen Begriff gelernt, "String-Interpolation", für etwas, das ich schon immer verwendet habe.
Joe
4

So'ne Art.

Eine einfache Möglichkeit, einen Rückruf in bash zu implementieren, besteht darin, den Namen eines Programms als Parameter zu akzeptieren, der als "Rückruffunktion" fungiert.

# This is script worker.sh accepts a callback in $1
cb="$1"
....
# Execute the call back, passing 3 parameters
$cb foo bar baz

Dies würde wie folgt verwendet werden:

# Invokes mycb.sh as a callback
worker.sh mycb.sh

Natürlich haben Sie keine Schließungen in der Bash. Daher hat die Rückruffunktion keinen Zugriff auf die Variablen auf der Anruferseite. Sie können jedoch Daten, die der Rückruf benötigt, in Umgebungsvariablen speichern. Das Zurückgeben von Informationen vom Rückruf an das Aufruferskript ist schwieriger. Daten können in eine Datei eingefügt werden.

Wenn Ihr Design zulässt, dass alles in einem einzigen Prozess behandelt wird, können Sie eine Shell-Funktion für den Rückruf verwenden, und in diesem Fall hat die Rückruffunktion natürlich Zugriff auf die Variablen auf der Aufruferseite.

user1934428
quelle
3

Nur um ein paar Worte zu den anderen Antworten hinzuzufügen. Der Funktionsrückruf wird für Funktionen ausgeführt, die außerhalb der zurückrufenden Funktion liegen. Damit dies möglich ist, muss entweder eine vollständige Definition der zurückzurufenden Funktion an die zurückrufende Funktion übergeben werden, oder ihr Code sollte für die zurückrufende Funktion verfügbar sein.

Ersteres (Weitergeben von Code an eine andere Funktion) ist möglich, obwohl ich ein Beispiel überspringe, das Komplexität mit sich bringen würde. Letzteres (Übergeben der Funktion als Name) ist eine gängige Praxis, da die Variablen und Funktionen, die außerhalb des Gültigkeitsbereichs einer Funktion deklariert wurden, in dieser Funktion verfügbar sind, solange ihre Definition dem Aufruf der Funktion vorausgeht, die sie ausführt (was wiederum der Fall ist) , wie zu deklarieren, bevor es heißt).

Beachten Sie auch, dass beim Exportieren von Funktionen ein ähnliches Problem auftritt. Eine Shell, die eine Funktion importiert, verfügt möglicherweise über ein Framework, das darauf wartet, dass Funktionsdefinitionen sie in Aktion setzen. Funktionsexport ist in Bash vorhanden und verursachte zuvor schwerwiegende Probleme, übrigens (das wurde Shellshock genannt):

Ich vervollständige diese Antwort mit einer weiteren Methode zum Übergeben einer Funktion an eine andere Funktion, die in Bash nicht explizit vorhanden ist. Dieser gibt es nach Adresse, nicht nach Namen. Dies kann zum Beispiel in Perl gefunden werden. Bash bietet auf diese Weise weder Funktionen noch Variablen an. Wenn Sie jedoch ein breiteres Bild mit Bash als Beispiel haben möchten, sollten Sie wissen, dass sich der Funktionscode möglicherweise irgendwo im Speicher befindet und dass auf diesen Code von diesem Speicherort aus zugegriffen werden kann nannte seine Adresse.

Tomasz
quelle
2

Eines der einfachsten Beispiele für Rückrufe in Bash ist eines, mit dem viele Leute vertraut sind, aber nicht wissen, welches Entwurfsmuster sie tatsächlich verwenden:

cron

Mit Cron können Sie eine ausführbare Datei (eine Binärdatei oder ein Skript) angeben, die bzw. das das Cron-Programm zurückruft, wenn bestimmte Bedingungen erfüllt sind (die Zeitangabe).

Angenommen, Sie haben ein Skript namens doEveryDay.sh. Das Skript kann ohne Rückruf wie folgt geschrieben werden:

#! /bin/bash
while true; do
    doSomething
    sleep $TWENTY_FOUR_HOURS
done

Der Callback-Weg, es zu schreiben, ist einfach:

#! /bin/bash
doSomething

Dann würden Sie in crontab so etwas einstellen

0 0 * * *     doEveryDay.sh

Sie müssten dann nicht den Code schreiben, um auf die Auslösung des Ereignisses zu warten, sondern müssen sich darauf verlassen cron, dass Sie Ihren Code zurückrufen.


Betrachten Sie nun, wie Sie diesen Code in Bash schreiben würden.

Wie würden Sie ein anderes Skript / eine andere Funktion in Bash ausführen?

Schreiben wir eine Funktion:

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
}

Jetzt haben Sie eine Funktion erstellt, die einen Rückruf akzeptiert. Sie können es einfach so nennen:

# "ping" google website every day
every24hours 'curl google.com'

Natürlich kehrt die Funktion alle 24 Stunden nie zurück. Bash ist insofern einzigartig, als wir es sehr einfach asynchron machen und einen Prozess erzeugen können, indem wir Folgendes anhängen &:

every24hours 'curl google.com' &

Wenn Sie dies nicht als Funktion möchten, können Sie dies stattdessen als Skript ausführen:

#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

Wie Sie sehen, sind Rückrufe in bash trivial. Es ist einfach:

CALLBACK_SCRIPT=$3 ;# or some other 
                    # argument to 
                    # function/script

Und den Rückruf anzurufen ist einfach:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Wie Sie oben sehen können, sind Rückrufe selten direkte Merkmale von Sprachen. Sie programmieren in der Regel kreativ mit vorhandenen Sprachfunktionen. Jede Sprache, die einen Zeiger / eine Referenz / eine Kopie eines Codeblocks / einer Funktion / eines Skripts speichern kann, kann Rückrufe implementieren.

Slebetman
quelle
Andere Beispiele für Programme / Skripte , die akzeptieren Rückrufe sind watchund find(wenn verwendet mit -execParameter)
slebetman
0

Ein Rückruf ist eine Funktion, die aufgerufen wird, wenn ein Ereignis eintritt. Mit bashist das einzige Event Handling Mechanismus zu Signalen im Zusammenhang, die Shell verlassen, und erweitert , um Fehler Ereignisse Shell, Debug - Ereignisse und Funktion / sourced Skripte zurückgeben Ereignisse.

Hier ist ein Beispiel für einen nutzlosen, aber einfachen Rückruf, der Signalfallen nutzt.

Erstellen Sie zuerst das Skript, das den Rückruf implementiert:

#!/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

Führen Sie dann das Skript in einem Terminal aus:

$ ./callback-example

und bei einem anderen senden Sie das USR1Signal an den Shell-Prozess.

$ pkill -USR1 callback-example

Jedes gesendete Signal sollte die Anzeige von Zeilen wie diesen im ersten Terminal auslösen:

I've been called at 20180925T003515
I've been called at 20180925T003517

ksh93bietet als Shell, die viele Funktionen implementiert, die bashspäter übernommen wurden, das, was sie "Disziplinfunktionen" nennt. Diese Funktionen, die mit nicht verfügbar sind bash, werden aufgerufen, wenn eine Shell-Variable geändert oder referenziert (dh gelesen) wird. Dies eröffnet den Weg für interessantere ereignisgesteuerte Anwendungen.

Diese Funktion ermöglichte es beispielsweise, Rückrufe im X11 / Xt / Motif-Stil auf Grafik-Widgets in einer alten Version der kshgenannten Grafikerweiterungen zu implementieren dtksh. Siehe dksh-Handbuch .

jlliagre
quelle