O conceito de "retorno de chamada" da programação existe no Bash?

21

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

O engraçado é que nunca encontrei uma explicação que possa chamar de "didática" ou "clara" para esse termo "função de retorno de chamada" (quase todas as explicações que li me pareceram bastante diferentes das outras e me senti confuso).

O conceito de "retorno de chamada" da programação existe no Bash? Nesse caso, responda com um exemplo pequeno e simples do Bash.

JohnDoea
fonte
2
O "retorno de chamada" é um conceito real ou é uma "função de primeira classe"?
Cedric H.
Você pode achar declarative.bashinteressante, como uma estrutura que explora explicitamente as funções configuradas para serem invocadas quando um determinado valor é necessário.
Charles Duffy
Outra estrutura relevante: bashup / eventos . Sua documentação inclui muitas demonstrações simples de uso de retorno de chamada, como validação, pesquisas etc.
PJ Eby
11
@CedricH. Votou para você. "Retorno de chamada" é um conceito real ou "função de primeira classe"? "É uma boa pergunta a ser feita como outra pergunta?
prosody-Gab Vereable Context
Entendo retorno de chamada como "uma função que é chamada de volta depois que um determinado evento foi acionado". Isso está correto?
JohnDoea 23/09

Respostas:

44

Na programação imperativa típica , 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 fornecidas serão executadas como resultado de sua execução. local no fluxo (ou nos locais dos sites de chamadas, se você estiver escrevendo funções).

Como os retornos de chamada alteram o fluxo

Ao usar retornos de chamada, 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 “baixar este recurso e, quando o download estiver concluído, chame esse retorno de chamada”. O Bash não possui uma construção genérica de retorno de chamada desse tipo, mas possui retornos de chamada, para tratamento de erros e algumas outras situações; por exemplo (é preciso primeiro entender os modos de substituição de comando e saída do Bash 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 tentar fazer isso sozinho, salve o acima em um arquivo, por exemplo cleanUpOnExit.sh, torne-o executável e execute-o:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Meu código aqui nunca chama explicitamente a cleanupfunção; ele diz ao Bash quando chamá-lo, usando trap cleanup EXIT, ou seja , “caro Bash, execute o cleanupcomando quando você sair” (e cleanupé uma função que eu defini anteriormente, mas pode ser qualquer coisa que o 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 cleanupfunção, que é "chamada de volta" por Bash imediatamente antes da saída do shell.

Você pode usar a capacidade do Bash para avaliar os parâmetros do shell como comandos, para criar uma estrutura orientada a retorno de chamada; isso está um pouco além do escopo desta resposta e talvez causasse mais confusão ao sugerir que a passagem de funções sempre envolva retornos de chamada. Consulte Bash: passe uma função como parâmetro para alguns exemplos da funcionalidade subjacente. A idéia aqui, como nos retornos de chamada de manipulação de eventos, é que as funções podem receber dados como parâmetros, mas também outras funções - isso permite que os chamadores forneçam comportamento e dados. Um exemplo simples dessa abordagem pode parecer

#!/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 cppode lidar com vários arquivos, é apenas para ilustração.)

Aqui criamos uma função, doonallque pega outro comando, dado como parâmetro, e a aplica ao restante de seus parâmetros; então usamos isso para chamar a backupfunção em todos os parâmetros fornecidos ao 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: doonalla responsabilidade é executar algo em todos os seus argumentos, um de cada vez; backupA responsabilidade de fazer uma cópia do seu argumento (único) em um diretório de backup. Ambos doonalle backuppodem ser usados ​​em outros contextos, o que permite mais reutilização de código, melhores testes etc.

Nesse caso, o retorno de chamada é a backupfunção, que dizemos doonallpara "retornar" em cada um de seus outros argumentos - fornecemos tanto o doonallcomportamento (seu primeiro argumento) quanto os dados (os argumentos restantes).

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

Stephen Kitt
fonte
25

Primeiro, é importante observar que o que faz de uma função uma função de retorno de chamada é como ela é usada, não o que faz. Um retorno de chamada é quando o código que você escreve é ​​chamado a partir do código que você não escreveu. Você está pedindo ao sistema para ligar de volta quando algum evento específico acontecer.

Um exemplo de retorno de chamada na programação de shell é traps. Uma interceptação é um retorno de chamada que não é expresso como uma função, mas como um pedaço de código para avaliar. Você está solicitando que o shell chame seu código quando o shell recebe um sinal específico.

Outro exemplo de retorno de chamada é a -execação do findcomando. O trabalho do findcomando é percorrer diretórios recursivamente e processar cada arquivo por vez. Por padrão, o processamento é para imprimir o nome do arquivo (implícito -print), mas com -execo processamento é para executar um comando que você especificar. Isso se encaixa na definição de um retorno de chamada, embora, no início, não seja muito flexível, pois o retorno de chamada é executado em um processo separado.

Se você implementou uma função de localização, você pode usar uma função de retorno de chamada para chamar cada arquivo. Aqui está uma função de localização ultra simplificada que pega um nome de função (ou nome de comando externo) como argumento e a chama em todos os arquivos regulares no diretório atual e em seus subdiretórios. A função é usada como um retorno de chamada que é chamado toda vez que call_on_regular_filesencontra 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
}

Os retornos de chamada não são tão comuns na programação de shell quanto 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.

Gilles 'SO- parar de ser mau'
fonte
11
Particularmente bem explicado
roaima 21/09/18
11
@JohnDoea Acho que a ideia é que seja ultra-simplificada, pois não é uma função que você realmente escreveria. Mas, talvez, um exemplo ainda mais simples seria algo com uma lista codificada para executar o retorno de chamada em: 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 você poderia funcionar como foreach_server echo, foreach_server nslookup, etc. O declare callback="$1"é tão simples como ele pode ficar no entanto: a chamada de retorno tem de ser aprovada em algum lugar, ou não é um retorno de chamada.
IMSOP
4
'Um retorno de chamada é quando o código que você escreve é ​​chamado a partir do código que você não escreveu.' está simplesmente errado. Você pode escrever algo que funcione de forma assíncrona sem bloqueio e executá-lo com um retorno de chamada que será executado quando concluído. Nada está relacionado ao que escreveu o código,
mikemaccana
5
@mikemaccana Claro que é possível que a mesma pessoa tenha escrito as duas partes do código. Mas não é o caso comum. Estou explicando o básico de um conceito, não dando uma definição formal. Se você explicar todos os casos de canto, é difícil transmitir o básico.
Gilles 'SO- stop be evil'
11
Fico feliz em ouvir isso. Não concordo que as pessoas que escrevem o código que usa um retorno de chamada e o retorno de chamada não sejam comuns ou sejam um caso delicado e, devido à confusão, que essa resposta transmita o básico.
Mikemaccana 21/09/19
7

"retornos de chamada" são apenas funções passadas como argumentos para outras funções.

No nível do shell, isso significa simplesmente scripts / funções / comandos 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 ...]

será aplicado filtera cada fileargumento e, em seguida, chame commandcom os resultados 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 próximo ao que você pode fazer no lisp (apenas brincando ;-))

Algumas pessoas insistem em limitar o termo "retorno de chamada" a "manipulador de eventos" e / ou "encerramento" (função + tupla de dados / ambiente); este não é de forma alguma o significado geralmente aceito . E uma razão pela qual "retornos de chamada" nesses sentidos restritos não são de muita utilidade no shell é porque os recursos de programação dinâmica pipes + paralelismo + programação são muito mais poderosos e você já está pagando por eles em termos de desempenho, mesmo se tente usar o shell como uma versão desajeitada de perlou python.

mosvy
fonte
Embora seu exemplo pareça bastante útil, é suficientemente denso que eu tenha que separá-lo com o manual do bash aberto para descobrir como ele funciona (e eu trabalhei com o bash mais simples na maioria dos dias durante anos.) Eu nunca aprendi lisp. ;)
Joe
11
@ Joe se ele está ok para o trabalho com apenas dois arquivos de entrada e sem %interpolação em filtros, a coisa toda poderia ser reduzido para: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Mas isso é muito menos útil e ilustrativo.
mosvy
11
Ou ainda melhor$1 <($2 "$3") <($2 "$4")
mosvy
+1 Obrigado. Seus comentários, além de encará-lo e brincar com o código por algum tempo, esclareceram isso para mim. Eu também aprendi um novo termo, "interpolação de strings", para algo que uso desde sempre.
Joe
4

Mais ou menos.

Uma maneira simples de implementar um retorno de chamada no bash é aceitar o nome de um programa como 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 fechamentos no bash. Portanto, a função de retorno de chamada não tem acesso às variáveis ​​no lado do chamador. No entanto, você pode armazenar dados que o retorno de chamada precisa em variáveis ​​de ambiente. Retornar 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ê poderá usar uma função shell para o retorno de chamada e, nesse caso, a função de retorno de chamada obviamente terá acesso às variáveis ​​no lado do invocador.

user1934428
fonte
3

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

O primeiro (passar código para outra função) é possível, embora eu pule um exemplo, pois isso envolveria complexidade. A última (passar 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 nessa função, desde que sua definição anteceda a chamada à função que opera nelas (que, por sua vez, , a 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 uma estrutura pronta e aguardar pelas definições de função para colocá-las em ação. A exportação de funções está presente no Bash e causou problemas anteriormente sérios, btw (que foi chamado de Shellshock):

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

Tomasz
fonte
2

Um dos exemplos mais simples de retorno de chamada no bash é 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 retornará 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

A forma de retorno de chamada para escrever é simplesmente:

#! /bin/bash
doSomething

Então, no crontab, você definiria algo como

0 0 * * *     doEveryDay.sh

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


Agora, considere 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'

Obviamente, a função every24hours nunca retorna. O Bash é um pouco único, pois podemos torná-lo assíncrono com muita facilidade e gerar um processo anexando &:

every24hours 'curl google.com' &

Se você não quiser isso como uma função, faça 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 no formulário acima, os retornos de chamada raramente são recursos diretamente dos 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 / função / script de código pode implementar retornos de chamada.

slebetman
fonte
Outros exemplos de programas / scripts que aceitam chamadas de retorno incluem watche find(quando utilizado com -execparâmetro)
slebetman
0

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

Aqui está um exemplo de um retorno de chamada inútil, mas simples, aproveitando os traps de sinal.

Primeiro, crie o script implementando 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 USR1sinal para o processo 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 o shell que implementa muitos recursos bashadotados 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 do shell é modificada ou referenciada (isto é, lida). Isso abre caminho para aplicativos mais direcionados a eventos.

Por exemplo, esse recurso permitiu que retornos de chamada no estilo X11 / Xt / Motif em widgets gráficos fossem implementados em uma versão antiga das kshextensões gráficas incluídas chamadas dtksh. Veja o manual do dksh .

jlliagre
fonte