web-dev-qa-db-fra.com

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

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? Si c'est le cas, veuillez répondre avec un petit exemple simple de Bash.

21
user149572

En typique programmation impérative , 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 gestion des erreurs et quelques autres situations; par exemple (il faut d'abord comprendre substitution de commande et Bash modes de sortie 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 l'essayer vous-même, enregistrez ce qui précède dans un fichier, dites cleanUpOnExit.sh, rendez-le exécutable et exécutez-le:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Mon code ici n'appelle jamais explicitement la fonction cleanup; il indique à Bash quand l'appeler, en utilisant trap cleanup EXIT, ie "cher Bash, veuillez exécuter la commande cleanup lorsque vous quittez" (et cleanup arrive à être 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 fonction cleanup, qui est "rappelée" par Bash juste avant la fermeture du shell.

Vous pouvez utiliser la capacité de Bash pour évaluer les paramètres Shell en tant que commandes, pour créer un cadre orienté rappel; cela dépasse quelque peu la portée de cette réponse, et pourrait peut-être causer plus de confusion en suggérant que le passage 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 cp peut gérer plusieurs fichiers, c'est uniquement à titre d'illustration.)

Ici, nous créons une fonction, doonall, qui 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 fonction backup 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: la responsabilité de doonall est d’exécuter quelque chose sur tous ses arguments, un à la fois; La responsabilité de backup est de faire une copie de son (unique) argument dans un répertoire de sauvegarde. doonall et backup peuvent ê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 fonction backup, à laquelle nous demandons à doonall de "rappeler" sur chacun de ses autres arguments - nous fournissons à doonall un comportement (son premier argument ) ainsi que des 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 , plutôt que d'enregistrer des rappels dans un système orienté événement.)

46
Stephen Kitt

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é en tant que fonction, mais en tant que 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 le -exec action de la commande find. Le travail de la commande find consiste à parcourir les répertoires de manière récursive et à traiter chaque fichier tour à tour. Par défaut, le traitement consiste à imprimer le nom du fichier (implicite -print), mais avec -exec le traitement consiste à exécuter une commande que vous spécifiez. Cela correspond à la définition d'un rappel, bien que lors des rappels, il 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 que call_on_regular_files trouve un fichier normal.

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.

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 ...]

appliquera filter à chaque file argument, puis appellera command avec 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 dans 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éralementaccepté . Et l'une des raisons pour lesquelles les "rappels" dans ces sens étroits ne sont pas très utiles dans 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 Perl ou python.

7
mosvy

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é en 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.

4
user1934428

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.

3
user147505

L'un des exemples les plus simples de rappel dans 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)

Supposons que vous ayez un script appelé doEveryDay.sh. La façon non callback d'é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 plutôt de compter sur cron pour 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 pouvant 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.

2
slebetman

Un rappel est une fonction appelée lorsqu'un événement se produit. Avec bash, le seul mécanisme de gestion d'é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, envoyez le USR1 signal 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, comme Shell implémentant de nombreuses fonctionnalités que bash a adopté plus tard, 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 de ksh qui comprenait des extensions graphiques appelées dtksh. Voir manuel dksh .

0
jlliagre