O conceito de callback de programação existe no Bash?

20

Algumas vezes, quando li sobre programação, deparei-me com o conceito de "retorno de chamada".

Engraçadamente, eu nunca encontrei uma explicação que eu possa chamar de "didática" ou "clara" para esse termo "função de callback" (quase qualquer explicação que eu lesse me pareceu bastante diferente da outra e me senti confusa).

O conceito "callback" de programação existe no Bash? Em caso afirmativo, responda com um pequeno e simples exemplo de Bash.

    
por JohnDoea 21.09.2018 / 06:22

7 respostas

42

Na típica programação imperativa , você escreve sequências de instruções e elas são executadas uma após a outra, com fluxo de controle explícito . Por exemplo:

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

etc.

Como pode ser visto no exemplo, na programação imperativa, você segue o fluxo de execução com bastante facilidade, sempre subindo a partir de qualquer linha de código para determinar seu contexto de execução, sabendo que todas as instruções dadas serão executadas como um resultado de sua localização no fluxo (ou nas localizações de seus locais de chamada, se você estiver escrevendo funções).

Como as chamadas de retorno mudam o fluxo

Quando você usa callbacks, em vez de colocar o uso de um conjunto de instruções “geograficamente”, você descreve quando deve ser chamado. Exemplos típicos em outros ambientes de programação são casos como “baixe este recurso e, quando o download estiver completo, chame este retorno de chamada”. O Bash não tem uma construção de retorno de chamada genérica desse tipo, mas ele possui callbacks, para tratamento de erros e algumas outras situações; por exemplo (primeiro é preciso entender substituição de comandos e Bash exit modes para entender esse exemplo):

#!/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 você quiser testar isso sozinho, salve o arquivo acima, diga cleanUpOnExit.sh , torne-o executável e execute-o:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Meu código aqui nunca chama explicitamente a função cleanup ; ele diz ao Bash quando chamá-lo, usando trap cleanup EXIT , ie “querido Bash, por favor execute o comando cleanup quando você sair” (e cleanup é uma função que defini anteriormente, mas pode ser qualquer coisa que Bash entenda). O Bash suporta isso para todos os sinais não fatais, saídas, falhas de comando e depuração geral (você pode especificar um retorno de chamada que é executado antes de cada comando). O retorno de chamada aqui é a função cleanup , que é “chamada de volta” pelo Bash logo antes da saída do shell.

Você pode usar a capacidade do Bash para avaliar os parâmetros do shell como comandos, para criar um framework orientado ao retorno de chamada; Isso está um pouco além do escopo dessa resposta e talvez cause mais confusão sugerindo que a passagem de funções sempre envolva retornos de chamada. Veja Bash: passe uma função como parâmetro para alguns exemplos da funcionalidade subjacente. A ideia aqui, como ocorre com retornos de chamada de tratamento de eventos, é que as funções podem usar dados como parâmetros, mas também outras funções - isso permite que os chamadores forneçam tanto comportamento quanto dados. Um exemplo simples dessa abordagem poderia se parecer com

#!/bin/bash

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

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

doonall backup "$@"

(Eu sei que isso é um pouco inútil, pois cp pode lidar com vários arquivos, é apenas para ilustração.)

Aqui criamos uma função, doonall , que recebe outro comando, dado como parâmetro, e aplica-o ao resto de seus parâmetros; então usamos isso para chamar a função backup em todos os parâmetros fornecidos para o script. O resultado é um script que copia todos os seus argumentos, um por um, para um diretório de backup.

Esse tipo de abordagem permite que as funções sejam escritas com responsabilidades únicas: doonall é responsável por executar algo em todos os seus argumentos, um de cada vez; A responsabilidade de backup é fazer uma cópia de seu argumento (único) em um diretório de backup. Tanto doonall como backup podem ser usados em outros contextos, o que permite mais reutilização de código, melhores testes, etc.

Nesse caso, o retorno de chamada é a função backup , que diz doonall para "chamar de volta" em cada um dos seus outros argumentos - nós fornecemos doonall com comportamento (seu primeiro argumento) e dados ( os argumentos restantes).

(Observe que, no tipo de caso de uso demonstrado no segundo exemplo, eu não usaria o termo "callback", mas talvez seja um hábito resultante dos idiomas que uso. Penso nisso como funções passageiras ou lambdas, em vez de registrar retornos de chamada em um sistema orientado a eventos.)

    
por 21.09.2018 / 09:10
23

Primeiro, é importante notar que o que torna uma função de retorno de chamada é como ela é usada, e não o que ela faz. Um callback é quando o código que você escreve é chamado pelo código que você não escreveu. Você está pedindo que o sistema ligue de volta quando algum evento em particular acontecer.

Um exemplo de retorno de chamada na programação shell é traps. Uma armadilha é um retorno de chamada que não é expresso como uma função, mas como um pedaço de código para avaliar. Você está pedindo ao shell para chamar seu código quando o shell recebe um sinal particular.

Outro exemplo de retorno de chamada é a ação -exec do comando find . O trabalho do comando find é percorrer os diretórios recursivamente e processar cada arquivo por vez. Por padrão, o processamento é para imprimir o nome do arquivo ( -print implícito), mas com -exec o processamento deve executar um comando especificado por você. Isso se encaixa na definição de um retorno de chamada, embora em retornos de chamada, ele não é muito flexível, pois o retorno de chamada é executado em um processo separado.

Se você implementou uma função de busca, você pode usar uma função de retorno de chamada para chamar cada arquivo. Aqui está uma função de busca simplificada que leva um nome de função (ou nome de comando externo) como argumento e o chama em todos os arquivos regulares no diretório atual e em seus subdiretórios. A função é usada como um callback que é chamado toda vez que call_on_regular_files encontra um arquivo regular.

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

Callbacks não são tão comuns na programação shell como em outros ambientes, porque os shells são projetados principalmente para programas simples. Os retornos de chamada são mais comuns em ambientes em que os dados e o fluxo de controle têm maior probabilidade de alternar entre partes do código que são gravadas e distribuídas independentemente: o sistema base, várias bibliotecas, o código do aplicativo.

    
por 21.09.2018 / 09:10
7

"callbacks" são apenas funções passadas como argumentos para outras funções.

No nível do shell, isso significa simplesmente scripts / functions / commands passados como argumentos para outros scripts / funções / comandos.

Agora, para um exemplo simples, considere o seguinte 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"

tendo a sinopse

x command filter [file ...]

aplicará filter a cada argumento file e, em seguida, chamará command com as saídas dos filtros como argumentos.

Por exemplo:

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

Isso é muito parecido com o que você pode fazer no lisp (brincadeirinha; -))

Algumas pessoas insistem em limitar o termo "retorno de chamada" para "manipulador de eventos" e / ou "fechamento" (tupla função + dados / ambiente); isto não é de forma alguma a geralmente aceito significado. E uma das razões pelas quais os "callbacks" nesses sentidos restritos não são muito úteis no shell é porque os recursos de + pipes + parallelism + programação dinâmica são muito mais poderosos, e você já está pagando por eles em termos de desempenho, mesmo se você tente usar o shell como uma versão desajeitada de perl ou python .

    
por 21.09.2018 / 10:52
4

Mais ou menos.

Uma maneira simples de implementar um retorno de chamada no bash é aceitar o nome de um programa como um parâmetro, que atua como "função de retorno de chamada".

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

Isso seria usado assim:

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

Claro que você não tem encerramentos no bash. Portanto, a função de retorno de chamada não tem acesso às variáveis no lado do chamador. Você pode, no entanto, armazenar dados que o retorno de chamada precisa em variáveis de ambiente. Passar informações do retorno de chamada para o script do invocador é mais complicado. Os dados podem ser colocados em um arquivo.

Se o seu design permitir que tudo seja tratado em um único processo, você poderia usar uma função de shell para o retorno de chamada e, nesse caso, a função de retorno de chamada tem acesso às variáveis no lado do invocador.

    
por 21.09.2018 / 10:27
3

Apenas para adicionar algumas palavras às outras respostas. O retorno de chamada da função opera na (s) função (ões) externa (s) para a função que chama de volta. Para que isso seja possível, toda a definição da função a ser chamada de volta precisa ser passada para a função que está chamando de volta, ou seu código deve estar disponível para a função que está chamando de volta.

O primeiro (passando o código para outra função) é possível, embora eu pule um exemplo para isso envolveria complexidade. O último (passando a função pelo nome) é uma prática comum, pois as variáveis e funções declaradas fora do escopo de uma função estão disponíveis naquela função desde que sua definição preceda a chamada à função que opera nelas (que, por sua vez, , como ser declarado antes de ser chamado).

Observe também que algo semelhante acontece quando as funções são exportadas. Um shell que importa uma função pode ter um framework pronto e estar apenas esperando por definições de função para colocá-las em ação. A exportação de função está presente no Bash e causou problemas anteriormente sérios, btw (que foi chamado de Shellshock):

Eu completarei essa resposta com mais um método de passar uma função para outra função, que não está explicitamente presente no Bash. Este está passando por endereço, não por nome. Isso pode ser encontrado em Perl, por exemplo. Bash oferece desta forma nem para funções, nem variáveis. Mas se, como você afirma, você quiser ter uma imagem mais ampla com o Bash como apenas um exemplo, então você deve saber que o código de função pode residir em algum lugar na memória e que o código pode ser acessado por essa localização de memória. chamou seu endereço.

    
por 21.09.2018 / 12:19
2

Um dos exemplos mais simples de retorno de chamada no bash é um com o qual muitas pessoas estão familiarizadas, mas não percebem qual padrão de design estão realmente usando:

cron

Cron permite que você especifique um executável (um binário ou script) que o programa cron chamará quando algumas condições forem atendidas (a especificação de tempo)

Digamos que você tenha um script chamado doEveryDay.sh . A maneira sem retorno de chamada para escrever o script é:

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

O caminho de retorno para escrever é simplesmente:

#! /bin/bash
doSomething

Então no crontab você configurou algo como

0 0 * * *     doEveryDay.sh

Você não precisaria escrever o código para aguardar o evento ser acionado, mas sim confiar em cron para chamar seu código de volta.

Agora, pense em COMO você escreveria esse código no bash.

Como você executaria outro script / função no bash?

Vamos escrever uma função:

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
}

Agora você criou uma função que aceita um retorno de chamada. Você pode simplesmente chamar assim:

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

É claro que a função every24hours nunca retorna. O Bash é um pouco único, pois podemos torná-lo muito assíncrono e gerar um processo adicionando & :

every24hours 'curl google.com' &

Se você não quiser isso como uma função, pode fazer isso como um 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 você pode ver, os retornos de chamada no bash são triviais. É simplesmente:

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

E chamar o retorno de chamada é simplesmente:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Como você pode ver o formulário acima, os retornos de chamada raramente são diretamente recursos de idiomas. Eles geralmente estão programando de maneira criativa usando os recursos de linguagem existentes. Qualquer idioma que possa armazenar um ponteiro / referência / cópia de algum bloco de código / função / script pode implementar retornos de chamada.

    
por 23.09.2018 / 10:13
0

Um retorno de chamada é uma função chamada quando ocorre algum evento. Com bash , o único mecanismo de manipulação de eventos no local está relacionado a sinais, a saída de shell e estendido a eventos de erros de shell, eventos de depuração e scripts de função / origem retornam eventos.

Aqui está um exemplo de um callback inútil, porém simples, que alavanca capturas de sinal.

Primeiro, crie o script que implementa o retorno de chamada:

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

Em seguida, execute o script em um terminal:

$ ./callback-example

e em outro, envie o sinal USR1 para o processo de shell.

$ pkill -USR1 callback-example

Cada sinal enviado deve acionar a exibição de linhas como estas no primeiro terminal:

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

ksh93 , como shell implementando muitos recursos que bash adotou posteriormente, fornece o que chama de "funções de disciplina". Essas funções, não disponíveis com bash , são chamadas quando uma variável shell é modificada ou referenciada (por exemplo, leitura). Isso abre o caminho para aplicativos orientados a eventos mais interessantes.

Por exemplo, esse recurso permitia que retornos de chamada de estilo X11 / Xt / Motif em widgets gráficos fossem implementados em uma versão antiga de ksh que incluía extensões gráficas chamadas dtksh . Consulte o manual do dksh .

    
por 25.09.2018 / 00:58

Tags