Le concept de «rappel» de la programmation existe-t-il dans Bash?

21

Quelques fois, lorsque j'ai lu sur la programmation, je suis tombé sur le concept de "rappel".

Curieusement, je n'ai jamais trouvé d'explication que je puisse appeler «didactique» ou «claire» pour ce terme «fonction de rappel» (presque toutes les explications que j'ai lues me semblaient assez différentes les unes des autres et je me sentais confuse).

Le concept de "rappel" de la programmation existe-t-il dans Bash? Dans l'affirmative, veuillez répondre avec un petit exemple simple de Bash.

JohnDoea
la source
2
Le «rappel» est-il un concept réel ou est-ce une «fonction de première classe»?
Cedric H.
Vous pouvez trouver declarative.bashintéressant, comme un cadre qui exploite explicitement les fonctions configurées pour être invoquées lorsqu'une valeur donnée est nécessaire.
Charles Duffy
Un autre cadre pertinent: bashup / events . Sa documentation comprend de nombreuses démos simples d'utilisation du rappel, comme pour la validation, les recherches, etc.
PJ Eby
1
@CedricH. A voté pour vous. "Le" rappel "est-il un concept réel ou est-ce une" fonction de première classe "?" Est une bonne question à poser comme autre question?
Prosody-Gab Vereable Context
Je comprends que le rappel signifie "une fonction qui est rappelée après le déclenchement d'un événement donné". Est-ce exact?
JohnDoea

Réponses:

44

Dans la programmation impérative typique , vous écrivez des séquences d'instructions et elles sont exécutées les unes après les autres, avec un flux de contrôle explicite. Par exemple:

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

etc.

Comme le montre l'exemple, dans la programmation impérative, vous suivez assez facilement le flux d'exécution, en remontant toujours à partir d'une ligne de code donnée pour déterminer son contexte d'exécution, sachant que toutes les instructions que vous donnerez seront exécutées à la suite de leur l'emplacement dans le flux (ou l'emplacement de leurs sites d'appel, si vous écrivez des fonctions).

Comment les rappels modifient le flux

Lorsque vous utilisez des rappels, au lieu de placer l'utilisation d'un ensemble d'instructions «géographiquement», vous décrivez quand il doit être appelé. Des exemples typiques dans d'autres environnements de programmation sont des cas tels que «téléchargez cette ressource, et lorsque le téléchargement est terminé, appelez ce rappel». Bash n'a pas de construction de rappel générique de ce type, mais il a des rappels, pour la gestion des erreurs et quelques autres situations; par exemple (il faut d'abord comprendre les modes de substitution de commandes et de sortie Bash pour comprendre cet exemple):

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

Si vous voulez essayer vous-même, enregistrez ce qui précède dans un fichier, par exemple cleanUpOnExit.sh, rendez-le exécutable et exécutez-le:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Mon code ici n'appelle jamais explicitement la cleanupfonction; il indique à Bash quand l'appeler, en utilisant trap cleanup EXIT, par exemple «cher Bash, veuillez exécuter la cleanupcommande lorsque vous quittez» (et cleanupil se trouve que c'est une fonction que j'ai définie plus tôt, mais cela pourrait être tout ce que Bash comprend). Bash le prend en charge pour tous les signaux non fatals, les sorties, les échecs de commande et le débogage général (vous pouvez spécifier un rappel qui est exécuté avant chaque commande). Le rappel ici est la cleanupfonction, qui est «rappelée» par Bash juste avant la sortie du shell.

Vous pouvez utiliser la capacité de Bash pour évaluer les paramètres du shell en tant que commandes, pour construire un framework orienté callback; c'est un peu au-delà de la portée de cette réponse, et provoquerait peut-être plus de confusion en suggérant que la transmission de fonctions implique toujours des rappels. Voir Bash: passer une fonction comme paramètre pour quelques exemples de la fonctionnalité sous-jacente. L'idée ici, comme pour les rappels de gestion d'événements, est que les fonctions peuvent prendre des données en tant que paramètres, mais aussi d'autres fonctions - cela permet aux appelants de fournir un comportement ainsi que des données. Un exemple simple de cette approche pourrait ressembler à

#!/bin/bash

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

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

doonall backup "$@"

(Je sais que c'est un peu inutile car cppeut traiter plusieurs fichiers, c'est uniquement à titre d'illustration.)

Ici, nous créons une fonction, doonallqui prend une autre commande, donnée en paramètre, et l'applique au reste de ses paramètres; nous utilisons ensuite cela pour appeler la backupfonction sur tous les paramètres donnés au script. Le résultat est un script qui copie tous ses arguments, un par un, dans un répertoire de sauvegarde.

Ce type d'approche permet d'écrire des fonctions avec des responsabilités uniques: doonallla responsabilité de est d'exécuter quelque chose sur tous ses arguments, un à la fois; backupLa responsabilité de est de faire une copie de son (unique) argument dans un répertoire de sauvegarde. Les deux doonallet backuppeuvent être utilisés dans d'autres contextes, ce qui permet une plus grande réutilisation du code, de meilleurs tests, etc.

Dans ce cas, le rappel est la backupfonction, que nous disons doonallde «rappeler» sur chacun de ses autres arguments - nous fournissons le doonallcomportement (son premier argument) ainsi que les données (les arguments restants).

(Notez que dans le type de cas d'utilisation démontré dans le deuxième exemple, je n'utiliserais pas le terme «rappel» moi-même, mais c'est peut-être une habitude résultant des langages que j'utilise. Je pense que cela passe des fonctions ou des lambdas autour , plutôt que d'enregistrer des rappels dans un système orienté événement.)

Stephen Kitt
la source
25

Tout d'abord, il est important de noter que ce qui fait d'une fonction une fonction de rappel est la façon dont elle est utilisée, pas ce qu'elle fait. Un rappel se produit lorsque le code que vous écrivez est appelé à partir de code que vous n'avez pas écrit. Vous demandez au système de vous rappeler lorsqu'un événement particulier se produit.

Les pièges sont un exemple de rappel dans la programmation shell. Un piège est un rappel qui n'est pas exprimé comme une fonction, mais comme un morceau de code à évaluer. Vous demandez au shell d'appeler votre code lorsque le shell reçoit un signal particulier.

Un autre exemple de rappel est l' -execaction de la findcommande. Le travail de la findcommande est de parcourir les répertoires de manière récursive et de traiter chaque fichier tour à tour. Par défaut, le traitement consiste à imprimer le nom du fichier (implicite -print), mais avec -execle traitement consiste à exécuter une commande que vous spécifiez. Cela correspond à la définition d'un rappel, bien que lors des rappels, ce n'est pas très flexible car le rappel s'exécute dans un processus distinct.

Si vous avez implémenté une fonction de recherche, vous pouvez lui faire utiliser une fonction de rappel pour appeler chaque fichier. Voici une fonction de recherche ultra simplifiée qui prend un nom de fonction (ou un nom de commande externe) comme argument et l'appelle sur tous les fichiers normaux du répertoire en cours et de ses sous-répertoires. La fonction est utilisée comme un rappel qui est appelé chaque fois call_on_regular_filesqu'un fichier normal est trouvé.

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

Les rappels ne sont pas aussi courants dans la programmation shell que dans certains autres environnements car les shells sont principalement conçus pour des programmes simples. Les rappels sont plus courants dans les environnements où les données et le flux de contrôle sont plus susceptibles de se déplacer d'avant en arrière entre les parties du code qui sont écrites et distribuées indépendamment: le système de base, diverses bibliothèques, le code d'application.

Gilles 'SO- arrête d'être méchant'
la source
1
Particulièrement bien expliqué
roaima
1
@JohnDoea Je pense que l'idée est que c'est ultra-simplifié en ce que ce n'est pas une fonction que vous écririez vraiment. Mais peut - être un exemple encore plus simple serait quelque chose avec une liste codée en dur pour exécuter le rappel sur: 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; }que vous pouvez exécuter comme foreach_server echo, foreach_server nslookup, etc. declare callback="$1"est à peu près aussi simple que cela peut arriver cependant: le rappel doit être passé quelque part, ou ce n'est pas un rappel.
IMSoP
4
«Un rappel est lorsque le code que vous écrivez est appelé à partir de code que vous n'avez pas écrit.» est tout simplement faux. Vous pouvez écrire une chose qui effectue un travail asynchrone non bloquant, et l'exécuter avec un rappel qu'il exécutera une fois terminé. Rien n'est lié à qui a écrit le code,
mikemaccana
5
@mikemaccana Bien sûr, il est possible que la même personne ait écrit les deux parties du code. Mais ce n'est pas le cas commun. J'explique les bases d'un concept, sans donner de définition formelle. Si vous expliquez tous les cas d'angle, il est difficile de transmettre les bases.
Gilles 'SO- arrête d'être méchant'
1
Heureux de l'entendre. Je ne suis pas d'accord pour dire que les gens qui écrivent à la fois le code qui utilise un rappel et le rappel n'est pas commun ou est un cas de bord, et, en raison de la confusion, que cette réponse transmet les bases.
mikemaccana
7

Les "rappels" ne sont que des fonctions passées en arguments à d'autres fonctions.

Au niveau du shell, cela signifie simplement des scripts / fonctions / commandes passés en arguments à d'autres scripts / fonctions / commandes.

Maintenant, pour un exemple simple, considérons le script suivant:

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

avoir le synopsis

x command filter [file ...]

s'appliquera filterà chaque fileargument, puis appellera commandavec les sorties des filtres comme arguments.

Par exemple:

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

C'est très proche de ce que vous pouvez faire en lisp (je plaisante ;-))

Certaines personnes insistent pour limiter le terme "rappel" au "gestionnaire d'événements" et / ou à la "fermeture" (fonction + données / environnement tuple); ce n'est en aucun cas le sens généralement accepté . Et l'une des raisons pour lesquelles les «rappels» dans ces sens étroits ne sont pas très utiles dans le shell est que les canaux + parallélisme + capacités de programmation dynamique sont tellement plus puissants, et vous les payez déjà en termes de performances, même si vous essayez d'utiliser le shell comme une version maladroite de perlou python.

mosvy
la source
Bien que votre exemple semble assez utile, il est suffisamment dense pour que je doive vraiment le séparer avec le manuel bash ouvert pour comprendre comment cela fonctionne (et j'ai travaillé avec bash plus simple la plupart du temps pendant des années.) Je n'ai jamais appris zézayer. ;)
Joe
1
Si elle est @ Joe OK pour travailler avec seulement deux fichiers d'entrée et pas d' %interpolation dans les filtres, la chose pourrait être réduite à: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Mais c'est beaucoup moins utile et illustratif à mon humble avis.
mosvy
1
Ou encore mieux$1 <($2 "$3") <($2 "$4")
mosvy
+1 Merci. Vos commentaires, en plus de le regarder et de jouer avec le code pendant un certain temps, l'ont clarifié pour moi. J'ai également appris un nouveau terme, "interpolation de chaînes", pour quelque chose que j'utilise depuis toujours.
Joe
4

En quelque sorte.

Une façon simple d'implémenter un rappel dans bash, est d'accepter le nom d'un programme comme paramètre, qui agit comme une "fonction de rappel".

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

Cela serait utilisé comme ceci:

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

Bien sûr, vous n'avez pas de fermetures en bash. Par conséquent, la fonction de rappel n'a pas accès aux variables côté appelant. Vous pouvez toutefois stocker les données dont le rappel a besoin dans des variables d'environnement. Il est plus difficile de renvoyer des informations du rappel au script d'invocateur. Les données pourraient être placées dans un fichier.

Si votre conception permet que tout soit géré dans un seul processus, vous pouvez utiliser une fonction shell pour le rappel, et dans ce cas, la fonction de rappel a bien sûr accès aux variables du côté invocateur.

user1934428
la source
3

Juste pour ajouter quelques mots aux autres réponses. La fonction de rappel fonctionne sur des fonctions externes à la fonction qui rappelle. Pour que cela soit possible, soit une définition complète de la fonction à rappeler doit être transmise à la fonction rappelant, soit son code doit être disponible pour la fonction rappelant.

Le premier (passer du code à une autre fonction) est possible, mais je vais sauter un exemple car cela impliquerait de la complexité. Cette dernière (en passant le nom de la fonction) est une pratique courante, car les variables et fonctions déclarées en dehors de la portée d'une fonction sont disponibles dans cette fonction tant que leur définition précède l'appel à la fonction qui les opère (qui, à son tour , à déclarer avant son appel).

Notez également qu'une chose similaire se produit lorsque les fonctions sont exportées. Un shell qui importe une fonction peut avoir un framework prêt et n'attendre que les définitions de fonction pour les mettre en action. L'exportation de fonction est présente dans Bash et a causé des problèmes auparavant graves, btw (qui s'appelait Shellshock):

Je vais compléter cette réponse avec une autre méthode pour passer une fonction à une autre fonction, qui n'est pas explicitement présente dans Bash. Celui-ci le transmet par adresse, pas par nom. Cela peut être trouvé en Perl, par exemple. Bash n'offre cette méthode ni pour les fonctions, ni pour les variables. Mais si, comme vous le dites, vous voulez avoir une image plus large avec Bash comme exemple, alors vous devez savoir que le code de fonction peut résider quelque part dans la mémoire et que ce code est accessible par cet emplacement de mémoire, qui est appelé son adresse.

Tomasz
la source
2

L'un des exemples les plus simples de rappel en bash est celui que beaucoup de gens connaissent mais ne réalisent pas quel modèle de conception ils utilisent réellement:

cron

Cron vous permet de spécifier un exécutable (un binaire ou un script) que le programme cron rappellera lorsque certaines conditions seront remplies (la spécification d'heure)

Disons que vous avez un script appelé doEveryDay.sh. La méthode sans callback pour écrire le script est:

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

La méthode de rappel pour l'écrire est simplement:

#! /bin/bash
doSomething

Ensuite, dans crontab, vous définissez quelque chose comme

0 0 * * *     doEveryDay.sh

Vous n'auriez alors pas besoin d'écrire le code pour attendre que l'événement se déclenche, mais vous devriez plutôt compter cronpour rappeler votre code.


Maintenant, considérez COMMENT vous écririez ce code en bash.

Comment exécuteriez-vous un autre script / fonction dans bash?

Écrivons une fonction:

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
}

Vous venez de créer une fonction qui accepte un rappel. Vous pouvez simplement l'appeler comme ceci:

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

Bien sûr, la fonction toutes les 24 heures ne revient jamais. Bash est un peu unique en ce sens que nous pouvons très facilement le rendre asynchrone et générer un processus en ajoutant &:

every24hours 'curl google.com' &

Si vous ne voulez pas cela en tant que fonction, vous pouvez le faire comme un script à la place:

#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

Comme vous pouvez le voir, les rappels en bash sont triviaux. C'est simplement:

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

Et appeler le rappel est simplement:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Comme vous pouvez le voir ci-dessus, les rappels sont rarement directement des fonctionnalités des langues. Ils programment généralement de manière créative en utilisant les fonctionnalités linguistiques existantes. Tout langage capable de stocker un pointeur / une référence / une copie d'un bloc de code / d'une fonction / d'un script peut implémenter des rappels.

Slebetman
la source
D'autres exemples de programmes / scripts qui acceptent les rappels incluent watchet find(lorsqu'ils sont utilisés avec le -execparamètre)
slebetman
0

Un rappel est une fonction appelée lorsqu'un événement se produit. Avec bash, le seul mécanisme de gestion des événements en place est lié aux signaux, à la sortie du shell et étendu aux événements d'erreurs du shell, aux événements de débogage et aux événements de fonction / scripts renvoyés.

Voici un exemple d'un rappel inutile mais simple exploitant des pièges à signaux.

Créez d'abord le script implémentant le rappel:

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

Exécutez ensuite le script dans un terminal:

$ ./callback-example

et sur un autre, envoyer le USR1signal au processus shell.

$ pkill -USR1 callback-example

Chaque signal envoyé devrait déclencher l'affichage de lignes comme celles-ci dans le premier terminal:

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

ksh93, en tant que shell implémentant de nombreuses fonctionnalités qui ont bashété adoptées par la suite, fournit ce qu'il appelle des «fonctions de discipline». Ces fonctions, non disponibles avec bash, sont appelées lorsqu'une variable shell est modifiée ou référencée (c'est-à-dire lue). Cela ouvre la voie à des applications événementielles plus intéressantes.

Par exemple, cette fonctionnalité a permis aux rappels de style X11 / Xt / Motif sur les widgets graphiques d'être implémentés dans une ancienne version des kshextensions graphiques appelées dtksh. Voir le manuel dksh .

jlliagre
la source