Il concetto di programmazione "callback" esiste in Bash?


21

Alcune volte, quando ho letto della programmazione, mi sono imbattuto nel concetto di "callback".

Stranamente, non ho mai trovato una spiegazione che posso chiamare "didattica" o "chiara" per questo termine "funzione di richiamata" (quasi ogni spiegazione che ho letto mi è sembrata abbastanza diversa dall'altra e mi sono sentita confusa).

Il concetto di programmazione "callback" esiste in Bash? In tal caso, rispondi con un piccolo, semplice esempio di Bash.


2
"Richiamata" è un concetto reale o è "funzione di prima classe"?
Cedric H.,

Potresti trovare declarative.bashinteressante come un framework che sfrutta esplicitamente le funzioni configurate per essere invocate quando è necessario un dato valore.
Charles Duffy,

Un altro framework pertinente: bashup / eventi . La sua documentazione include molte semplici dimostrazioni sull'uso della callback, come per convalida, ricerche, ecc.
PJ Eby

1
@CedricH. Votato per te. "La" richiamata "è un concetto reale o è" funzione di prima classe "?" È una buona domanda da porre come un'altra domanda?
prosody-Gab Vereable Context

Comprendo che callback significa "una funzione richiamata dopo l'attivazione di un determinato evento". È corretto?
JohnDoea,

Risposte:


44

Nella tipica programmazione imperativa , si scrivono sequenze di istruzioni e vengono eseguite una dopo l'altra, con flusso di controllo esplicito. Per esempio:

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

eccetera.

Come si può vedere dall'esempio, nella programmazione imperativa segui abbastanza facilmente il flusso di esecuzione, risalendo sempre da una determinata riga di codice per determinarne il contesto di esecuzione, sapendo che tutte le istruzioni che darai verranno eseguite come risultato della loro posizione nel flusso (o posizioni dei relativi siti di chiamata, se si stanno scrivendo funzioni).

Come i callback cambiano il flusso

Quando si utilizzano i callback, invece di inserire "geograficamente" un insieme di istruzioni, si descrive quando deve essere chiamato. Esempi tipici in altri ambienti di programmazione sono casi come "scarica questa risorsa e quando il download è completo, chiama questo callback". Bash non ha un costrutto di callback generico di questo tipo, ma ha callback, per la gestione degli errori e alcune altre situazioni; per esempio ( per capire quell'esempio bisogna prima capire la sostituzione dei comandi e le modalità di uscita di 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

Se vuoi provarlo tu stesso, salva quanto sopra in un file, diciamo cleanUpOnExit.sh, rendilo eseguibile ed eseguilo:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Il mio codice qui non chiama mai esplicitamente la cleanupfunzione; dice a Bash quando chiamarlo, usando trap cleanup EXIT, ad esempio , "caro Bash, per favore esegui il cleanupcomando quando esci" (e cleanupsembra essere una funzione che ho definito in precedenza, ma potrebbe essere qualunque cosa capisca Bash). Bash lo supporta per tutti i segnali non fatali, le uscite, gli errori di comando e il debug generale (è possibile specificare un callback che viene eseguito prima di ogni comando). Il callback qui è la cleanupfunzione, che viene "richiamata" da Bash appena prima che la shell esca.

È possibile utilizzare la capacità di Bash di valutare i parametri della shell come comandi, per creare un framework orientato alla callback; questo è un po 'oltre lo scopo di questa risposta, e forse causerebbe più confusione suggerendo che il passaggio di funzioni comporta sempre callback. Vedi Bash: passa una funzione come parametro per alcuni esempi della funzionalità sottostante. L'idea qui, come per i callback di gestione degli eventi, è che le funzioni possono prendere i dati come parametri, ma anche altre funzioni: ciò consente ai chiamanti di fornire comportamenti e dati. Un semplice esempio di questo approccio potrebbe apparire

#!/bin/bash

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

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

doonall backup "$@"

(So ​​che questo è un po 'inutile poiché cppuò gestire più file, è solo a scopo illustrativo.)

Qui creiamo una funzione, doonallche accetta un altro comando, dato come parametro, e la applica al resto dei suoi parametri; quindi usiamo quello per chiamare la backupfunzione su tutti i parametri dati allo script. Il risultato è uno script che copia tutti i suoi argomenti, uno per uno, in una directory di backup.

Questo tipo di approccio consente di scrivere funzioni con singole responsabilità: doonallla responsabilità è di eseguire qualcosa su tutti i suoi argomenti, uno alla volta; backupLa responsabilità è di fare una copia del suo (unico) argomento in una directory di backup. Entrambi doonalle backuppossono essere utilizzati in altri contesti, il che consente un maggiore riutilizzo del codice, test migliori ecc.

In questo caso il callback è la backupfunzione, che diciamo doonalldi "richiamare" su ciascuno dei suoi altri argomenti: forniamo il doonallcomportamento (il suo primo argomento) così come i dati (gli argomenti rimanenti).

(Si noti che nel tipo di caso d'uso dimostrato nel secondo esempio, non userei il termine "callback", ma questa è forse un'abitudine derivante dalle lingue che uso. Penso a questo come funzioni di passaggio o lambda in giro , piuttosto che registrare callback in un sistema orientato agli eventi.)


25

Innanzitutto è importante notare che ciò che rende una funzione una funzione di richiamata è il modo in cui viene utilizzata, non ciò che fa. Un callback è quando il codice che scrivi viene chiamato dal codice che non hai scritto. Stai chiedendo al sistema di richiamarti quando si verifica un evento particolare.

Un esempio di callback nella programmazione della shell sono le trap. Una trap è un callback che non è espresso come una funzione, ma come un pezzo di codice da valutare. Stai chiedendo alla shell di chiamare il tuo codice quando la shell riceve un segnale particolare.

Un altro esempio di callback è l' -execazione del findcomando. Il compito del findcomando è di attraversare ricorsivamente le directory ed elaborare a turno ciascun file. Per impostazione predefinita, l'elaborazione deve stampare il nome del file (implicito -print), ma con -execl'elaborazione è eseguire un comando specificato. Questo si adatta alla definizione di callback, anche se a callback va, non è molto flessibile poiché il callback viene eseguito in un processo separato.

Se hai implementato una funzione simile a find, potresti farne uso una funzione di callback per chiamare ogni file. Ecco una funzione di ricerca ultra-semplificata che prende come argomento un nome di funzione (o nome di comando esterno) e lo chiama su tutti i file regolari nella directory corrente e nelle sue sottodirectory. La funzione viene utilizzata come callback che viene chiamata ogni volta che call_on_regular_filestrova un file normale.

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

I callback non sono così comuni nella programmazione della shell come in alcuni altri ambienti perché le shell sono progettate principalmente per programmi semplici. I callback sono più comuni negli ambienti in cui i flussi di dati e di controllo hanno maggiori probabilità di spostarsi avanti e indietro tra le parti del codice scritte e distribuite in modo indipendente: il sistema di base, varie librerie, il codice dell'applicazione.


1
Spiegazione particolarmente precisa
roaima,

1
@JohnDoea Penso che l'idea sia che sia ultra-semplificata in quanto non è una funzione che vorresti davvero scrivere. Ma forse un esempio ancora più semplice sarebbe qualcosa con una lista hard-coded per eseguire il callback su: 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; }che si potrebbe correre come foreach_server echo, foreach_server nslookup, ecc declare callback="$1"è quanto di più semplice come si può ottenere però: il callback deve essere trasferita in qualche parte, o non è un callback.
IMSoP,

4
"Una richiamata è quando il codice che scrivi viene chiamato da un codice che non hai scritto." è semplicemente sbagliato. Puoi scrivere qualcosa che fa un lavoro asincrono non bloccante ed eseguirlo con un callback che verrà eseguito al termine. Nulla è collegato a chi ha scritto il codice,
mikemaccana,

5
@mikemaccana Naturalmente è possibile che la stessa persona abbia scritto le due parti del codice. Ma non è il caso comune. Sto spiegando le basi di un concetto, non dando una definizione formale. Se spieghi tutti i casi angolari, è difficile comunicare le basi.
Gilles 'SO- smetti di essere malvagio' il

1
Felice di sentirlo. Non sono d'accordo sul fatto che le persone che scrivono sia il codice che utilizza un callback sia il callback non siano comuni o siano un caso limite e, a causa della confusione, che questa risposta trasmetta le basi.
mikemaccana,

7

I "callbacks" sono solo funzioni passate come argomenti ad altre funzioni.

A livello di shell, ciò significa semplicemente script / funzioni / comandi passati come argomenti ad altri script / funzioni / comandi.

Ora, per un semplice esempio, considera il seguente script:

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

avere la sinossi

x command filter [file ...]

si applicherà filtera ciascun fileargomento, quindi chiamerà commandcon gli output dei filtri come argomenti.

Per esempio:

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

Questo è molto vicino a quello che puoi fare in lisp (scherzando ;-))

Alcune persone insistono nel limitare il termine "callback" a "gestore eventi" e / o "chiusura" (funzione + dati / tupla ambiente); questo non è affatto il significato generalmente accettato . E uno dei motivi per cui i "callback" in quei sensi ristretti non sono molto utili nella shell è perché i tubi + parallelismo + capacità di programmazione dinamica sono molto più potenti e li stai già pagando in termini di prestazioni, anche se prova a usare la shell come una versione goffo di perlo python.


Mentre il tuo esempio sembra abbastanza utile, è sufficientemente denso che dovrei davvero selezionarlo con il manuale bash aperto per capire come funziona (e ho lavorato con bash più semplice quasi tutti i giorni per anni.) Non ho mai imparato Lisp. ;)
Joe,

1
@ Joe se è ok per lavorare con solo due file di input e nessuna %interpolazione nei filtri, il tutto potrebbe essere ridotto a: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Ma questo è un imho molto meno utile e illustrativo.
mosvy,

1
O meglio$1 <($2 "$3") <($2 "$4")
mosvy,

+1 Grazie. I tuoi commenti, oltre a fissarlo e giocare con il codice per qualche tempo, mi hanno chiarito. Ho anche imparato un nuovo termine, "interpolazione di stringhe", per qualcosa che uso da sempre.
Joe,

4

Tipo.

Un modo semplice per implementare un callback in bash è accettare il nome di un programma come parametro, che funge da "funzione di callback".

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

Questo sarebbe usato in questo modo:

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

Ovviamente non hai chiusure in bash. Pertanto, la funzione di richiamata non ha accesso alle variabili sul lato chiamante. Tuttavia, è possibile memorizzare i dati di cui il callback ha bisogno nelle variabili di ambiente. Il passaggio di informazioni dal callback allo script del chiamante è più complicato. I dati potrebbero essere inseriti in un file.

Se il tuo progetto consente che tutto sia gestito in un unico processo, potresti utilizzare una funzione shell per il callback, e in questo caso la funzione callback ha ovviamente accesso alle variabili sul lato invocatore.


3

Solo per aggiungere qualche parola alle altre risposte. La funzione di richiamata opera su funzioni esterne alla funzione che richiama. Perché ciò sia possibile o un'intera definizione della funzione da richiamare deve essere passata alla funzione di richiamata, oppure il suo codice dovrebbe essere disponibile per la funzione di richiamata.

Il primo (passare il codice ad un'altra funzione) è possibile, anche se salterò un esempio perché ciò comporterebbe complessità. Quest'ultimo (passando la funzione per nome) è una pratica comune, poiché le variabili e le funzioni dichiarate al di fuori dell'ambito di una funzione sono disponibili in quella funzione fintanto che la loro definizione precede la chiamata alla funzione che opera su di esse (che, a sua volta, , da dichiarare prima che venga chiamato).

Si noti inoltre che una cosa simile accade quando le funzioni vengono esportate. Una shell che importa una funzione può avere un framework pronto ed essere in attesa di definizioni delle funzioni per metterle in azione. L'esportazione di funzioni è presente in Bash e ha causato problemi precedentemente gravi, tra l'altro (che si chiamava Shellshock):

Completerò questa risposta con un altro metodo per passare una funzione a un'altra funzione, che non è esplicitamente presente in Bash. Questo lo sta passando per indirizzo, non per nome. Questo può essere trovato in Perl, per esempio. Bash offre in questo modo né per funzioni né per variabili. Ma se, come dici, vuoi avere un'immagine più ampia con Bash come solo un esempio, allora dovresti sapere che il codice della funzione potrebbe risiedere da qualche parte nella memoria, e che il codice potrebbe essere accessibile da quella posizione di memoria, che è chiamato il suo indirizzo.


2

Uno dei più semplici esempi di callback in bash è quello con cui molte persone hanno familiarità ma non si rendono conto di quale modello di progettazione stanno effettivamente usando:

cron

Cron consente di specificare un eseguibile (binario o script) che il programma cron richiamerà quando vengono soddisfatte alcune condizioni (la specifica temporale)

Supponi di avere uno script chiamato doEveryDay.sh. Il modo non callback per scrivere lo script è:

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

Il modo di richiamare per scriverlo è semplicemente:

#! /bin/bash
doSomething

Quindi in crontab avresti impostato qualcosa di simile

0 0 * * *     doEveryDay.sh

Non sarà quindi necessario scrivere il codice per attendere l'attivazione dell'evento, ma fare affidamento su cronper richiamare il codice.


Ora, considera come scriveresti questo codice in bash.

Come eseguiresti un altro script / funzione in bash?

Scriviamo una funzione:

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
}

Ora hai creato una funzione che accetta un callback. Puoi semplicemente chiamarlo così:

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

Ovviamente, la funzione ogni 24 ore non ritorna mai. Bash è un po 'unico in quanto possiamo facilmente renderlo asincrono e generare un processo aggiungendo &:

every24hours 'curl google.com' &

Se non lo desideri come funzione, puoi farlo come script:

#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

Come puoi vedere, i callback in bash sono banali. È semplicemente:

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

E chiamare il callback è semplicemente:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Come puoi vedere sopra, i callback raramente sono direttamente funzionalità delle lingue. Di solito programmano in modo creativo utilizzando le funzionalità del linguaggio esistente. Qualsiasi lingua in grado di memorizzare un puntatore / riferimento / copia di alcuni blocchi di codice / funzioni / script può implementare callback.


Altri esempi di programmi / script che accettano callback includono watche find(se utilizzato con il -execparametro)
slebetman

0

Una richiamata è una funzione chiamata quando si verifica un evento. Con bash, l'unico meccanismo di gestione degli eventi in atto è correlato ai segnali, all'uscita della shell e agli eventi di errore della shell estesi, agli eventi di debug e agli script di funzione / provenienza restituiscono eventi.

Ecco un esempio di un callback inutile ma semplice che sfrutta le trap del segnale.

Innanzitutto creare lo script implementando il callback:

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

Quindi eseguire lo script in un terminale:

$ ./callback-example

e su un altro, invia il USR1segnale al processo shell.

$ pkill -USR1 callback-example

Ogni segnale inviato dovrebbe attivare la visualizzazione di linee come queste nel primo terminale:

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

ksh93, poiché la shell implementa molte funzionalità che in bashseguito hanno adottato, fornisce quelle che chiama "funzioni disciplinari". Queste funzioni, non disponibili con bash, vengono chiamate quando una variabile di shell viene modificata o referenziata (cioè letta). Questo apre la strada ad applicazioni guidate da eventi più interessanti.

Ad esempio, questa funzione ha consentito l'implementazione di callback in stile X11 / Xt / Motif sui widget grafici in una versione precedente di kshquelle estensioni grafiche incluse chiamate dtksh. Vedi manuale dksh .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.