Czy w Bash istnieje koncepcja programowania zwrotnego?

21

Kilka razy, kiedy czytałem o programowaniu, natknąłem się na koncepcję „oddzwaniania”.

Co zabawne, nigdy nie znalazłem wyjaśnienia, które mogę nazwać „dydaktycznym” lub „jasnym” dla tego terminu „funkcja zwrotna” (prawie każde wyjaśnienie, które przeczytałem, wydawało mi się dość odmienne od drugiego i czułem się zdezorientowany).

Czy w Bash istnieje koncepcja programowania zwrotnego? Jeśli tak, odpowiedz na mały, prosty przykład Bash.

JohnDoea
źródło
2
Czy „oddzwanianie” jest rzeczywistą koncepcją, czy też jest „funkcją pierwszej klasy”?
Cedric H.
declarative.bashInteresujące może być środowisko , które wyraźnie wykorzystuje funkcje skonfigurowane do wywoływania, gdy potrzebna jest dana wartość.
Charles Duffy,
Kolejne istotne ramy: bashup / events . Jego dokumentacja zawiera wiele prostych demonstracji użycia oddzwaniania, takich jak sprawdzanie poprawności, wyszukiwania itp.
PJ Eby
1
@CedricH. Zagłosowałem na ciebie. „Czy„ oddzwanianie ”to rzeczywista koncepcja, czy też jest to„ pierwszorzędna funkcja ”?” To dobre pytanie, które należy zadać jako kolejne pytanie?
prosody-Gab Vereable Kontekst
Rozumiem, że callback oznacza „funkcję, która jest wywoływana po wywołaniu danego zdarzenia”. Czy to jest poprawne?
JohnDoea,

Odpowiedzi:

44

W typowym programowaniu imperatywnym piszesz sekwencje instrukcji i są one wykonywane jeden po drugim, z wyraźnym przepływem sterowania. Na przykład:

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

itp.

Jak widać z przykładu, w programowaniu imperatywnym dość łatwo podążasz za procesem wykonywania, zawsze pracując w górę od dowolnego wiersza kodu, aby określić jego kontekst wykonania, wiedząc, że wszelkie instrukcje, które podasz, zostaną wykonane w wyniku ich lokalizacja w przepływie (lub lokalizacje ich witryn wywołujących, jeśli piszesz funkcje).

Jak wywołania zwrotne zmieniają przepływ

Kiedy używasz wywołań zwrotnych, zamiast umieszczania zestawu instrukcji „geograficznie”, określasz, kiedy należy go wywołać. Typowymi przykładami w innych środowiskach programowych są przypadki takie jak „pobierz ten zasób, a gdy pobieranie zostanie ukończone, oddzwoń”. Bash nie ma takiej ogólnej konstrukcji wywołania zwrotnego, ale ma wywołania zwrotne do obsługi błędów i kilku innych sytuacji; na przykład (najpierw trzeba zrozumieć podstawianie poleceń i tryby wyjścia Bash, aby zrozumieć ten przykład):

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

Jeśli chcesz to wypróbować samodzielnie, zapisz powyższe w pliku, powiedzmy cleanUpOnExit.sh, wykonaj go i uruchom:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Mój kod tutaj nigdy nie wywołuje jawnie cleanupfunkcji; mówi Basha kiedy to nazwać, używając trap cleanup EXIT, czyli „droga Bash, należy uruchomić cleanupkomendę przy wyjściu” (a cleanupzdarza się to funkcja I zdefiniowane wcześniej, ale może to być cokolwiek Bash rozumie). Bash obsługuje to dla wszystkich niekrytycznych sygnałów, wyjść, błędów poleceń i ogólnego debugowania (możesz określić wywołanie zwrotne uruchamiane przed każdą komendą). Oddzwanianie jest tutaj cleanupfunkcją, która jest „wywoływana” przez Basha tuż przed wyjściem powłoki.

Możesz użyć zdolności Basha do oceny parametrów powłoki jako poleceń, aby zbudować strukturę zorientowaną na wywołanie zwrotne; jest to nieco poza zakresem tej odpowiedzi i być może spowodowałoby więcej zamieszania, sugerując, że przekazywanie funkcji zawsze wiąże się z wywołaniami zwrotnymi. Zobacz Bash: przekaż funkcję jako parametr dla niektórych przykładów podstawowej funkcjonalności. Pomysł tutaj, podobnie jak w przypadku wywołań zwrotnych obsługi zdarzeń, polega na tym, że funkcje mogą przyjmować dane jako parametry, ale także inne funkcje - pozwala to dzwoniącym na zachowanie i dane. Prosty przykład takiego podejścia może wyglądać

#!/bin/bash

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

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

doonall backup "$@"

(Wiem, że jest to trochę bezużyteczne, ponieważ cpmoże poradzić sobie z wieloma plikami, to tylko dla ilustracji.)

Tutaj tworzymy funkcję, doonallktóra pobiera inne polecenie podane jako parametr i stosuje je do pozostałych parametrów; następnie używamy tego do wywołania backupfunkcji dla wszystkich parametrów podanych w skrypcie. Wynikiem jest skrypt, który kopiuje wszystkie argumenty, jeden po drugim, do katalogu kopii zapasowej.

Tego rodzaju podejście pozwala na pisanie funkcji z pojedynczymi obowiązkami: doonallodpowiedzialność polega na tym, aby uruchomić wszystkie argumenty, pojedynczo; backupobowiązkiem jest wykonanie kopii (jedynego) argumentu w katalogu kopii zapasowej. Zarówno doonalli backupmoże być używany w innych kontekstach, co pozwala na bardziej kodu ponowne wykorzystanie, lepszych testów itp

W tym przypadku wywołanie zwrotne jest backupfunkcją, którą nakazujemy doonall„oddzwonić” przy każdym z pozostałych argumentów - zapewniamy doonallzachowanie (pierwszy argument), a także dane (pozostałe argumenty).

(Zauważ, że w przypadku użycia pokazanym w drugim przykładzie sam nie użyłbym terminu „oddzwanianie”, ale być może jest to nawyk wynikający z języków, których używam. Myślę o tym jako o przekazywaniu funkcji lub lambdas w pobliżu , zamiast rejestrować połączenia zwrotne w systemie zorientowanym na zdarzenie).

Stephen Kitt
źródło
25

Po pierwsze, należy zauważyć, że to, co czyni funkcję funkcją zwrotną, to sposób jej użycia, a nie to, co robi. Oddzwonienie ma miejsce, gdy kod, który piszesz, jest wywoływany z kodu, którego nie napisałeś. Poprosisz system, aby oddzwonił, gdy wydarzy się jakieś szczególne zdarzenie.

Przykładem wywołania zwrotnego w programowaniu powłoki są pułapki. Pułapka to wywołanie zwrotne, które nie jest wyrażone jako funkcja, ale jako fragment kodu do oceny. Pytasz powłokę o wywołanie kodu, gdy powłoka odbierze określony sygnał.

Innym przykładem wywołania zwrotnego jest -execdziałanie findpolecenia. Zadaniem tego findpolecenia jest rekurencyjne przeglądanie katalogów i przetwarzanie każdego pliku po kolei. Domyślnie przetwarzanie ma na celu wydrukowanie nazwy pliku (niejawne -print), ale wraz -execz przetwarzaniem jest uruchomienie określonego polecenia. Jest to zgodne z definicją wywołania zwrotnego, chociaż w przypadku wywołań zwrotnych nie jest zbyt elastyczne, ponieważ wywołanie zwrotne działa w osobnym procesie.

Jeśli zaimplementowano funkcję wyszukiwania, można użyć funkcji wywołania zwrotnego do wywołania każdego pliku. Oto bardzo uproszczona funkcja znajdowania, która przyjmuje jako argument nazwę funkcji (lub zewnętrzną nazwę polecenia) i wywołuje ją na wszystkich zwykłych plikach w bieżącym katalogu i jego podkatalogach. Funkcja jest używana jako wywołanie zwrotne, które jest wywoływane za każdym razem, gdy call_on_regular_filesznajdzie zwykły plik.

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

Wywołania zwrotne nie są tak powszechne w programowaniu powłoki, jak w niektórych innych środowiskach, ponieważ powłoki są zaprojektowane głównie dla prostych programów. Oddzwanianie jest bardziej powszechne w środowiskach, w których przepływ danych i kontroli ma większe szanse na przechodzenie między częściami kodu, które są pisane i dystrybuowane niezależnie: system podstawowy, różne biblioteki, kod aplikacji.

Gilles „SO- przestań być zły”
źródło
1
Szczególnie ładnie wyjaśnione
roaima,
1
@JohnDoea Myślę, że pomysł polega na tym, że jest bardzo uproszczony, ponieważ nie jest to funkcja, którą naprawdę napisałbyś. Ale może jeszcze prostsze Przykładem może być coś z twardym zakodowane listy, aby uruchomić oddzwanianie: 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; }który można uruchomić jako foreach_server echo, foreach_server nslookupitp declare callback="$1"jest tak proste, jak może dostać choć: callback musi zostać przekazany w gdzieś, lub to nie jest oddzwanianie.
IMSoP,
4
„Oddzwonienie to wywołanie kodu, który piszesz, z kodu, którego nie napisałeś”. jest po prostu zły. Możesz napisać coś, co wykonuje pewne nieblokujące działania asynchroniczne, i uruchomić go z wywołaniem zwrotnym, które uruchomi się po zakończeniu. Nic nie ma związku z tym, kto napisał kod,
mikemaccana,
5
@mikemaccana Oczywiście możliwe jest, że ta sama osoba napisała dwie części kodu. Ale to nie jest powszechny przypadek. Wyjaśniam podstawy koncepcji, nie podając formalnej definicji. Jeśli wyjaśnisz wszystkie przypadki narożne, trudno jest przekazać podstawy.
Gilles „SO- przestań być zły”
1
Miło to słyszeć. Nie zgadzam się z tym, że ludzie piszący zarówno kod, który używa wywołania zwrotnego, jak i wywołanie zwrotne, nie jest powszechny lub jest przypadkiem granicznym, a ze względu na zamieszanie ta odpowiedź przekazuje podstawy.
mikemaccana
7

„wywołania zwrotne” to tylko funkcje przekazywane jako argumenty do innych funkcji.

Na poziomie powłoki oznacza to po prostu skrypty / funkcje / polecenia przekazywane jako argumenty do innych skryptów / funkcji / poleceń.

Teraz, dla prostego przykładu, rozważ następujący skrypt:

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

mając streszczenie

x command filter [file ...]

zastosuje się filterdo każdego fileargumentu, a następnie wywołaj commandz wyjściami filtrów jako argumentami.

Na przykład:

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

To bardzo blisko tego, co możesz zrobić w seplenieniu (tylko żart ;-))

Niektóre osoby nalegają na ograniczenie terminu „wywołanie zwrotne” do „modułu obsługi zdarzeń” i / lub „zamknięcia” (krotka funkcja + dane / środowisko); w żadnym wypadku nie jest to ogólnie przyjęte znaczenie. Jednym z powodów, dla których „oddzwanianie” w tych wąskich zmysłach nie ma większego zastosowania w powłoce, jest to, że potoki + równoległość + możliwości programowania dynamicznego są o wiele potężniejsze i już za nie płacisz pod względem wydajności, nawet jeśli spróbuj użyć powłoki jako niezgrabnej wersji perllub python.

mosvy
źródło
Chociaż twój przykład wygląda na całkiem użyteczny, jest wystarczająco gęsty, że musiałbym go naprawdę rozdzielić z otwartą instrukcją bash, aby dowiedzieć się, jak to działa (i pracowałem z prostszą grą przez większość dni od lat). Nigdy się nie nauczyłem seplenienie. ;)
Joe
1
@Joe jeśli to OK do pracy z tylko dwóch plików wejściowych i bez %interpolacji w filtrach, cała sprawa może być zmniejszona do: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Ale to o wiele mniej użyteczne i ilustrujące imho.
mosvy
1
Lub jeszcze lepiej$1 <($2 "$3") <($2 "$4")
mosvy
+1 dzięki. Twoje komentarze, a także wpatrywanie się w nią i granie z kodem przez jakiś czas, wyjaśniły mi to. Nauczyłem się także nowego terminu „interpolacja ciągów” dla czegoś, czego używałem od zawsze.
Joe
4

Rodzaj.

Jednym prostym sposobem na implementację wywołania zwrotnego w bash jest zaakceptowanie nazwy programu jako parametru, który działa jako „funkcja wywołania zwrotnego”.

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

Byłoby to wykorzystane w następujący sposób:

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

Oczywiście nie masz zamknięć w bash. Dlatego funkcja oddzwaniania nie ma dostępu do zmiennych po stronie wywołującej. Możesz jednak przechowywać dane potrzebne do wywołania zwrotnego w zmiennych środowiskowych. Przekazywanie informacji z powrotem do skryptu wywołującego jest trudniejsze. Dane mogą być umieszczone w pliku.

Jeśli twój projekt pozwala, aby wszystko było obsługiwane w jednym procesie, możesz użyć funkcji powłoki dla wywołania zwrotnego, aw tym przypadku funkcja wywołania zwrotnego ma oczywiście dostęp do zmiennych po stronie wywołującego.

użytkownik1934428
źródło
3

Wystarczy dodać kilka słów do pozostałych odpowiedzi. Funkcja oddzwaniania działa na funkcje poza funkcją, która oddzwania. Aby było to możliwe, do funkcji wywołującej musi zostać przekazana cała definicja funkcji, która ma zostać wywołana, lub jej kod powinien być dostępny dla funkcji wywołującej.

To pierwsze (przekazanie kodu do innej funkcji) jest możliwe, chociaż pominię przykład, który wiązałby się ze złożonością. To ostatnie (przekazywanie funkcji po nazwie) jest powszechną praktyką, ponieważ zmienne i funkcje zadeklarowane poza zakresem jednej funkcji są dostępne w tej funkcji, o ile ich definicja poprzedza wywołanie funkcji, która na nich działa (co z kolei , co ma zostać zadeklarowane przed wywołaniem).

Zauważ też, że podobnie dzieje się podczas eksportowania funkcji. Powłoka, która importuje funkcję, może mieć gotowy szkielet i tylko czeka na definicje funkcji, aby je uruchomić. Eksport funkcji jest obecny w Bash i powodował wcześniej poważne problemy, btw (tak zwane Shellshock):

Uzupełnię tę odpowiedź jeszcze jedną metodą przekazania funkcji do innej funkcji, która nie jest wyraźnie obecna w Bash. Ten przekazuje go według adresu, a nie imienia. Można to znaleźć na przykład w Perlu. Bash nie oferuje w ten sposób ani funkcji, ani zmiennych. Ale jeśli, jak oświadczasz, chcesz mieć szerszy obraz z Bash jako przykładem, powinieneś wiedzieć, że kod funkcji może znajdować się gdzieś w pamięci, a kod ten może być dostępny przez to miejsce w pamięci, które jest zwany jego adresem.

Tomasz
źródło
2

Jednym z najprostszych przykładów wywołania zwrotnego w bash jest to, że wiele osób jest zaznajomionych, ale nie zdaje sobie sprawy, z jakiego wzorca projektowego faktycznie korzystają:

cron

Cron pozwala na określenie pliku wykonywalnego (binarnego lub skryptu), który program cron oddzwoni, gdy zostaną spełnione określone warunki (specyfikacja czasu)

Załóżmy, że masz skrypt o nazwie doEveryDay.sh. Nieskryptowym sposobem napisania skryptu jest:

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

Sposób wywołania zwrotnego do napisania jest po prostu:

#! /bin/bash
doSomething

Następnie w crontab ustawiłbyś coś takiego

0 0 * * *     doEveryDay.sh

Nie trzeba wtedy pisać kodu, aby czekać na uruchomienie zdarzenia, ale zamiast tego polegać na cronponownym wywołaniu kodu.


Teraz zastanów się, JAK napisałeś ten kod w bash.

Jak wykonałbyś inny skrypt / funkcję w bash?

Napiszmy funkcję:

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
}

Teraz masz utworzoną funkcję, która akceptuje oddzwonienie. Możesz po prostu nazwać to tak:

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

Oczywiście funkcja co 24 godziny nigdy nie powraca. Bash jest nieco wyjątkowy, ponieważ możemy bardzo łatwo uczynić go asynchronicznym i spawnować proces, dodając &:

every24hours 'curl google.com' &

Jeśli nie chcesz tego jako funkcji, możesz to zrobić jako skrypt:

#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

Jak widać, wywołania zwrotne w bash są banalne. Jest to po prostu:

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

A oddzwonienie to po prostu:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Jak widać powyżej, wywołania zwrotne rzadko są bezpośrednią cechą języków. Zazwyczaj programują w sposób kreatywny, wykorzystując istniejące funkcje językowe. Każdy język, który może przechowywać wskaźnik / referencję / kopię jakiegoś bloku kodu / funkcji / skryptu, może implementować wywołania zwrotne.

Slebetman
źródło
Inne przykłady programów / skryptów, które akceptują wywołania zwrotne obejmują watchi find(gdy są używane z -execparametrem)
slebetman
0

Oddzwonienie to funkcja wywoływana, gdy wystąpi jakieś zdarzenie. Dzięki bashtemu jedyny dostępny mechanizm obsługi zdarzeń jest powiązany z sygnałami, wyjściem powłoki i rozszerzony na zdarzenia błędów powłoki, zdarzenia debugowania i skrypty funkcji / źródła zwracane zdarzenia.

Oto przykład bezużytecznych, ale prostych pułapek sygnału zwrotnego.

Najpierw utwórz skrypt implementujący wywołanie zwrotne:

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

Następnie uruchom skrypt w jednym terminalu:

$ ./callback-example

a na innym wyślij USR1sygnał do procesu powłoki.

$ pkill -USR1 callback-example

Każdy wysłany sygnał powinien wyzwalać wyświetlanie linii takich jak te w pierwszym terminalu:

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

ksh93, jako powłoka implementująca wiele bashpóźniejszych funkcji , zapewnia tak zwane „funkcje dyscypliny”. Funkcje te, niedostępne z bash, są wywoływane, gdy zmienna powłoki jest modyfikowana lub odwoływana (tj. Czytana). To otwiera drogę do ciekawszych aplikacji sterowanych zdarzeniami.

Na przykład ta funkcja umożliwiła zaimplementowanie wywołań zwrotnych w stylu X11 / Xt / Motif w widżetach graficznych w starej wersji kshdołączonych rozszerzeń graficznych o nazwie dtksh. Zobacz podręcznik dksh .

jlliagre
źródło