Существует ли в Bash концепция обратного вызова программирования?

21

Несколько раз, читая о программировании, я сталкивался с концепцией «обратного вызова».

Как ни странно, я так и не нашел объяснения, которое я мог бы назвать «дидактическим» или «понятным» для этого термина «функция обратного вызова» (почти любое прочитанное мной объяснение показалось мне достаточно отличным от другого, и я почувствовал растерянность).

Существует ли в Bash концепция обратного вызова программирования? Если это так, пожалуйста, ответьте с небольшим, простым примером Bash.

JohnDoea
источник
2
Является ли «обратный вызов» актуальной концепцией или это «первоклассная функция»?
Седрик Х.
Вы можете найти declarative.bashинтересным, как платформу, которая явно использует функции, сконфигурированные для вызова, когда необходимо данное значение.
Чарльз Даффи
Еще одна важная структура: bashup / events . Его документация включает в себя множество простых демонстраций использования обратного вызова, например, для проверки, поиска и т. Д.
PJ Eby
1
@CedricH. Проголосовал за тебя. «Является ли« обратный вызов »актуальной концепцией или это« первоклассная функция »?» - хороший вопрос, чтобы задать другой вопрос?
prosody-Gab Vereable Context
Я понимаю, что обратный вызов означает «функцию, которая вызывается после того, как данное событие было инициировано». Это верно?
JohnDoea

Ответы:

44

В типичном императивном программировании вы пишете последовательности инструкций, и они выполняются одна за другой с явным потоком управления. Например:

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

и т.п.

Как видно из примера, в императивном программировании вы достаточно легко следите за потоком выполнения, всегда пробираясь вверх от любой строки кода, чтобы определить его контекст выполнения, зная, что любые инструкции, которые вы дадите, будут выполнены в результате их выполнения. местоположение в потоке (или местоположение их сайтов вызовов, если вы пишете функции).

Как обратные вызовы меняют поток

Когда вы используете обратные вызовы, вместо того, чтобы использовать набор инструкций «географически», вы описываете, когда он должен быть вызван. Типичными примерами в других средах программирования являются такие случаи, как «загрузить этот ресурс, а когда загрузка будет завершена, вызвать этот обратный вызов». Bash не имеет общей конструкции обратного вызова такого рода, но он имеет обратные вызовы для обработки ошибок и некоторых других ситуаций; например (чтобы понять пример, нужно сначала понять подстановку команд и режимы выхода Bash ):

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

Если вы хотите попробовать это сами, сохраните вышеупомянутое в файле, скажем cleanUpOnExit.sh, сделайте его исполняемым и запустите его:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Мой код здесь никогда явно не вызывает cleanupфункцию; он сообщает Bash, когда его вызывать, используя trap cleanup EXIT, например, «дорогой Bash, пожалуйста, запустите cleanupкоманду при выходе» (и cleanupэто функция, которую я определил ранее, но это может быть все, что понимает Bash). Bash поддерживает это для всех нефатальных сигналов, выходов, сбоев команд и общей отладки (вы можете указать обратный вызов, который запускается перед каждой командой). Обратный вызов здесь - это cleanupфункция, которую Bash «вызывает» перед выходом из оболочки.

Вы можете использовать способность Bash оценивать параметры оболочки в виде команд, чтобы создать ориентированную на обратный вызов структуру; это несколько выходит за рамки этого ответа, и, возможно, приведет к еще большей путанице, если предположить, что передача функций всегда связана с обратными вызовами. См. Bash: передать функцию в качестве параметра для некоторых примеров базовой функциональности. Идея здесь, как и в случае обратных вызовов обработки событий, заключается в том, что функции могут принимать данные в качестве параметров, но также и другие функции - это позволяет вызывающим сторонам предоставлять поведение, а также данные. Простой пример такого подхода может выглядеть так

#!/bin/bash

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

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

doonall backup "$@"

(Я знаю, что это немного бесполезно, поскольку cpможет работать с несколькими файлами, это только для иллюстрации.)

Здесь мы создаем функцию, doonallкоторая принимает другую команду, заданную в качестве параметра, и применяет ее к остальным ее параметрам; затем мы используем это для вызова backupфункции по всем параметрам, переданным скрипту. В результате получается скрипт, который копирует все свои аргументы, один за другим, в каталог резервных копий.

Такой подход позволяет писать функции с единственной ответственностью: doonallответственность состоит в том, чтобы запускать что-то по всем своим аргументам, по одному за раз; backupответственность заключается в том, чтобы сделать копию своего (единственного) аргумента в резервном каталоге. И то, doonallи другое backupможно использовать в других контекстах, что позволяет больше повторного использования кода, лучшие тесты и т. Д.

В этом случае обратный вызов - это backupфункция, которую мы говорим doonall«перезванивать» по каждому из ее других аргументов - мы предоставляем doonallповедение (его первый аргумент), а также данные (остальные аргументы).

(Обратите внимание, что в случае использования, продемонстрированном во втором примере, я бы сам не использовал термин «обратный вызов», но, возможно, это привычка, вытекающая из языков, которые я использую. Я думаю об этом как о передаче функций или лямбд вокруг вместо регистрации обратных вызовов в ориентированной на события системе.)

Стивен Китт
источник
25

Во-первых, важно отметить, что то, что делает функцию функцией обратного вызова, - это то, как она используется, а не то, что она делает. Обратный вызов - это когда код, который вы пишете, вызывается из кода, который вы не написали. Вы просите систему перезвонить вам, когда происходит какое-то конкретное событие.

Примером обратного вызова в программировании оболочки являются ловушки. Ловушка - это обратный вызов, который выражается не как функция, а как кусок кода для оценки. Вы просите, чтобы оболочка вызывала ваш код, когда оболочка получает определенный сигнал.

Другим примером обратного вызова является -execдействие findкоманды. Задача findкоманды - рекурсивно обходить каталоги и обрабатывать каждый файл по очереди. По умолчанию обработка состоит в том, чтобы напечатать имя файла (неявное -print), а при -execобработке - запустить указанную вами команду. Это соответствует определению обратного вызова, хотя при обратном вызове он не очень гибкий, так как обратный вызов выполняется в отдельном процессе.

Если вы реализовали функцию поиска типа, вы можете использовать функцию обратного вызова для вызова каждого файла. Вот очень упрощенная функция поиска типа, которая принимает имя функции (или имя внешней команды) в качестве аргумента и вызывает его для всех обычных файлов в текущем каталоге и его подкаталогах. Функция используется в качестве обратного вызова, который вызывается каждый раз, когда call_on_regular_filesнаходит обычный файл.

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

Обратные вызовы не так распространены в программировании оболочки, как в некоторых других средах, потому что оболочки в первую очередь предназначены для простых программ. Обратные вызовы более распространены в средах, где поток данных и управления с большей вероятностью перемещается назад и вперед между частями кода, которые написаны и распределены независимо: базовая система, различные библиотеки, код приложения.

Жиль "ТАК - перестать быть злым
источник
1
Особенно приятно объяснил
Ройма
1
@JohnDoea Я думаю, что идея в том, что он очень упрощен, потому что это не та функция, которую вы действительно написали бы. Но, возможно, еще более простым примером будет что-то с жестко запрограммированным списком для выполнения обратного вызова: 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; }который вы можете запустить как foreach_server echo, foreach_server nslookupи т. Д. Это declare callback="$1"примерно настолько просто, насколько это возможно получить: обратный вызов должен быть где-то передан, или это не обратный вызов.
IMSoP
4
«Обратный вызов - это когда код, который вы пишете, вызывается из кода, который вы не написали». это просто неправильно. Вы можете написать вещь, которая выполняет некоторую неблокирующую асинхронную работу, и запустить ее с обратным вызовом, который будет выполнен после завершения. Ничто не связано с тем, кто написал код,
mikemaccana
5
@mikemaccana Конечно, возможно, что один и тот же человек написал две части кода. Но это не частый случай. Я объясняю основы концепции, а не даю формальное определение. Если вы объясните все угловые случаи, трудно передать основы.
Жиль "ТАК - перестань быть злым"
1
Рад слышать это. Я не согласен с тем, что люди, пишущие как код, который использует обратный вызов, так и обратный вызов, не распространены или являются крайним случаем, и, из-за путаницы, этот ответ передает основы.
mikemaccana
7

«Обратные вызовы» - это просто функции, передаваемые в качестве аргументов другим функциям.

На уровне оболочки это просто означает, что скрипты / функции / команды передаются в качестве аргументов другим скриптам / функциям / командам.

Теперь для простого примера рассмотрим следующий скрипт:

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

с кратким изложением

x command filter [file ...]

будет применяться filterк каждому fileаргументу, а затем вызывать commandвыходные данные фильтров в качестве аргументов.

Например:

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

Это очень близко к тому, что вы можете сделать в lisp (шучу ;-))

Некоторые люди настаивают на ограничении термина «обратный вызов» термином «обработчик событий» и / или «закрытие» (кортеж функция + данные / окружение); это, никоим образом обычно принимается смысл. И одна из причин, по которой «обратные вызовы» в этих узких смыслах не очень полезны в оболочке, заключается в том, что каналы + параллелизм + возможности динамического программирования намного мощнее, и вы уже платите за них с точки зрения производительности, даже если вы попробуйте использовать оболочку как неуклюжую версию perlили python.

mosvy
источник
Хотя ваш пример выглядит довольно полезным, он достаточно плотный, поэтому мне пришлось бы по-настоящему его разобрать с открытым руководством по bash, чтобы выяснить, как он работает (а я работал с более простым bash большинство дней в течение многих лет). Я никогда не учился шепелявость. ;)
Джо
1
@Joe , если это нормально , чтобы работать только с двумя входными файлами и не %интерполяции в фильтрах, все это может быть сведено к: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Но это гораздо менее полезный и иллюстративный имхо.
Мосви
1
Или еще лучше$1 <($2 "$3") <($2 "$4")
Мосви
+1 Спасибо. Ваши комментарии, а также пялиние на него и какое-то время играли с кодом, прояснили это для меня. Я также выучил новый термин «интерполяция строк» ​​для чего-то, что я использовал всегда.
Джо
4

Вид.

Один простой способ реализовать обратный вызов в bash - это принять имя программы в качестве параметра, который действует как «функция обратного вызова».

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

Это будет использоваться так:

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

Конечно, у вас нет замыканий в bash. Следовательно, функция обратного вызова не имеет доступа к переменным на стороне вызывающей стороны. Однако вы можете хранить данные, необходимые для обратного вызова, в переменных среды. Передача информации обратно из обратного вызова в скрипт invoker сложнее. Данные могут быть помещены в файл.

Если ваш дизайн допускает, что все обрабатывается в одном процессе, вы можете использовать функцию оболочки для обратного вызова, и в этом случае функция обратного вызова, конечно, имеет доступ к переменным на стороне вызывающего.

user1934428
источник
3

Просто чтобы добавить несколько слов к другим ответам. Функция обратного вызова работает с функциями, внешними по отношению к функции, которая вызывает вызов. Для того чтобы это было возможно, либо полное определение функции, которая должна быть вызвана обратно, необходимо передать функции, вызывающей функцию, либо ее код должен быть доступен функции, вызывающей функцию.

Первый вариант (передача кода в другую функцию) возможен, хотя я пропущу пример, поскольку это повлечет за собой сложность. Последнее (передача функции по имени) является обычной практикой, поскольку переменные и функции, объявленные вне области действия одной функции, доступны в этой функции, если их определение предшествует вызову функции, которая над ними работает (что, в свою очередь, , как будет объявлено до того, как он будет вызван).

Также обратите внимание, что подобное происходит и при экспорте функций. Оболочка, которая импортирует функцию, может иметь готовую платформу и просто ждать, пока определения функции приведут их в действие. Функция экспорта присутствует в Bash и вызывает ранее серьезные проблемы, кстати (это называлось Shellshock):

Я дополню этот ответ еще одним способом передачи функции в другую функцию, которая явно отсутствует в Bash. Этот передает его по адресу, а не по имени. Это можно найти в Perl, например. Bash предлагает этот способ ни для функций, ни для переменных. Но если, как вы заявляете, вы хотите иметь более широкую картинку с Bash в качестве примера, то вы должны знать, что код функции может находиться где-то в памяти, и этот код может быть доступен из той области памяти, которая назвал его адрес.

Tomasz
источник
2

Один из простейших примеров обратного вызова в bash - это тот, с которым многие знакомы, но не понимают, какой шаблон дизайна они на самом деле используют:

хрон

Cron позволяет вам указать исполняемый файл (двоичный файл или скрипт), который программа cron будет вызывать при выполнении некоторых условий (указание времени)

Скажем, у вас есть сценарий под названием doEveryDay.sh. Способ написания сценария без обратного вызова:

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

Способ обратного вызова написать это просто:

#! /bin/bash
doSomething

Затем в crontab вы установите что-то вроде

0 0 * * *     doEveryDay.sh

Тогда вам не нужно было бы писать код для ожидания события, а вместо этого полагаться на cronобратный вызов кода.


Теперь рассмотрим, как бы вы написали этот код на bash.

Как бы вы выполнили другой скрипт / функцию в bash?

Давайте напишем функцию:

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
}

Теперь вы создали функцию, которая принимает обратный вызов. Вы можете просто назвать это так:

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

Конечно, функция каждые 24 часа никогда не возвращается. Bash немного уникален тем, что мы можем очень легко сделать его асинхронным и запустить процесс, добавив &:

every24hours 'curl google.com' &

Если вы не хотите использовать это как функцию, вместо этого вы можете сделать это как скрипт:

#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

Как видите, обратные вызовы в bash тривиальны. Это просто:

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

И вызвать обратный вызов просто:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Как видно из приведенной выше формы, обратные вызовы редко являются прямым признаком языков. Они обычно программируют творчески, используя существующие языковые возможности. Любой язык, который может хранить указатель / ссылку / копию некоторого блока кода / функции / скрипта, может реализовать обратные вызовы.

slebetman
источник
Другие примеры программ / сценариев, которые принимают обратные вызовы, включают watchи find(при использовании с -execпараметром)
slebetman
0

Обратный вызов - это функция, вызываемая при возникновении какого-либо события. При bashэтом единственный действующий механизм обработки событий связан с сигналами, выходом из оболочки и распространяется на события ошибок оболочки, события отладки и сценарии возврата функций / источников.

Вот пример бесполезного, но простого обратного вызова, использующего сигнальные ловушки.

Сначала создайте скрипт, реализующий обратный вызов:

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

Затем запустите скрипт в одном терминале:

$ ./callback-example

и на другом, отправьте USR1сигнал процессу оболочки.

$ pkill -USR1 callback-example

Каждый отправленный сигнал должен вызывать отображение строк, подобных этим, в первом терминале:

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

ksh93В качестве оболочки, реализующей многие функции, которые bashвпоследствии были приняты, она предоставляет то, что она называет «дисциплинарными функциями». Эти функции, недоступные с bash, вызываются, когда переменная оболочки модифицируется или на нее ссылаются (т. Е. Читает). Это открывает путь к более интересным приложениям, управляемым событиями.

Например, эта функция позволяла реализовывать обратные вызовы в стиле X11 / Xt / Motif для графических виджетов в старой версии ksh, включающей в себя графические расширения, называемые dtksh. Смотрите руководство по dksh .

jlliagre
источник