¿Existe el concepto de programación de "devolución de llamada" en Bash?

21

Algunas veces, cuando leí sobre programación, me encontré con el concepto de "devolución de llamada".

Curiosamente, nunca encontré una explicación que pueda llamar "didáctica" o "clara" para este término "función de devolución de llamada" (casi cualquier explicación que leí me pareció lo suficientemente diferente de otra y me sentí confundido).

¿El concepto de "devolución de llamada" de programación existe en Bash? Si es así, responda con un pequeño y simple ejemplo de Bash.

JohnDoea
fuente
2
¿Es "devolución de llamada" un concepto real o es "función de primera clase"?
Cedric H.
Puede encontrar declarative.bashinteresante, como un marco que aprovecha explícitamente las funciones configuradas para ser invocadas cuando se necesita un valor dado.
Charles Duffy
Otro marco relevante: bashup / eventos . Su documentación incluye muchas demostraciones simples de uso de devolución de llamada, como validación, búsquedas, etc.
PJ Eby
1
@CedricH. Votado por ti. "¿Es la" devolución de llamada "un concepto real o es una" función de primera clase "? ¿Es una buena pregunta como otra pregunta?
Prosody-Gab Vereable Context
Entiendo que la devolución de llamada significa "una función que se vuelve a llamar después de que se activó un evento determinado". ¿Es eso correcto?
JohnDoea

Respuestas:

44

En la programación imperativa típica , se escriben secuencias de instrucciones y se ejecutan una tras otra, con un flujo de control explícito. Por ejemplo:

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

etc.

Como se puede ver en el ejemplo, en la programación imperativa sigue el flujo de ejecución con bastante facilidad, siempre avanzando desde cualquier línea de código dada para determinar su contexto de ejecución, sabiendo que cualquier instrucción que dé se ejecutará como resultado de su ubicación en el flujo (o las ubicaciones de sus sitios de llamadas, si está escribiendo funciones).

Cómo las devoluciones de llamada cambian el flujo

Cuando usa devoluciones de llamada, en lugar de colocar el uso de un conjunto de instrucciones "geográficamente", describe cuándo debe llamarse. Ejemplos típicos en otros entornos de programación son casos como "descargue este recurso, y cuando la descarga esté completa, llame a esta devolución de llamada". Bash no tiene una construcción de devolución de llamada genérica de este tipo, pero tiene devoluciones de llamada, para el manejo de errores y algunas otras situaciones; por ejemplo (primero hay que entender la sustitución de comandos y los modos de salida de Bash para comprender ese ejemplo):

#!/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 desea probar esto usted mismo, guarde lo anterior en un archivo, por ejemplo cleanUpOnExit.sh, hágalo ejecutable y ejecútelo:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Mi código aquí nunca llama explícitamente a la cleanupfunción; le dice a Bash cuándo llamarlo trap cleanup EXIT, es decir , "querido Bash, ejecuta el cleanupcomando cuando salgas" (y cleanupresulta ser una función que definí anteriormente, pero podría ser cualquier cosa que Bash entienda). Bash admite esto para todas las señales no fatales, salidas, fallas de comandos y depuración general (puede especificar una devolución de llamada que se ejecuta antes de cada comando). La devolución de llamada aquí es la cleanupfunción, que Bash "vuelve a llamar" justo antes de que salga el shell.

Puede usar la capacidad de Bash para evaluar los parámetros de shell como comandos, para construir un marco orientado a la devolución de llamadas; eso está algo más allá del alcance de esta respuesta, y tal vez causaría más confusión al sugerir que pasar funciones siempre implica devoluciones de llamada. Ver Bash: pasar una función como parámetro para algunos ejemplos de la funcionalidad subyacente. La idea aquí, al igual que con las devoluciones de llamadas de manejo de eventos, es que las funciones pueden tomar datos como parámetros, pero también otras funciones, esto permite a las personas que llaman proporcionar comportamiento y datos. Un ejemplo simple de este enfoque podría ser

#!/bin/bash

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

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

doonall backup "$@"

(Sé que esto es un poco inútil ya que cppuede manejar múltiples archivos, es solo para ilustración).

Aquí creamos una función, doonallque toma otro comando, dado como parámetro, y lo aplica al resto de sus parámetros; luego usamos eso para llamar a la backupfunción en todos los parámetros dados al script. El resultado es un script que copia todos sus argumentos, uno por uno, en un directorio de respaldo.

Este tipo de enfoque permite que las funciones se escriban con responsabilidades únicas: doonallla responsabilidad es ejecutar algo en todos sus argumentos, uno a la vez; backupLa responsabilidad de él es hacer una copia de su (único) argumento en un directorio de respaldo. Ambos doonally backupse pueden usar en otros contextos, lo que permite una mayor reutilización del código, mejores pruebas, etc.

En este caso, la devolución de llamada es la backupfunción, a la que le decimos doonallque "devuelva la llamada" a cada uno de sus otros argumentos: proporcionamos doonallcomportamiento (su primer argumento) así como datos (los argumentos restantes).

(Tenga en cuenta que en el tipo de caso de uso demostrado en el segundo ejemplo, yo no usaría el término "devolución de llamada", pero ese es quizás un hábito resultante de los idiomas que uso. Pienso en esto como pasar funciones o lambdas alrededor , en lugar de registrar devoluciones de llamada en un sistema orientado a eventos).

Stephen Kitt
fuente
25

Primero, es importante tener en cuenta que lo que hace que una función sea una función de devolución de llamada es cómo se usa, no lo que hace. Una devolución de llamada es cuando el código que escribe se llama desde el código que no escribió. Le está pidiendo al sistema que le devuelva la llamada cuando ocurra algún evento en particular.

Un ejemplo de devolución de llamada en la programación de shell son las trampas. Una trampa es una devolución de llamada que no se expresa como una función, sino como un fragmento de código para evaluar. Le está pidiendo al shell que llame a su código cuando el shell recibe una señal particular.

Otro ejemplo de devolución de llamada es la -execacción del findcomando. El trabajo del findcomando es recorrer directorios de manera recursiva y procesar cada archivo a su vez. Por defecto, el procesamiento es imprimir el nombre del archivo (implícito -print), pero con -execel procesamiento es ejecutar un comando que usted especifique. Esto se ajusta a la definición de una devolución de llamada, aunque en las devoluciones de llamada, no es muy flexible ya que la devolución de llamada se ejecuta en un proceso separado.

Si implementó una función de búsqueda, puede hacer que use una función de devolución de llamada para llamar a cada archivo. Aquí hay una función de búsqueda ultra simplificada que toma un nombre de función (o nombre de comando externo) como argumento y lo llama a todos los archivos normales en el directorio actual y sus subdirectorios. La función se utiliza como una devolución de llamada que se llama cada vez que call_on_regular_filesencuentra un archivo 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
}

Las devoluciones de llamada no son tan comunes en la programación de shell como en algunos otros entornos porque los shells están diseñados principalmente para programas simples. Las devoluciones de llamada son más comunes en entornos donde los datos y el flujo de control tienen más probabilidades de moverse hacia adelante y hacia atrás entre las partes del código que se escriben y distribuyen de forma independiente: el sistema base, varias bibliotecas, el código de la aplicación.

Gilles 'SO- deja de ser malvado'
fuente
1
Particularmente bien explicado
roaima
1
@JohnDoea Creo que la idea es que está ultra simplificado porque no es una función que realmente escribirías. Pero tal vez un ejemplo aún más simple sería algo con una lista no modificable para ejecutar la devolución de llamada en: 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; }la que usted podría funcionar como foreach_server echo, foreach_server nslookup, etc. El declare callback="$1"es tan simple como se puede conseguir sin embargo: la devolución de llamada tiene que ser aprobada en algún lugar, o no es una devolución de llamada.
IMSoP
44
'Una devolución de llamada es cuando el código que escribes se llama desde un código que no escribiste'. Simplemente está mal. Puede escribir algo que haga un trabajo asincrónico sin bloqueo y ejecutarlo con una devolución de llamada que se ejecutará cuando se complete. Nada está relacionado con quién escribió el código,
mikemaccana
55
@mikemaccana Por supuesto, es posible que la misma persona haya escrito las dos partes del código. Pero no es el caso común. Estoy explicando los conceptos básicos de un concepto, no dando una definición formal. Si explica todos los casos de esquina, es difícil transmitir los conceptos básicos.
Gilles 'SO- deja de ser malvado'
1
Alegra oírlo. No estoy de acuerdo con que las personas que escriben tanto el código que usa una devolución de llamada como la devolución de llamada no sean comunes o sean un caso marginal y, debido a la confusión, que esta respuesta transmite los conceptos básicos.
mikemaccana
7

Las "devoluciones de llamada" son solo funciones pasadas como argumentos a otras funciones.

A nivel de shell, eso simplemente significa scripts / funciones / comandos pasados ​​como argumentos a otros scripts / funciones / comandos.

Ahora, para un ejemplo simple, considere el siguiente 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"

teniendo la sinopsis

x command filter [file ...]

se aplicará filtera cada fileargumento, luego llame commandcon las salidas de los filtros como argumentos.

Por ejemplo:

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

Esto está muy cerca de lo que puedes hacer en lisp (es broma ;-))

Algunas personas insisten en limitar el término "devolución de llamada" a "controlador de eventos" y / o "cierre" (función + datos / tupla de entorno); Este no es el significado generalmente aceptado . Y una de las razones por las que las "devoluciones de llamada" en esos sentidos estrechos no son de mucha utilidad en shell es porque las capacidades de programación de tuberías + paralelismo + dinámico son mucho más potentes, y ya está pagando por ellas en términos de rendimiento, incluso si usted intente usar el shell como una versión torpe de perlo python.

Mosvy
fuente
Si bien su ejemplo parece bastante útil, es lo suficientemente denso como para tener que separarlo realmente con el manual de bash abierto para descubrir cómo funciona (y he trabajado con bash más simple la mayoría de los días durante años). Nunca aprendí ceceo. ;)
Joe
1
@ Joe si está bien para trabajar con sólo dos archivos de entrada y sin %interpolación en los filtros, todo el asunto podría reducirse a: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Pero eso es mucho menos útil e ilustrativo en mi humilde opinión.
mosvy
1
O incluso mejor$1 <($2 "$3") <($2 "$4")
mosvy
+1 gracias. Sus comentarios, además de mirarlo y jugar con el código durante algún tiempo, me lo aclararon. También aprendí un nuevo término, "interpolación de cadenas", para algo que he estado usando desde siempre.
Joe
4

Mas o menos.

Una forma sencilla de implementar una devolución de llamada en bash es aceptar el nombre de un programa como parámetro, que actúa como "función de devolución de llamada".

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

Esto se usaría así:

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

Por supuesto que no tienes cierres en bash. Por lo tanto, la función de devolución de llamada no tiene acceso a las variables en el lado del llamante. Sin embargo, puede almacenar datos que la devolución de llamada necesita en variables de entorno. Pasar la información desde la devolución de llamada al script de invocador es más complicado. Los datos se pueden colocar en un archivo.

Si su diseño permite que todo se maneje en un solo proceso, puede usar una función de shell para la devolución de llamada, y en este caso la función de devolución de llamada tiene, por supuesto, acceso a las variables en el lado del invocador.

usuario1934428
fuente
3

Solo para agregar algunas palabras a las otras respuestas. La función de devolución de llamada funciona en funciones externas a la función que devuelve la llamada. Para que esto sea posible, se debe pasar una definición completa de la función a la que se debe volver a llamar a la función que devuelve la llamada, o su código debe estar disponible para la función que vuelve a llamar.

El primero (pasar código a otra función) es posible, aunque omitiré un ejemplo para esto implicaría complejidad. El último (pasar la función por nombre) es una práctica común, ya que las variables y funciones declaradas fuera del alcance de una función están disponibles en esa función siempre que su definición preceda a la llamada a la función que opera en ellas (que, a su vez , como se declarará antes de que se llame).

También tenga en cuenta que sucede algo similar cuando se exportan funciones. Un shell que importa una función puede tener un marco listo y estar esperando las definiciones de función para ponerlas en acción. La exportación de funciones está presente en Bash y causó problemas anteriormente serios, por cierto (que se llamaba Shellshock):

Completaré esta respuesta con un método más de pasar una función a otra función, que no está explícitamente presente en Bash. Este lo pasa por dirección, no por nombre. Esto se puede encontrar en Perl, por ejemplo. Bash ofrece de esta manera ni funciones ni variables. Pero si, como usted dice, desea tener una imagen más amplia con Bash como ejemplo, entonces debe saber que el código de función puede residir en algún lugar de la memoria, y esa ubicación de memoria puede acceder a ese código, que es llamó a su dirección.

Tomász
fuente
2

Uno de los ejemplos más simples de devolución de llamada en bash es uno con el que muchas personas están familiarizadas, pero no se dan cuenta del patrón de diseño que realmente están utilizando:

cron

Cron le permite especificar un ejecutable (un binario o script) al que el programa cron devolverá la llamada cuando se cumplan algunas condiciones (la especificación de tiempo)

Digamos que tiene un guión llamado doEveryDay.sh. La forma sin devolución de llamada para escribir el script es:

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

La forma de devolución de llamada para escribirlo es simplemente:

#! /bin/bash
doSomething

Luego, en crontab, establecerías algo como

0 0 * * *     doEveryDay.sh

Entonces no necesitará escribir el código para esperar a que se active el evento, sino confiar en crondevolverle el código.


Ahora, considere CÓMO escribiría este código en bash.

¿Cómo ejecutarías otro script / función en bash?

Escribamos una función:

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
}

Ahora ha creado una función que acepta una devolución de llamada. Simplemente puede llamarlo así:

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

Por supuesto, la función every24hours nunca regresa. Bash es un poco único en el sentido de que podemos hacerlo fácilmente asíncrono y generar un proceso agregando &:

every24hours 'curl google.com' &

Si no desea esto como una función, puede hacerlo como un 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

Como puede ver, las devoluciones de llamada en bash son triviales. Es sencillo:

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

Y llamar a la devolución de llamada es simplemente:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Como puede ver en el formulario anterior, las devoluciones de llamada rara vez son características directas de los idiomas. Por lo general, están programando de manera creativa utilizando las funciones de lenguaje existentes. Cualquier lenguaje que pueda almacenar un puntero / referencia / copia de algún bloque de código / función / script puede implementar devoluciones de llamada.

slebetman
fuente
Otros ejemplos de programas / scripts que aceptan devoluciones de llamada incluyen watchy find(cuando se usan con -execparámetros)
slebetman el
0

Una devolución de llamada es una función llamada cuando se produce algún evento. Conbash , el único mecanismo de manejo de eventos implementado está relacionado con las señales, la salida del shell y los eventos de errores extendidos al shell, eventos de depuración y eventos de retorno de scripts de función / fuente.

Aquí hay un ejemplo de una devolución de llamada inútil pero simple aprovechando trampas de señal.

Primero cree el script implementando la devolución de llamada:

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

Luego ejecute el script en una terminal:

$ ./callback-example

y en otro, envíe la USR1señal al proceso de shell.

$ pkill -USR1 callback-example

Cada señal enviada debe activar la visualización de líneas como estas en el primer terminal:

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

ksh93, ya que Shell implementa muchas características que bashluego adoptaron, proporciona lo que llama "funciones de disciplina". Estas funciones, no disponibles con bash, se invocan cuando una variable de shell se modifica o hace referencia (es decir, se lee). Esto abre el camino a aplicaciones más interesantes impulsadas por eventos.

Por ejemplo, esta característica permitió implementar devoluciones de llamada de estilo X11 / Xt / Motif en widgets gráficos en una versión anterior de las kshextensiones gráficas incluidas llamadas dtksh. Ver el manual de dksh .

jlliagre
fuente