Чи існує в Bash концепція програмування зворотного дзвінка?


21

Кілька разів, коли я читав про програмування, я стикався з концепцією "зворотного виклику".

Як не дивно, я ніколи не знайшов пояснення, яке можу назвати "дидактичним" або "зрозумілим" для цього терміна "функцією зворотного виклику" (майже будь-яке пояснення, яке я прочитав, здавалося мені досить відмінним від іншого, і я відчував розгубленість).

Чи існує в Bash концепція програмування зворотного дзвінка? Якщо так, будь ласка, дайте відповідь невеликим, простим прикладом Баша.


2
Це "зворотний виклик" - це фактичне поняття чи це "першокласна функція"?
Седрик Х.

Вам може бути declarative.bashцікаво, як рамка, яка явно використовує функції, налаштовані для виклику, коли потрібно задане значення.
Чарльз Даффі

Ще одна відповідна рамка: башуп / події . Документація включає в себе безліч простих демонстрацій використання зворотного дзвінка, таких як перевірка, пошук і т.д.
PJ Eby

1
@CedricH. Голосували за вас. "Це" зворотний виклик "- це власне поняття чи це" першокласна функція "?" - це гарне запитання як інше питання?
Прозоді-Габ Верестний контекст

Я розумію, що зворотний виклик означає "функцію, яка викликається назад після запуску певної події". Це правильно?
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, наприклад, "Шановний Баш, будь ласка, запустіть cleanupкоманду, коли ви виходите" (і cleanupце буде функцією, яку я визначив раніше, але це може бути все, що Баш розуміє). 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
Особливо красиво пояснено
roaima

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.


Хоча ваш приклад виглядає досить корисним, він досить щільний, що я мусив би насправді відібрати його разом із відкритим посібником з bash, щоб зрозуміти, як це працює (і я працював з більш простим башем протягом багатьох днів.) Я ніколи не вчився лисити ;)
Джо

1
@Joe , якщо це нормально , щоб працювати тільки з двома вхідними файлами і не %інтерполяції в фільтрах, все це може бути зведене до: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Але це набагато менш корисний та показовий imho.
mosvy

1
Або ще краще$1 <($2 "$3") <($2 "$4")
mosvy

+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

Звичайно, у вас не закриваються в баш. Отже, функція зворотного дзвінка не має доступу до змінних на стороні виклику. Однак ви можете зберігати дані, необхідні для зворотного дзвінка, у змінних оточення. Повернення інформації назад із зворотного дзвінка до сценарію виклику складніше. Дані можна помістити у файл.

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


3

Просто додати кілька слів до інших відповідей. Зворотний виклик функції працює на функціях, які є зовнішніми за функцію, яка передзвонює. Щоб це стало можливим, або ціле визначення функції, яку потрібно викликати, має бути передано функції, що викликає зворотний зв'язок, або її код повинен бути доступний для функції, що викликає зворотний зв'язок.

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

Також зауважте, що подібне відбувається і під час експорту функцій. Оболонка, яка імпортує функцію, може мати готовий фреймворк і просто чекає визначень функції, щоб ввести їх у дію. Експорт функцій присутній у Bash і спричинив раніше серйозні проблеми, btw (що називалося Shellshock):

Я завершу цю відповідь ще одним методом переходу функції до іншої функції, який явно не присутній у Bash. Цей передає його за адресою, а не по імені. Це можна знайти, наприклад, у Perl. Bash пропонує таким чином ні функції, ні змінні. Але якщо, як ви заявляєте, ви хочете мати більш широку картину з Bash як лише приклад, тоді ви повинні знати, що код функції може знаходитися десь у пам'яті, і до цього коду може бути доступне те місце пам'яті, яке є називається його адреса.


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

Як бачите, зворотні дзвінки в баші - тривіальні. Це просто:

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

А зворотний виклик викликає просто:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Як ви бачите форму вище, зворотні виклики рідко є безпосередньо функціями мов. Зазвичай вони програмують творчо, використовуючи існуючі мовні функції. Будь-яка мова, яка може зберігати вказівник / довідку / копію якогось кодового блоку / функції / скрипту, може реалізовувати зворотні дзвінки.


Інші приклади програм / скриптів, які приймають зворотні дзвінки, включають 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 .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.